Skip to content

Commit 08645f3

Browse files
authored
feat(dsl): add duplicate message tag detection with compilation errors (#9)
1 parent bd8584e commit 08645f3

File tree

2 files changed

+135
-2
lines changed

2 files changed

+135
-2
lines changed

lib/engine_system/engine/dsl/interface_builder.ex

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,91 @@ defmodule EngineSystem.Engine.DSL.InterfaceBuilder do
1111
"""
1212
defmacro interface(do: block) do
1313
quote do
14-
# Temporarily store current interface
14+
# Temporarily store current interface and all message definitions (including duplicates)
1515
Module.put_attribute(__MODULE__, :current_interface, [])
16+
Module.put_attribute(__MODULE__, :all_message_definitions, [])
1617
unquote(block)
1718

19+
# Validate for duplicates before updating spec
20+
all_definitions =
21+
Module.get_attribute(__MODULE__, :all_message_definitions) |> Enum.reverse()
22+
23+
current_interface = Module.get_attribute(__MODULE__, :current_interface)
24+
25+
case EngineSystem.Engine.DSL.InterfaceBuilder.validate_duplicate_tags(all_definitions) do
26+
:ok ->
27+
:ok
28+
29+
{:error, duplicate_info} ->
30+
{tag, first_location, duplicate_location} = duplicate_info
31+
32+
raise CompileError,
33+
file: duplicate_location.file,
34+
line: duplicate_location.line,
35+
description: """
36+
duplicate message tag #{inspect(tag)}
37+
First definition at #{first_location.file}:#{first_location.line}
38+
Duplicate definition at #{duplicate_location.file}:#{duplicate_location.line}
39+
40+
Suggestion: Use different tag names like #{inspect(:"#{tag}_by_key")} and #{inspect(:"#{tag}_by_id")}
41+
"""
42+
end
43+
1844
# Update spec with collected interface
1945
spec_data = Module.get_attribute(__MODULE__, :engine_spec_data)
20-
interface = Module.get_attribute(__MODULE__, :current_interface) |> Enum.reverse()
46+
interface = current_interface |> Enum.reverse()
2147
updated_spec = %{spec_data | interface: interface}
2248
Module.put_attribute(__MODULE__, :engine_spec_data, updated_spec)
2349
Module.delete_attribute(__MODULE__, :current_interface)
50+
Module.delete_attribute(__MODULE__, :all_message_definitions)
2451
end
2552
end
2653

2754
@doc """
2855
I define a message type in the interface.
2956
"""
3057
defmacro message(tag, fields \\ []) do
58+
location = %{
59+
file: __CALLER__.file,
60+
line: __CALLER__.line
61+
}
62+
3163
quote do
3264
current_interface = Module.get_attribute(__MODULE__, :current_interface)
65+
all_definitions = Module.get_attribute(__MODULE__, :all_message_definitions)
66+
67+
# Add this definition to our tracking list
68+
Module.put_attribute(__MODULE__, :all_message_definitions, [
69+
{unquote(tag), unquote(Macro.escape(location))} | all_definitions
70+
])
3371

72+
# Add to interface as well
3473
Module.put_attribute(__MODULE__, :current_interface, [
3574
{unquote(tag), unquote(fields)} | current_interface
3675
])
3776
end
3877
end
3978

79+
@doc """
80+
I validate for duplicate message tags and return detailed error information.
81+
"""
82+
def validate_duplicate_tags(all_definitions) do
83+
find_first_duplicate(all_definitions, %{})
84+
end
85+
86+
# Helper function to find the first duplicate and its locations
87+
defp find_first_duplicate([], _seen), do: :ok
88+
89+
defp find_first_duplicate([{tag, location} | rest], seen) do
90+
case Map.get(seen, tag) do
91+
nil ->
92+
find_first_duplicate(rest, Map.put(seen, tag, location))
93+
94+
first_location ->
95+
{:error, {tag, first_location, location}}
96+
end
97+
end
98+
4099
@doc """
41100
I validate a message interface definition.
42101
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
defmodule EngineSystem.Unit.DuplicateMessageTest do
2+
use ExUnit.Case, async: true
3+
4+
describe "duplicate message tag detection" do
5+
test "should raise compilation error for duplicate message tags" do
6+
assert_raise CompileError, ~r/duplicate message tag/, fn ->
7+
defmodule TestEngineWithDuplicates do
8+
import EngineSystem.Engine.DSL
9+
10+
defengine TestEngineWithDuplicates do
11+
version("1.0.0")
12+
mode(:process)
13+
14+
interface do
15+
message(:get, key: :binary)
16+
message(:put, key: :binary, value: :any)
17+
# Duplicate tag - should be an error!
18+
message(:get, id: :integer)
19+
message(:delete, key: :binary)
20+
end
21+
end
22+
end
23+
end
24+
end
25+
26+
test "should accept unique message tags without error" do
27+
# This should compile without any errors
28+
defmodule TestEngineWithUniqueMessages do
29+
import EngineSystem.Engine.DSL
30+
31+
defengine TestEngineWithUniqueMessages do
32+
version("1.0.0")
33+
mode(:process)
34+
35+
interface do
36+
message(:get, key: :binary)
37+
message(:put, key: :binary, value: :any)
38+
message(:delete, key: :binary)
39+
message(:list)
40+
end
41+
end
42+
end
43+
44+
# If we get here, compilation succeeded
45+
assert true
46+
end
47+
48+
test "should provide detailed error message with line numbers" do
49+
exception =
50+
assert_raise CompileError, fn ->
51+
defmodule TestEngineWithDetailedDuplicate do
52+
import EngineSystem.Engine.DSL
53+
54+
defengine TestEngineWithDetailedDuplicate do
55+
version("1.0.0")
56+
mode(:process)
57+
58+
interface do
59+
message(:user_action, type: :create)
60+
# This should provide line info
61+
message(:user_action, type: :update)
62+
end
63+
end
64+
end
65+
end
66+
67+
# Check that the error message contains useful information
68+
assert exception.description =~ "duplicate message tag :user_action"
69+
assert exception.description =~ "First definition at"
70+
assert exception.description =~ "Duplicate definition at"
71+
assert exception.description =~ "Suggestion:"
72+
end
73+
end
74+
end

0 commit comments

Comments
 (0)