From 959d93ee58def5931e558115eb8c6a6de7dfbc4e Mon Sep 17 00:00:00 2001 From: Tyler Witt Date: Fri, 10 Jan 2025 22:27:35 +0700 Subject: [PATCH 1/4] Support JSON Includes a bump on Decimal to get the tests to pass. --- lib/ecto/json.ex | 49 +++++++++++++++++++++++++++++++++++++++++ mix.lock | 2 +- test/ecto/json_test.exs | 37 +++++++++++++++++++------------ 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/lib/ecto/json.ex b/lib/ecto/json.ex index b28c5d462e..6ad19633ba 100644 --- a/lib/ecto/json.ex +++ b/lib/ecto/json.ex @@ -46,3 +46,52 @@ if Code.ensure_loaded?(Jason.Encoder) do end end end + +if Code.ensure_loaded?(JSON.Encoder) do + defimpl JSON.Encoder, for: Ecto.Association.NotLoaded do + def encode(%{__owner__: owner, __field__: field}, _) do + raise """ + cannot encode association #{inspect(field)} from #{inspect(owner)} to \ + JSON because the association was not loaded. + + You can either preload the association: + + Repo.preload(#{inspect(owner)}, #{inspect(field)}) + + Or choose to not encode the association when converting the struct \ + to JSON by explicitly listing the JSON fields in your schema: + + defmodule #{inspect(owner)} do + # ... + + @derive {Jason.Encoder, only: [:name, :title, ...]} + schema ... do + + You can also use the :except option instead of :only if you would \ + prefer to skip some fields. + """ + end + end + + defimpl JSON.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 or explicitly list the JSON fields in your schema: + + defmodule #{inspect(schema)} do + # ... + + @derive {Jason.Encoder, only: [:name, :title, ...]} + schema ... do + + You can also use the :except option instead of :only if you would \ + prefer to skip some fields. + """ + end + end +end diff --git a/mix.lock b/mix.lock index 462cdbebf3..b85b92f4ef 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, "ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, diff --git a/test/ecto/json_test.exs b/test/ecto/json_test.exs index 5459902923..e0ae19d46f 100644 --- a/test/ecto/json_test.exs +++ b/test/ecto/json_test.exs @@ -1,29 +1,38 @@ defmodule Ecto.JsonTest do use ExUnit.Case, async: true + @implementations [{Jason, Jason.Encoder}, {JSON, JSON.Encoder}] + + loaded_implementations = for {_lib, encoder} = implementation <- @implementations, Code.ensure_loaded?(encoder), do: implementation + defmodule User do use Ecto.Schema - @derive Jason.Encoder + @derive Enum.map(loaded_implementations, fn {_lib, encoder} -> encoder end) schema "users" do has_many :comments, Ecto.Comment end end - test "encodes decimal" do - decimal = Decimal.new("1.0") - assert Jason.encode!(decimal) == ~s("1.0") - end + for {json_library, _encoder} <- loaded_implementations do - test "fails on association not loaded" do - assert_raise RuntimeError, - ~r/cannot encode association :comments from Ecto.JsonTest.User to JSON/, - fn -> Jason.encode!(%User{}.comments) end - end + describe to_string(json_library) do + test "encodes decimal" do + decimal = Decimal.new("1.0") + assert unquote(json_library).encode!(decimal) == ~s("1.0") + end + + test "fails on association not loaded" do + assert_raise RuntimeError, + ~r/cannot encode association :comments from Ecto.JsonTest.User to JSON/, + fn -> unquote(json_library).encode!(%User{}.comments) end + end - test "fails when encoding __meta__" do - assert_raise RuntimeError, - ~r/cannot encode metadata from the :__meta__ field for Ecto.JsonTest.User to JSON/, - fn -> Jason.encode!(%User{comments: []}) end + test "fails when encoding __meta__" do + assert_raise RuntimeError, + ~r/cannot encode metadata from the :__meta__ field for Ecto.JsonTest.User to JSON/, + fn -> unquote(json_library).encode!(%User{comments: []}) end + end + end end end From 6d8dab699a074edb55d9662862b62dc2f2ace776 Mon Sep 17 00:00:00 2001 From: Tyler Witt Date: Fri, 10 Jan 2025 22:36:37 +0700 Subject: [PATCH 2/4] Fix typo --- lib/ecto/json.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ecto/json.ex b/lib/ecto/json.ex index 6ad19633ba..3a100d8114 100644 --- a/lib/ecto/json.ex +++ b/lib/ecto/json.ex @@ -64,7 +64,7 @@ if Code.ensure_loaded?(JSON.Encoder) do defmodule #{inspect(owner)} do # ... - @derive {Jason.Encoder, only: [:name, :title, ...]} + @derive {JSON.Encoder, only: [:name, :title, ...]} schema ... do You can also use the :except option instead of :only if you would \ @@ -86,7 +86,7 @@ if Code.ensure_loaded?(JSON.Encoder) do defmodule #{inspect(schema)} do # ... - @derive {Jason.Encoder, only: [:name, :title, ...]} + @derive {JSON.Encoder, only: [:name, :title, ...]} schema ... do You can also use the :except option instead of :only if you would \ From 64559c997cbbe6b3b2b49f0cfe4df6789f15d503 Mon Sep 17 00:00:00 2001 From: Tyler Witt Date: Sat, 11 Jan 2025 01:14:31 +0700 Subject: [PATCH 3/4] DRY up json protocol usage --- lib/ecto/json.ex | 117 +++++++++++++--------------------------- test/ecto/json_test.exs | 14 ++--- 2 files changed, 44 insertions(+), 87 deletions(-) diff --git a/lib/ecto/json.ex b/lib/ecto/json.ex index 3a100d8114..ac2a5b499a 100644 --- a/lib/ecto/json.ex +++ b/lib/ecto/json.ex @@ -1,97 +1,52 @@ -if Code.ensure_loaded?(Jason.Encoder) do - defimpl Jason.Encoder, for: Ecto.Association.NotLoaded do - def encode(%{__owner__: owner, __field__: field}, _) do - raise """ - cannot encode association #{inspect(field)} from #{inspect(owner)} to \ - JSON because the association was not loaded. +for encoder <- [Jason.Encoder, JSON.Encoder] do + module = Macro.inspect_atom(:literal, encoder) - You can either preload the association: + if Code.ensure_loaded?(encoder) do + defimpl encoder, for: Ecto.Association.NotLoaded do + def encode(%{__owner__: owner, __field__: field}, _) do + raise """ + cannot encode association #{inspect(field)} from #{inspect(owner)} to \ + JSON because the association was not loaded. - Repo.preload(#{inspect(owner)}, #{inspect(field)}) + You can either preload the association: - Or choose to not encode the association when converting the struct \ - to JSON by explicitly listing the JSON fields in your schema: + Repo.preload(#{inspect(owner)}, #{inspect(field)}) - defmodule #{inspect(owner)} do - # ... + Or choose to not encode the association when converting the struct \ + to JSON by explicitly listing the JSON fields in your schema: - @derive {Jason.Encoder, only: [:name, :title, ...]} - schema ... do + defmodule #{inspect(owner)} do + # ... - You can also use the :except option instead of :only if you would \ - prefer to skip some fields. - """ - end - end - - defimpl Jason.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 or explicitly list the JSON fields in your schema: - - defmodule #{inspect(schema)} do - # ... + @derive {#{unquote(module)}, only: [:name, :title, ...]} + schema ... do - @derive {Jason.Encoder, only: [:name, :title, ...]} - schema ... do - - You can also use the :except option instead of :only if you would \ - prefer to skip some fields. - """ + You can also use the :except option instead of :only if you would \ + prefer to skip some fields. + """ + end end - end -end - -if Code.ensure_loaded?(JSON.Encoder) do - defimpl JSON.Encoder, for: Ecto.Association.NotLoaded do - def encode(%{__owner__: owner, __field__: field}, _) do - raise """ - cannot encode association #{inspect(field)} from #{inspect(owner)} to \ - JSON because the association was not loaded. - - You can either preload the association: - - Repo.preload(#{inspect(owner)}, #{inspect(field)}) - - Or choose to not encode the association when converting the struct \ - to JSON by explicitly listing the JSON fields in your schema: - - defmodule #{inspect(owner)} do - # ... - - @derive {JSON.Encoder, only: [:name, :title, ...]} - schema ... do - - You can also use the :except option instead of :only if you would \ - prefer to skip some fields. - """ - end - end - defimpl JSON.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. + defimpl 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 or explicitly list the JSON fields in your schema: + You can either map the schemas to remove the :__meta__ field before \ + encoding or explicitly list the JSON fields in your schema: - defmodule #{inspect(schema)} do - # ... + defmodule #{inspect(schema)} do + # ... - @derive {JSON.Encoder, only: [:name, :title, ...]} - schema ... do + @derive {#{unquote(module)}, only: [:name, :title, ...]} + schema ... do - You can also use the :except option instead of :only if you would \ - prefer to skip some fields. - """ + You can also use the :except option instead of :only if you would \ + prefer to skip some fields. + """ + end end end end diff --git a/test/ecto/json_test.exs b/test/ecto/json_test.exs index e0ae19d46f..38de49cb14 100644 --- a/test/ecto/json_test.exs +++ b/test/ecto/json_test.exs @@ -3,7 +3,10 @@ defmodule Ecto.JsonTest do @implementations [{Jason, Jason.Encoder}, {JSON, JSON.Encoder}] - loaded_implementations = for {_lib, encoder} = implementation <- @implementations, Code.ensure_loaded?(encoder), do: implementation + loaded_implementations = + for {_lib, encoder} = implementation <- @implementations, + Code.ensure_loaded?(encoder), + do: implementation defmodule User do use Ecto.Schema @@ -15,7 +18,6 @@ defmodule Ecto.JsonTest do end for {json_library, _encoder} <- loaded_implementations do - describe to_string(json_library) do test "encodes decimal" do decimal = Decimal.new("1.0") @@ -24,14 +26,14 @@ defmodule Ecto.JsonTest do test "fails on association not loaded" do assert_raise RuntimeError, - ~r/cannot encode association :comments from Ecto.JsonTest.User to JSON/, - fn -> unquote(json_library).encode!(%User{}.comments) end + ~r/cannot encode association :comments from Ecto.JsonTest.User to JSON/, + fn -> unquote(json_library).encode!(%User{}.comments) end end test "fails when encoding __meta__" do assert_raise RuntimeError, - ~r/cannot encode metadata from the :__meta__ field for Ecto.JsonTest.User to JSON/, - fn -> unquote(json_library).encode!(%User{comments: []}) end + ~r/cannot encode metadata from the :__meta__ field for Ecto.JsonTest.User to JSON/, + fn -> unquote(json_library).encode!(%User{comments: []}) end end end end From 4e4b8fe060bc118e47804dbe3eee4a0885266f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 10 Jan 2025 20:05:04 +0100 Subject: [PATCH 4/4] Update test/ecto/json_test.exs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Wígny Almeida --- test/ecto/json_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ecto/json_test.exs b/test/ecto/json_test.exs index 38de49cb14..3e0b079213 100644 --- a/test/ecto/json_test.exs +++ b/test/ecto/json_test.exs @@ -11,7 +11,7 @@ defmodule Ecto.JsonTest do defmodule User do use Ecto.Schema - @derive Enum.map(loaded_implementations, fn {_lib, encoder} -> encoder end) + @derive Keyword.values(loaded_implementations) schema "users" do has_many :comments, Ecto.Comment end