Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
edgurgel committed Aug 27, 2024
1 parent 16de241 commit ab42f01
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 32 deletions.
13 changes: 12 additions & 1 deletion lib/ham/type_engine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ defmodule Ham.TypeEngine do
alias Ham.Utils

@type_kinds [:type, :typep, :opaque]

@typep entry_type :: tuple
@typep value :: term

@type reason ::
{:protocol_type_mismatch, value, module}
| {:remote_type_fetch_failure, {module, atom, arity}}
| {:module_fetch_failure, module}
| {:struct_name_type_mismatch, module | nil, module}
| {:required_field_unfulfilled_map_type_mismatch, entry_type}
| {:type_mismatch, value, entry_type}

@spec match_type(term, entry_type) :: :ok | {:error, [reason]}
def match_type(value, {:type, _, :union, union_types} = union) when is_list(union_types) do
results =
Enum.reduce_while(union_types, [], fn type, reason_stacks ->
Expand Down
30 changes: 2 additions & 28 deletions lib/ham/type_match_error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ defmodule Ham.TypeMatchError do
match types defined in typespecs.
"""
defexception [:reasons]

@type t :: %__MODULE__{}

alias Ham.Utils
import Ham.Utils, only: [type_to_string: 1]

@impl Exception
def exception({:error, reasons}), do: %__MODULE__{reasons: reasons}
Expand Down Expand Up @@ -123,32 +125,4 @@ defmodule Ham.TypeMatchError do

padding <> string
end

defp type_to_string({:type, _, :map_field_exact, [type1, type2]}) do
"required(#{type_to_string(type1)}) => #{type_to_string(type2)}"
end

defp type_to_string({:type, _, :map_field_assoc, [type1, type2]}) do
"optional(#{type_to_string(type1)}) => #{type_to_string(type2)}"
end

defp type_to_string(type) do
dbg(type)
# We really want to access Code.Typespec.typespec_to_quoted/1 here but it's
# private... this hack needs to suffice.
{:foo, type, []}
|> Code.Typespec.type_to_quoted()
|> IO.inspect(label: :type_to_quoted)
|> Macro.to_string()
|> IO.inspect(label: :macro_to_string)
|> String.split("\n")
|> Enum.map_join(&String.replace(&1, ~r/ +/, " "))
|> IO.inspect(label: :before_split)
|> String.split(" :: ", parts: 3)
|> IO.inspect(label: :split)
|> case do
[_, type_string] -> type_string
[_, type_name, type_string] -> "#{type_string} (\"#{type_name}\")"
end
end
end
34 changes: 34 additions & 0 deletions lib/ham/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,38 @@ defmodule Ham.Utils do
defp suffix(2), do: "nd"
defp suffix(3), do: "rd"
defp suffix(_), do: "th"

def type_to_string(type) do
# We really want to access Code.Typespec.typespec_to_quoted/1 here but it's
# private... this hack needs to suffice.
macro =
{:foo, type, []}
|> Code.Typespec.type_to_quoted()

macro
|> Macro.to_string()
|> String.split("\n")
|> Enum.map_join(&String.replace(&1, ~r/ +/, " "))
|> String.split(" :: ", parts: type_parts(macro))
|> case do
[_foo, type_string] -> type_string
[_foo, type_name, type_string] -> "#{type_string} (\"#{type_name}\")"
end
end

def type_to_string({:type, _, :map_field_exact, [type1, type2]}) do
"required(#{type_to_string(type1)}) => #{type_to_string(type2)}"
end

def type_to_string({:type, _, :map_field_assoc, [type1, type2]}) do
"optional(#{type_to_string(type1)}) => #{type_to_string(type2)}"
end

defp type_parts(macro) do
case macro do
# The type has a name
{:"::", [], [{:foo, [], []}, {:"::", _, _}]} -> 3
_ -> 2
end
end
end
6 changes: 6 additions & 0 deletions src/test.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-module(test).
-type req() :: #{
binary() => binary(),
_ => _
}.
-export_type([req/0]).
9 changes: 7 additions & 2 deletions test/ham/type_checker_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1274,11 +1274,16 @@ defmodule Ham.TypeCheckerTest do

describe "multiple options" do
test "pass" do
assert_pass(:foo_multiple_options, [], {:ok, "a", "b"})
assert_pass(:foo_multiple_options_annotated, [], {:ok, "a", "b"})
end

test "fail" do
assert_fail(:foo_multiple_options, [], :ok, "error")
assert_fail(
:foo_multiple_options_annotated,
[],
:ok,
~r/type \{:ok, binary\(\), req :: binary\(\)\} | \{:more, binary\(\), req :: binary\(\)\} \("result"\)/
)
end
end

Expand Down
17 changes: 17 additions & 0 deletions test/ham/utils_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule Ham.UtilsTest do
use ExUnit.Case, async: true
alias Ham.Utils

describe "type_to_string/1" do
test "complex type" do
{:ok, [type: {:complex_with_name, type_with_name, _}, type: {:complex, type, _}]} =
Code.Typespec.fetch_types(Complex)

assert Utils.type_to_string(type) ==
"{:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()}"

assert Utils.type_to_string(type_with_name) ==
"{:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()} (\"result\")"
end
end
end
12 changes: 11 additions & 1 deletion test/support/module.ex
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,17 @@ defmodule Ham.Test.TestModule do
@spec map_type_with_underscore() :: :test.req()
def map_type_with_underscore, do: %{method: "GET"}

@spec foo_multiple_options_annotated ::
result :: {:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()}
def foo_multiple_options_annotated, do: {:ok, "a", "b"}

@spec foo_multiple_options ::
{:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()}
result :: {:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()}
def foo_multiple_options, do: {:ok, "a", "b"}
end

defmodule Complex do
@type complex :: {:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()}
@type complex_with_name ::
result :: {:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()}
end

0 comments on commit ab42f01

Please sign in to comment.