Repository: msz/hammox
Pull request #64 (2020-12-06 00:35:04)

fix: handle maps with `__struct__` key
Fixes  #57

Ecto.Schema has a typespec that looks like this:

```elixir
 schema() :: %{
  optional(atom()) => any(),
  :__struct__ => atom(),
  :__meta__ => Ecto.Schema.Metadata.t()
}
```

Because of the `__struct__` key, it looks like a struct, but really isn't and that was causing a `CaseClauseError` when trying to type-check an Ecto.Changeset.t() (which has a `schema()` inside of it.

This change adds a case clause that matches this case and then recursively ensures that the value of the `__struct__` key matches the specified type and that the rest of the fields also match the rest of the type.


Patch:
diff --git a/lib/hammox/type_engine.ex b/lib/hammox/type_engine.ex
index bbe6403..10bcc29 100644
--- a/lib/hammox/type_engine.ex
+++ b/lib/hammox/type_engine.ex
@@ -352,6 +352,13 @@ defmodule Hammox.TypeEngine do
 
       [{:type, _, :map_field_exact, [{:atom, _, :__struct__}, {:atom, _, other_struct_name}]}] ->
         {:error, [{:struct_name_type_mismatch, struct_name, other_struct_name}]}
+
+      [{:type, _, :map_field_exact, [{:atom, _, :__struct__}, {:type, _, _, _} = struct_type]}] ->
+        with :ok <- match_type(struct_name, struct_type),
+             :ok <-
+               match_type(Map.delete(value, :__struct__), {:type, 0, :map, rest_field_types}) do
+          :ok
+        end
     end
   end
 


Test patch:
diff --git a/test/hammox_test.exs b/test/hammox_test.exs
index 07f2159..8034c34 100644
--- a/test/hammox_test.exs
+++ b/test/hammox_test.exs
@@ -623,6 +623,44 @@ defmodule HammoxTest do
     end
   end
 
+  describe "map with __struct__ key" do
+    test "empty fail" do
+      assert_fail(:foo_map_struct_key, %{})
+    end
+
+    test "pass" do
+      assert_pass(:foo_map_struct_key, %{
+        __struct__: :foo,
+        key: 42
+      })
+    end
+
+    test "fail with missing __struct__" do
+      assert_fail(:foo_map_struct_key, %{key: 42})
+    end
+
+    test "fail with incorrect __struct__" do
+      assert_fail(:foo_map_struct_key, %{
+        __struct__: "wrong type",
+        key: 42
+      })
+    end
+
+    test "fail with incorrect fields" do
+      assert_fail(:foo_map_struct_key, %{
+        __struct__: :foo,
+        key: "not a number"
+      })
+    end
+
+    test "fail with incorrect __struct__ and fields" do
+      assert_fail(:foo_map_struct_key, %{
+        __struct__: "wrong type",
+        key: "not a number"
+      })
+    end
+  end
+
   describe "struct literal" do
     test "fail map" do
       assert_fail(:foo_struct_literal, %{foo: :bar})
diff --git a/test/support/behaviour.ex b/test/support/behaviour.ex
index aa69862..e42ad55 100644
--- a/test/support/behaviour.ex
+++ b/test/support/behaviour.ex
@@ -47,6 +47,10 @@ defmodule Hammox.Test.Behaviour do
               required(atom()) => atom(),
               required(atom() | number()) => atom()
             }
+  @callback foo_map_struct_key() :: %{
+              :__struct__ => atom(),
+              key: number()
+            }
   @callback foo_struct_literal() :: %Hammox.Test.Struct{}
   @callback foo_struct_fields_literal() :: %Hammox.Test.Struct{foo: number()}
   @callback foo_empty_tuple_literal() :: {}