Skip to content

Commit

Permalink
Made type aliases a thing
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
scohen committed Dec 19, 2022
1 parent 4d0ac96 commit 443b959
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 17 deletions.
1 change: 1 addition & 0 deletions apps/language_server/.formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ impossible_to_format = [
]

proto_dsl = [
defalias: 1,
defenum: 1,
defnotification: 2,
defnotification: 3,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto do
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.LspTypes

import ElixirLS.LanguageServer.Experimental.Protocol.Proto.TypeFunctions
import Proto.Alias, only: [defalias: 1]
import Proto.Enum, only: [defenum: 1]
import Proto.Notification, only: [defnotification: 2, defnotification: 3]
import Proto.Request, only: [defrequest: 3]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Alias do
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.CompileMetadata

defmacro defalias(alias_definition) do
caller_module = __CALLER__.module
CompileMetadata.add_type_alias_module(caller_module)

quote location: :keep do
def definition do
unquote(alias_definition)
end

def __meta__(:type) do
:type_alias
end

def __meta__(:param_names) do
[]
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.CompileMetadata do

@notification_modules_key {__MODULE__, :notification_modules}
@type_modules_key {__MODULE__, :type_modules}
@type_alias_modules_key {__MODULE__, :type_alias_modules}
@request_modules_key {__MODULE__, :request_modules}
@response_modules_key {__MODULE__, :response_modules}

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

def type_alias_modules do
:persistent_term.get(@type_alias_modules_key)
end

def type_modules do
:persistent_term.get(@type_modules_key)
end
Expand All @@ -40,6 +45,10 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.CompileMetadata do
add_module(@type_modules_key, module)
end

def add_type_alias_module(module) do
add_module(@type_alias_modules_key, module)
end

defp update(key, initial_value, update_fn) do
case :persistent_term.get(key, :not_found) do
:not_found -> :persistent_term.put(key, initial_value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Enum do
for {name, value} <- opts do
quote location: :keep do
def encode(unquote(name)) do
unquote(value)
{:ok, unquote(value)}
end
end
end
Expand All @@ -36,6 +36,10 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Enum do

unquote_splicing(encoders)

def encode(val) do
{:error, {:invalid_value, __MODULE__, val}}
end

unquote_splicing(enum_macros)

def __meta__(:types) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Field do
{:ok, orig_value}
end

def extract(:float, _name, orig_value) when is_float(orig_value) do
{:ok, orig_value}
end

def extract(:string, _name, orig_value) when is_binary(orig_value) do
{:ok, orig_value}
end
Expand All @@ -59,8 +63,12 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Field do
{:ok, orig_value}
end

def extract({:type_alias, alias_module}, name, orig_value) do
extract(alias_module.definition(), name, orig_value)
end

def extract(module, _name, orig_value)
when is_atom(module) and module not in [:integer, :string, :boolean] do
when is_atom(module) and module not in [:integer, :string, :boolean, :float] do
module.parse(orig_value)
end

Expand Down Expand Up @@ -103,15 +111,15 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Field do
end

def encode(:any, field_value) do
field_value
{:ok, field_value}
end

def encode({:literal, value}, _) do
value
{:ok, value}
end

def encode({:optional, _}, nil) do
:"$__drop__"
{:ok, :"$__drop__"}
end

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

def encode({:list, list_type}, field_value) when is_list(field_value) do
Enum.map(field_value, &encode(list_type, &1))
encoded =
Enum.reduce_while(field_value, [], fn element, acc ->
case encode(list_type, element) do
{:ok, encoded} -> {:cont, [encoded | acc]}
error -> {:halt, error}
end
end)

case encoded do
encoded_list when is_list(encoded_list) ->
{:ok, Enum.reverse(encoded_list)}

error ->
error
end
end

def encode(:integer, field_value) do
field_value
def encode(:integer, field_value) when is_integer(field_value) do
{:ok, field_value}
end

def encode(:integer, string_value) when is_binary(string_value) do
case Integer.parse(string_value) do
{int_value, ""} -> {:ok, int_value}
_ -> {:error, {:invalid_integer, string_value}}
end
end

def encode(:float, float_value) when is_float(float_value) do
{:ok, float_value}
end

def encode(:string, field_value) when is_binary(field_value) do
field_value
{:ok, field_value}
end

def encode(:boolean, field_value) when is_boolean(field_value) do
field_value
{:ok, field_value}
end

def encode({:map, value_type, _}, field_value) when is_map(field_value) do
Map.new(field_value, fn {k, v} -> {k, encode(value_type, v)} end)
map_fields =
Enum.reduce_while(field_value, [], fn {key, value}, acc ->
case encode(value_type, value) do
{:ok, encoded_value} -> {:cont, [{key, encoded_value} | acc]}
error -> {:halt, error}
end
end)

case map_fields do
fields when is_list(fields) -> {:ok, Map.new(fields)}
error -> error
end
end

def encode({:params, param_defs}, field_value) when is_map(field_value) do
Map.new(param_defs, fn {param_name, param_type} ->
{param_name, encode(param_type, Map.get(field_value, param_name))}
end)
param_fields =
Enum.reduce_while(param_defs, [], fn {param_name, param_type}, acc ->
unencoded = Map.get(field_value, param_name)

case encode(param_type, unencoded) do
{:ok, encoded_value} -> {:cont, [{param_name, encoded_value} | acc]}
error -> {:halt, error}
end
end)

case param_fields do
fields when is_list(fields) -> {:ok, Map.new(fields)}
error -> error
end
end

def encode({:constant, constant_module}, field_value) do
constant_module.encode(field_value)
{:ok, constant_module.encode(field_value)}
end

def encode({:type_alias, alias_module}, field_value) do
encode(alias_module.definition(), field_value)
end

def encode(module, field_value) when is_atom(module) do
if function_exported?(module, :encode, 1) do
module.encode(field_value)
else
field_value
{:ok, field_value}
end
end

def encode(_, nil) do
nil
end

def encode(type, value) do
{:error, {:invalid_type, type, value}}
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Macros.Json do
encoded_pairs =
for {field_name, field_type} <- unquote(dest_module).__meta__(:types),
field_value = get_field_value(value, field_name),
encoded_value = Field.encode(field_type, field_value),
{:ok, encoded_value} = Field.encode(field_type, field_value),
encoded_value != :"$__drop__" do
{field_name, encoded_value}
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.TypeFunctions do
:integer
end

def float do
:float
end

def string do
:string
end
Expand All @@ -15,6 +19,10 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.TypeFunctions do
:string
end

def type_alias(alias_module) do
{:type_alias, alias_module}
end

def literal(what) do
{:literal, what}
end
Expand Down
53 changes: 53 additions & 0 deletions apps/language_server/test/experimental/protocol/proto_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ defmodule ElixirLS.LanguageServer.Experimental.ProtoTest do
end
end

describe "float fields" do
defmodule FloatField do
use Proto
deftype float_field: float()
end

test "can parse a float field" do
assert {:ok, val} = FloatField.parse(%{"floatField" => 494.02})
assert val.float_field == 494.02
end

test "rejects nil float fields" do
assert {:error, {:invalid_value, :float_field, "string"}} =
FloatField.parse(%{"floatField" => "string"})
end
end

describe "list fields" do
defmodule ListField do
use Proto
Expand Down Expand Up @@ -99,6 +116,42 @@ defmodule ElixirLS.LanguageServer.Experimental.ProtoTest do
end
end

describe "type aliases" do
defmodule TypeAlias do
use Proto
defalias one_of([string(), list_of(string())])
end

defmodule UsesAlias do
use Proto

deftype alias: type_alias(TypeAlias), name: string()
end

test "parses a single item correctly" do
assert {:ok, uses} = UsesAlias.parse(%{"name" => "uses", "alias" => "foo"})
assert uses.name == "uses"
assert uses.alias == "foo"
end

test "parses a list correctly" do
assert {:ok, uses} = UsesAlias.parse(%{"name" => "uses", "alias" => ["foo", "bar"]})
assert uses.name == "uses"
assert uses.alias == ~w(foo bar)
end

test "encodes correctly" do
assert {:ok, encoded} = encode_and_decode(UsesAlias.new(alias: "hi", name: "easy"))
assert encoded["alias"] == "hi"
assert encoded["name"] == "easy"
end

test "parse fails if the type isn't correct" do
assert {:error, {:incorrect_type, _, %{}}} =
UsesAlias.parse(%{"name" => "ua", "alias" => %{}})
end
end

describe "optional fields" do
defmodule OptionalString do
use Proto
Expand Down

0 comments on commit 443b959

Please sign in to comment.