Skip to content

Add support for record specs #150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions lib/hammox/type_engine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,28 @@ defmodule Hammox.TypeEngine do
type_mismatch(value, type)
end

def match_type(value, {:type, _, :record, record_types})
when is_tuple(value) and tuple_size(value) == length(record_types) do
error =
[Tuple.to_list(value), record_types, 0..(tuple_size(value) - 1)]
|> Enum.zip()
|> Enum.find_value(fn {elem, elem_type, index} ->
case match_type(elem, elem_type) do
:ok ->
nil

{:error, reasons} ->
{:error, [{:tuple_elem_type_mismatch, index, elem, elem_type} | reasons]}
end
end)

error || :ok
end

def match_type(value, {:type, _, :record, _} = type) do
type_mismatch(value, type)
end

def match_type(value, {:type, _, :float, []}) when is_float(value) do
:ok
end
Expand Down
23 changes: 14 additions & 9 deletions lib/hammox/type_match_error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,20 @@ defmodule Hammox.TypeMatchError do
defp 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.
{:foo, type, []}
|> Code.Typespec.type_to_quoted()
|> Macro.to_string()
|> String.split("\n")
|> Enum.map_join(&String.replace(&1, ~r/ +/, " "))
|> String.split(" :: ")
|> case do
[_, type_string] -> type_string
[_, type_name, type_string] -> "#{type_string} (\"#{type_name}\")"
{:"::", _meta, [_name, quoted_type]} = Code.Typespec.type_to_quoted({:foo, type, []})

case quoted_type do
{:"::", _meta, [quoted_name, quoted_type]} ->
"#{quoted_to_string(quoted_type)} (\"#{quoted_to_string(quoted_name)}\")"

quoted_type ->
quoted_to_string(quoted_type)
end
end

defp quoted_to_string(quoted) do
quoted
|> Macro.to_string()
|> String.replace(~r/\s+/, " ")
end
end
29 changes: 29 additions & 0 deletions test/hammox_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule HammoxTest do
use ExUnit.Case, async: true

import Hammox
import Hammox.Test.Behaviour, only: [foo_record: 0, foo_record: 1]

defmock(TestMock, for: Hammox.Test.Behaviour)

Expand Down Expand Up @@ -169,6 +170,20 @@ defmodule HammoxTest do
end
end

describe "record(:foo_record)" do
test "pass" do
assert_pass(:foo_record, foo_record())
end

test "fail non-tuple" do
assert_fail(:foo_record, %{atom: :ok, list: ["ok"]})
end

test "fail non-record" do
assert_fail(:foo_record, {:foo_record, :ok})
end
end

describe "reference()" do
test "pass" do
assert_pass(:foo_reference, Kernel.make_ref())
Expand Down Expand Up @@ -729,6 +744,20 @@ defmodule HammoxTest do
end
end

describe "record fields" do
test "pass" do
assert_pass(:foo_specific_record, foo_record(atom: :ok, list: ["a", "b", "c"]))
end

test "fail child type" do
assert_fail(:foo_specific_record, foo_record(atom: "ok", list: ["a", "b", "c"]))
end

test "fail overall type" do
assert_fail(:foo_specific_record, {:another_record, :ok, ["a", "b", "c"]})
end
end

describe "term()" do
test "pass" do
assert_pass(:foo_term, :any)
Expand Down
7 changes: 6 additions & 1 deletion test/support/behaviour.ex
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
defmodule Hammox.Test.Behaviour do
@moduledoc false
require Record

Record.defrecord(:foo_record, atom: nil, list: [])

@callback foo_any() :: any()
@callback foo_none() :: none()
@callback foo_atom() :: atom()
@callback foo_map() :: map()
@callback foo_pid() :: pid()
@callback foo_port() :: port()
@callback foo_record() :: record(:foo_record)
@callback foo_reference() :: reference()
@callback foo_struct() :: struct()
@callback foo_tuple() :: tuple()
Expand All @@ -26,7 +30,7 @@ defmodule Hammox.Test.Behaviour do
@callback foo_bitstring_size_literal() :: <<_::3>>
@callback foo_bitstring_unit_literal() :: <<_::_*3>>
@callback foo_bitstring_size_unit_literal() :: <<_::2, _::_*3>>
@callback foo_nullary_function_literal() :: (() -> :ok)
@callback foo_nullary_function_literal() :: (-> :ok)
@callback foo_binary_function_literal() :: (:a, :b -> :ok)
@callback foo_any_arity_function_literal() :: (... -> :ok)
@callback foo_integer_literal() :: 1
Expand Down Expand Up @@ -57,6 +61,7 @@ defmodule Hammox.Test.Behaviour do
@callback foo_struct_fields_literal() :: %Hammox.Test.Struct{foo: number()}
@callback foo_empty_tuple_literal() :: {}
@callback foo_two_tuple_literal() :: {:ok, atom()}
@callback foo_specific_record() :: record(:foo_record, atom: atom(), list: list(binary()))

@callback foo_term() :: term()
@callback foo_arity() :: arity()
Expand Down