Skip to content

Commit 48fd12f

Browse files
committed
Made type aliases a thing
While working on the automatic protocol generators, it became clear that type aliases needed to be their own thing, as they operate quite differently from the other defined things in the jsonrpc protocol. Since they're just aliases, it makes sense to keep their definitions on hand and then spit them out when other things make use of them during encode and decode. This did require going back to encoding and ensuring all the encode functions return OK tuples.
1 parent a763669 commit 48fd12f

File tree

9 files changed

+178
-17
lines changed

9 files changed

+178
-17
lines changed

apps/language_server/.formatter.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ impossible_to_format = [
44
]
55

66
proto_dsl = [
7+
defalias: 1,
78
defenum: 1,
89
defnotification: 2,
910
defnotification: 3,

apps/language_server/lib/language_server/experimental/protocol/proto.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto do
77
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.LspTypes
88

99
import ElixirLS.LanguageServer.Experimental.Protocol.Proto.TypeFunctions
10+
import Proto.Alias, only: [defalias: 1]
1011
import Proto.Enum, only: [defenum: 1]
1112
import Proto.Notification, only: [defnotification: 2, defnotification: 3]
1213
import Proto.Request, only: [defrequest: 3]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Alias do
2+
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.CompileMetadata
3+
4+
defmacro defalias(alias_definition) do
5+
caller_module = __CALLER__.module
6+
CompileMetadata.add_type_alias_module(caller_module)
7+
8+
quote location: :keep do
9+
def definition do
10+
unquote(alias_definition)
11+
end
12+
13+
def __meta__(:type) do
14+
:type_alias
15+
end
16+
17+
def __meta__(:param_names) do
18+
[]
19+
end
20+
end
21+
end
22+
end

apps/language_server/lib/language_server/experimental/protocol/proto/compile_metadata.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.CompileMetadata do
55

66
@notification_modules_key {__MODULE__, :notification_modules}
77
@type_modules_key {__MODULE__, :type_modules}
8+
@type_alias_modules_key {__MODULE__, :type_alias_modules}
89
@request_modules_key {__MODULE__, :request_modules}
910
@response_modules_key {__MODULE__, :response_modules}
1011

@@ -20,6 +21,10 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.CompileMetadata do
2021
:persistent_term.get(@response_modules_key, [])
2122
end
2223

24+
def type_alias_modules do
25+
:persistent_term.get(@type_alias_modules_key)
26+
end
27+
2328
def type_modules do
2429
:persistent_term.get(@type_modules_key)
2530
end
@@ -40,6 +45,10 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.CompileMetadata do
4045
add_module(@type_modules_key, module)
4146
end
4247

48+
def add_type_alias_module(module) do
49+
add_module(@type_alias_modules_key, module)
50+
end
51+
4352
defp update(key, initial_value, update_fn) do
4453
case :persistent_term.get(key, :not_found) do
4554
:not_found -> :persistent_term.put(key, initial_value)

apps/language_server/lib/language_server/experimental/protocol/proto/enum.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Enum do
2222
for {name, value} <- opts do
2323
quote location: :keep do
2424
def encode(unquote(name)) do
25-
unquote(value)
25+
{:ok, unquote(value)}
2626
end
2727
end
2828
end
@@ -36,6 +36,10 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Enum do
3636

3737
unquote_splicing(encoders)
3838

39+
def encode(val) do
40+
{:error, {:invalid_value, __MODULE__, val}}
41+
end
42+
3943
unquote_splicing(enum_macros)
4044

4145
def __meta__(:types) do

apps/language_server/lib/language_server/experimental/protocol/proto/field.ex

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Field do
5151
{:ok, orig_value}
5252
end
5353

54+
def extract(:float, _name, orig_value) when is_float(orig_value) do
55+
{:ok, orig_value}
56+
end
57+
5458
def extract(:string, _name, orig_value) when is_binary(orig_value) do
5559
{:ok, orig_value}
5660
end
@@ -59,8 +63,12 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Field do
5963
{:ok, orig_value}
6064
end
6165

66+
def extract({:type_alias, alias_module}, name, orig_value) do
67+
extract(alias_module.definition(), name, orig_value)
68+
end
69+
6270
def extract(module, _name, orig_value)
63-
when is_atom(module) and module not in [:integer, :string, :boolean] do
71+
when is_atom(module) and module not in [:integer, :string, :boolean, :float] do
6472
module.parse(orig_value)
6573
end
6674

@@ -103,15 +111,15 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Field do
103111
end
104112

105113
def encode(:any, field_value) do
106-
field_value
114+
{:ok, field_value}
107115
end
108116

109117
def encode({:literal, value}, _) do
110-
value
118+
{:ok, value}
111119
end
112120

113121
def encode({:optional, _}, nil) do
114-
:"$__drop__"
122+
{:ok, :"$__drop__"}
115123
end
116124

117125
def encode({:optional, field_type}, field_value) do
@@ -128,44 +136,99 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Field do
128136
end
129137

130138
def encode({:list, list_type}, field_value) when is_list(field_value) do
131-
Enum.map(field_value, &encode(list_type, &1))
139+
encoded =
140+
Enum.reduce_while(field_value, [], fn element, acc ->
141+
case encode(list_type, element) do
142+
{:ok, encoded} -> {:cont, [encoded | acc]}
143+
error -> {:halt, error}
144+
end
145+
end)
146+
147+
case encoded do
148+
encoded_list when is_list(encoded_list) ->
149+
{:ok, Enum.reverse(encoded_list)}
150+
151+
error ->
152+
error
153+
end
132154
end
133155

134-
def encode(:integer, field_value) do
135-
field_value
156+
def encode(:integer, field_value) when is_integer(field_value) do
157+
{:ok, field_value}
158+
end
159+
160+
def encode(:integer, string_value) when is_binary(string_value) do
161+
case Integer.parse(string_value) do
162+
{int_value, ""} -> {:ok, int_value}
163+
_ -> {:error, {:invalid_integer, string_value}}
164+
end
165+
end
166+
167+
def encode(:float, float_value) when is_float(float_value) do
168+
{:ok, float_value}
136169
end
137170

138171
def encode(:string, field_value) when is_binary(field_value) do
139-
field_value
172+
{:ok, field_value}
140173
end
141174

142175
def encode(:boolean, field_value) when is_boolean(field_value) do
143-
field_value
176+
{:ok, field_value}
144177
end
145178

146179
def encode({:map, value_type, _}, field_value) when is_map(field_value) do
147-
Map.new(field_value, fn {k, v} -> {k, encode(value_type, v)} end)
180+
map_fields =
181+
Enum.reduce_while(field_value, [], fn {key, value}, acc ->
182+
case encode(value_type, value) do
183+
{:ok, encoded_value} -> {:cont, [{key, encoded_value} | acc]}
184+
error -> {:halt, error}
185+
end
186+
end)
187+
188+
case map_fields do
189+
fields when is_list(fields) -> {:ok, Map.new(fields)}
190+
error -> error
191+
end
148192
end
149193

150194
def encode({:params, param_defs}, field_value) when is_map(field_value) do
151-
Map.new(param_defs, fn {param_name, param_type} ->
152-
{param_name, encode(param_type, Map.get(field_value, param_name))}
153-
end)
195+
param_fields =
196+
Enum.reduce_while(param_defs, [], fn {param_name, param_type}, acc ->
197+
unencoded = Map.get(field_value, param_name)
198+
199+
case encode(param_type, unencoded) do
200+
{:ok, encoded_value} -> {:cont, [{param_name, encoded_value} | acc]}
201+
error -> {:halt, error}
202+
end
203+
end)
204+
205+
case param_fields do
206+
fields when is_list(fields) -> {:ok, Map.new(fields)}
207+
error -> error
208+
end
154209
end
155210

156211
def encode({:constant, constant_module}, field_value) do
157-
constant_module.encode(field_value)
212+
{:ok, constant_module.encode(field_value)}
213+
end
214+
215+
def encode({:type_alias, alias_module}, field_value) do
216+
encode(alias_module.definition(), field_value)
158217
end
159218

160219
def encode(module, field_value) when is_atom(module) do
161220
if function_exported?(module, :encode, 1) do
162221
module.encode(field_value)
163222
else
164-
field_value
223+
{:ok, field_value}
165224
end
166225
end
167226

168227
def encode(_, nil) do
169228
nil
170229
end
230+
231+
def encode(type, value) do
232+
{:error, {:invalid_type, type, value}}
233+
end
171234
end

apps/language_server/lib/language_server/experimental/protocol/proto/macros/json.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Macros.Json do
88
encoded_pairs =
99
for {field_name, field_type} <- unquote(dest_module).__meta__(:types),
1010
field_value = get_field_value(value, field_name),
11-
encoded_value = Field.encode(field_type, field_value),
11+
{:ok, encoded_value} = Field.encode(field_type, field_value),
1212
encoded_value != :"$__drop__" do
1313
{field_name, encoded_value}
1414
end

apps/language_server/lib/language_server/experimental/protocol/proto/type_functions.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.TypeFunctions do
33
:integer
44
end
55

6+
def float do
7+
:float
8+
end
9+
610
def string do
711
:string
812
end
@@ -15,6 +19,10 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.TypeFunctions do
1519
:string
1620
end
1721

22+
def type_alias(alias_module) do
23+
{:type_alias, alias_module}
24+
end
25+
1826
def literal(what) do
1927
{:literal, what}
2028
end

apps/language_server/test/experimental/protocol/proto_test.exs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,23 @@ defmodule ElixirLS.LanguageServer.Experimental.ProtoTest do
5050
end
5151
end
5252

53+
describe "float fields" do
54+
defmodule FloatField do
55+
use Proto
56+
deftype float_field: float()
57+
end
58+
59+
test "can parse a float field" do
60+
assert {:ok, val} = FloatField.parse(%{"floatField" => 494.02})
61+
assert val.float_field == 494.02
62+
end
63+
64+
test "rejects nil float fields" do
65+
assert {:error, {:invalid_value, :float_field, "string"}} =
66+
FloatField.parse(%{"floatField" => "string"})
67+
end
68+
end
69+
5370
describe "list fields" do
5471
defmodule ListField do
5572
use Proto
@@ -99,6 +116,42 @@ defmodule ElixirLS.LanguageServer.Experimental.ProtoTest do
99116
end
100117
end
101118

119+
describe "type aliases" do
120+
defmodule TypeAlias do
121+
use Proto
122+
defalias one_of([string(), list_of(string())])
123+
end
124+
125+
defmodule UsesAlias do
126+
use Proto
127+
128+
deftype alias: type_alias(TypeAlias), name: string()
129+
end
130+
131+
test "parses a single item correctly" do
132+
assert {:ok, uses} = UsesAlias.parse(%{"name" => "uses", "alias" => "foo"})
133+
assert uses.name == "uses"
134+
assert uses.alias == "foo"
135+
end
136+
137+
test "parses a list correctly" do
138+
assert {:ok, uses} = UsesAlias.parse(%{"name" => "uses", "alias" => ["foo", "bar"]})
139+
assert uses.name == "uses"
140+
assert uses.alias == ~w(foo bar)
141+
end
142+
143+
test "encodes correctly" do
144+
assert {:ok, encoded} = encode_and_decode(UsesAlias.new(alias: "hi", name: "easy"))
145+
assert encoded["alias"] == "hi"
146+
assert encoded["name"] == "easy"
147+
end
148+
149+
test "parse fails if the type isn't correct" do
150+
assert {:error, {:incorrect_type, _, %{}}} =
151+
UsesAlias.parse(%{"name" => "ua", "alias" => %{}})
152+
end
153+
end
154+
102155
describe "optional fields" do
103156
defmodule OptionalString do
104157
use Proto

0 commit comments

Comments
 (0)