Repository: elixir-ecto/ecto
Pull request #2300 (2017-11-08 13:54:18)

Implement Poison.Encoder for the metadata struct.
This allows us to provide a nice error message to the user instead of the
"encoder is not implemented for tuples" message they would get right now.


Patch:
diff --git a/lib/ecto/poison.ex b/lib/ecto/poison.ex
index a39bfcd2..be8679eb 100644
--- a/lib/ecto/poison.ex
+++ b/lib/ecto/poison.ex
@@ -24,4 +24,23 @@ if Code.ensure_loaded?(Poison) do
       """
     end
   end
+
+  defimpl Poison.Encoder, for: Ecto.Schema.Metadata do
+    def encode(%{schema: schema}, _) do
+      raise """
+      cannot encode metadata from the :__meta__ field for #{inspect schema} \
+      to JSON. This metadata is used internally by ecto and should never be \
+      exposed externally.
+
+      You can either map the schemas to remove the :__meta__ field before \
+      encoding to JSON, or explicit list the JSON fields in your schema:
+
+          defmodule #{inspect schema} do
+            # ...
+
+            @derive {Poison.Encoder, only: [:name, :title, ...]}
+            schema ... do
+      """
+    end
+  end
 end
diff --git a/lib/ecto/schema.ex b/lib/ecto/schema.ex
index 268f95bb..9568b541 100644
--- a/lib/ecto/schema.ex
+++ b/lib/ecto/schema.ex
@@ -339,13 +339,18 @@ defmodule Ecto.Schema do
     The `:context` field represents additional state some databases require
     for proper updates of data. It is not used by the built-in adapters of
     `Ecto.Adapters.Postres` and `Ecto.Adapters.MySQL`.
+
+    ## Schema
+
+    The `:schema` field refers the module name for the schema this metadata belongs to.
     """
-    defstruct [:state, :source, :context]
+    defstruct [:state, :source, :context, :schema]
 
     @type state :: :built | :loaded | :deleted
     @type source :: {Ecto.Schema.prefix, Ecto.Schema.source}
     @type context :: any
-    @type t :: %__MODULE__{state: state, source: source, context: context}
+    @type schema :: module
+    @type t :: %__MODULE__{state: state, source: source, context: context, schema: schema}
 
     defimpl Inspect do
       import Inspect.Algebra
@@ -431,8 +436,8 @@ defmodule Ecto.Schema do
           raise ArgumentError, "schema source must be a string, got: #{inspect source}"
         end
 
-        Module.put_attribute(__MODULE__, :struct_fields,
-                             {:__meta__, %Metadata{state: :built, source: {prefix, source}}})
+        meta = %Metadata{state: :built, source: {prefix, source}, schema: __MODULE__}
+        Module.put_attribute(__MODULE__, :struct_fields, {:__meta__, meta})
       end
 
       if @primary_key == nil do


Test patch:
diff --git a/test/ecto/poison_test.exs b/test/ecto/poison_test.exs
index 6de587f6..9d464934 100644
--- a/test/ecto/poison_test.exs
+++ b/test/ecto/poison_test.exs
@@ -4,7 +4,7 @@ defmodule Ecto.PoisonTest do
   defmodule User do
     use Ecto.Schema
 
-    embedded_schema do
+    schema "users" do
       has_many :comments, Ecto.Comment
     end
   end
@@ -20,4 +20,11 @@ defmodule Ecto.PoisonTest do
       Poison.encode!(%User{}.comments)
     end
   end
+
+  test "fails when encoding __meta__" do
+    assert_raise RuntimeError,
+                 ~r/cannot encode metadata from the :__meta__ field for Ecto.PoisonTest.User to JSON/, fn ->
+      Poison.encode!(%User{comments: []})
+    end
+  end
 end
diff --git a/test/ecto/schema_test.exs b/test/ecto/schema_test.exs
index 992fb51d..cce27759 100644
--- a/test/ecto/schema_test.exs
+++ b/test/ecto/schema_test.exs
@@ -79,6 +79,7 @@ defmodule Ecto.SchemaTest do
     assert schema.__meta__.source == {"prefix", "my schema"}
     assert Ecto.get_meta(schema, :prefix) == "prefix"
     assert Ecto.get_meta(schema, :source) == "my schema"
+    assert schema.__meta__.schema == Schema
 
     schema = Ecto.put_meta(schema, context: "foobar", state: :loaded)
     assert schema.__meta__.state == :loaded