From 9d05d83a26814f15dac7e99926d7b0a62e0d1777 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 13 Mar 2025 11:25:13 +0100 Subject: [PATCH 1/9] add backwards compatible polymorphic resource support --- lib/jsonapi/serializer.ex | 4 +- lib/jsonapi/view.ex | 86 +++++++++++++++++++++++++------- test/jsonapi/serializer_test.exs | 42 +++++++++++++++- test/jsonapi/view_test.exs | 55 ++++++++++++++++++++ 4 files changed, 165 insertions(+), 22 deletions(-) diff --git a/lib/jsonapi/serializer.ex b/lib/jsonapi/serializer.ex index 4ff2c5c4..395399aa 100644 --- a/lib/jsonapi/serializer.ex +++ b/lib/jsonapi/serializer.ex @@ -62,7 +62,7 @@ defmodule JSONAPI.Serializer do encoded_data = %{ id: view.id(data), - type: view.type(), + type: view.resource_type(data), attributes: transform_fields(view.attributes(data, conn)), relationships: %{} } @@ -255,7 +255,7 @@ defmodule JSONAPI.Serializer do def encode_rel_data(view, data) do %{ - type: view.type(), + type: view.resource_type(data), id: view.id(data) } end diff --git a/lib/jsonapi/view.ex b/lib/jsonapi/view.ex index 4b007ba0..55f6fd66 100644 --- a/lib/jsonapi/view.ex +++ b/lib/jsonapi/view.ex @@ -140,10 +140,12 @@ defmodule JSONAPI.View do @type options :: keyword() @type resource_id :: String.t() @type resource_type :: String.t() + @type polymorphic_resource :: boolean() @callback attributes(data(), Conn.t() | nil) :: map() @callback id(data()) :: resource_id() | nil @callback fields() :: [field()] + @callback polymorphic_fields(data()) :: [field()] @callback get_field(field(), data(), Conn.t()) :: any() @callback hidden(data()) :: [field()] @callback links(data(), Conn.t()) :: links() @@ -156,6 +158,7 @@ defmodule JSONAPI.View do {atom(), t() | {t(), :include} | {atom(), t()} | {atom(), t(), :include}} ] @callback type() :: resource_type() + @callback polymorphic_type(data()) :: resource_type() @callback url_for(data(), Conn.t() | nil) :: String.t() @callback url_for_pagination(data(), Conn.t(), Paginator.params()) :: String.t() @callback url_for_rel(term(), String.t(), Conn.t() | nil) :: String.t() @@ -167,7 +170,8 @@ defmodule JSONAPI.View do {type, opts} = Keyword.pop(opts, :type) {namespace, opts} = Keyword.pop(opts, :namespace) {path, opts} = Keyword.pop(opts, :path) - {paginator, _opts} = Keyword.pop(opts, :paginator) + {paginator, opts} = Keyword.pop(opts, :paginator) + {polymorphic_resource, _opts} = Keyword.pop(opts, :polymorphic_resource, false) quote do alias JSONAPI.{Serializer, View} @@ -178,6 +182,7 @@ defmodule JSONAPI.View do @namespace unquote(namespace) @path unquote(path) @paginator unquote(paginator) + @polymorphic_resource unquote(polymorphic_resource) @impl View def id(nil), do: nil @@ -205,8 +210,21 @@ defmodule JSONAPI.View do end) end - @impl View - def fields, do: raise("Need to implement fields/0") + cond do + !@polymorphic_resource -> + @impl View + def fields, do: raise("Need to implement fields/0") + + @impl View + def polymorphic_fields(_data), do: nil + + @polymorphic_resource -> + @impl View + def fields, do: nil + + @impl View + def polymorphic_fields(_data), do: raise("Need to implement polymorphic_fields/1") + end @impl View def hidden(_data), do: [] @@ -241,11 +259,27 @@ defmodule JSONAPI.View do @impl View def relationships, do: [] - @impl View - if @resource_type do - def type, do: @resource_type - else - def type, do: raise("Need to implement type/0") + cond do + @resource_type -> + @impl View + def type, do: @resource_type + + @impl View + def polymorphic_type(_data), do: nil + + !@polymorphic_resource -> + @impl View + def type, do: raise("Need to implement type/0") + + @impl View + def polymorphic_type(_data), do: nil + + @polymorphic_resource -> + @impl View + def type, do: nil + + @impl View + def polymorphic_type(_data), do: raise("Need to implement polymorphic_type/1") end @impl View @@ -264,6 +298,22 @@ defmodule JSONAPI.View do def visible_fields(data, conn), do: View.visible_fields(__MODULE__, data, conn) + def resource_fields(data) do + if @polymorphic_resource do + polymorphic_fields(data) + else + fields() + end + end + + def resource_type(data) do + if @polymorphic_resource do + polymorphic_type(data) + else + type() + end + end + defoverridable View def index(models, conn, _params, meta \\ nil, options \\ []), @@ -336,11 +386,11 @@ defmodule JSONAPI.View do @spec url_for(t(), term(), Conn.t() | nil) :: String.t() def url_for(view, data, nil = _conn) when is_nil(data) or is_list(data), - do: URI.to_string(%URI{path: Enum.join([view.namespace(), path_for(view)], "/")}) + do: URI.to_string(%URI{path: Enum.join([view.namespace(), path_for(view, data)], "/")}) def url_for(view, data, nil = _conn) do URI.to_string(%URI{ - path: Enum.join([view.namespace(), path_for(view), view.id(data)], "/") + path: Enum.join([view.namespace(), path_for(view, data), view.id(data)], "/") }) end @@ -349,7 +399,7 @@ defmodule JSONAPI.View do scheme: scheme(conn), host: host(conn), port: port(conn), - path: Enum.join([view.namespace(), path_for(view)], "/") + path: Enum.join([view.namespace(), path_for(view, data)], "/") }) end @@ -358,7 +408,7 @@ defmodule JSONAPI.View do scheme: scheme(conn), host: host(conn), port: port(conn), - path: Enum.join([view.namespace(), path_for(view), view.id(data)], "/") + path: Enum.join([view.namespace(), path_for(view, data), view.id(data)], "/") }) end @@ -392,8 +442,8 @@ defmodule JSONAPI.View do def visible_fields(view, data, conn) do all_fields = view - |> requested_fields_for_type(conn) - |> net_fields_for_type(view.fields()) + |> requested_fields_for_type(data, conn) + |> net_fields_for_type(view.resource_fields(data)) hidden_fields = view.hidden(data) @@ -420,11 +470,11 @@ defmodule JSONAPI.View do |> URI.to_string() end - defp requested_fields_for_type(view, %Conn{assigns: %{jsonapi_query: %{fields: fields}}}) do - fields[view.type()] + defp requested_fields_for_type(view, data, %Conn{assigns: %{jsonapi_query: %{fields: fields}}}) do + fields[view.resource_type(data)] end - defp requested_fields_for_type(_view, _conn), do: nil + defp requested_fields_for_type(_view, _data, _conn), do: nil defp host(%Conn{host: host}), do: Application.get_env(:jsonapi, :host, host) @@ -438,5 +488,5 @@ defmodule JSONAPI.View do defp scheme(%Conn{scheme: scheme}), do: Application.get_env(:jsonapi, :scheme, to_string(scheme)) - defp path_for(view), do: view.path() || view.type() + defp path_for(view, data), do: view.path() || view.resource_type(data) end diff --git a/test/jsonapi/serializer_test.exs b/test/jsonapi/serializer_test.exs index 54f4d803..7f02e2a3 100644 --- a/test/jsonapi/serializer_test.exs +++ b/test/jsonapi/serializer_test.exs @@ -13,7 +13,8 @@ defmodule JSONAPI.SerializerTest do def relationships do [ author: {JSONAPI.SerializerTest.UserView, :include}, - best_comments: {JSONAPI.SerializerTest.CommentView, :include} + best_comments: {JSONAPI.SerializerTest.CommentView, :include}, + polymorphics: {JSONAPI.SerializerTest.PolymorphicView, :include} ] end end @@ -183,6 +184,38 @@ defmodule JSONAPI.SerializerTest do end end + defmodule PolymorphicDataOne do + defstruct [:id, :some_field] + end + + defmodule PolymorphicDataTwo do + defstruct [:id, :some_other_field] + end + + defmodule PolymorphicView do + use JSONAPI.View, polymorphic_resource: true + + def polymorphic_type(data) do + case data do + %PolymorphicDataOne{} -> + "polymorphic_data_one" + + %PolymorphicDataTwo{} -> + "polymorphic_data_one" + end + end + + def polymorphic_fields(data) do + case data do + %PolymorphicDataOne{} -> + [:some_field] + + %PolymorphicDataTwo{} -> + [:some_other_field] + end + end + end + setup do Application.put_env(:jsonapi, :field_transformation, :underscore) @@ -219,6 +252,10 @@ defmodule JSONAPI.SerializerTest do best_comments: [ %{id: 5, text: "greatest comment ever", user: %{id: 4, username: "jack"}}, %{id: 6, text: "not so great", user: %{id: 2, username: "jason"}} + ], + polymorphic: [ + %PolymorphicDataOne{id: 1, some_field: "foo"}, + %PolymorphicDataTwo{id: 2, some_other_field: "foo"} ] } @@ -842,7 +879,8 @@ defmodule JSONAPI.SerializerTest do assert configs == [ {:author, :author, JSONAPI.SerializerTest.UserView, true}, - {:best_comments, :best_comments, JSONAPI.SerializerTest.CommentView, true} + {:best_comments, :best_comments, JSONAPI.SerializerTest.CommentView, true}, + {:polymorphics, :polymorphics, JSONAPI.SerializerTest.PolymorphicView, true} ] end diff --git a/test/jsonapi/view_test.exs b/test/jsonapi/view_test.exs index 8ea8af01..aecf70a1 100644 --- a/test/jsonapi/view_test.exs +++ b/test/jsonapi/view_test.exs @@ -55,6 +55,38 @@ defmodule JSONAPI.ViewTest do def get_field(field, _data, _conn), do: "#{field}!" end + defmodule PolymorphicDataOne do + defstruct [:some_field] + end + + defmodule PolymorphicDataTwo do + defstruct [:some_other_field] + end + + defmodule PolymorphicView do + use JSONAPI.View, polymorphic_resource: true + + def polymorphic_type(data) do + case data do + %PolymorphicDataOne{} -> + "polymorphic_data_one" + + %PolymorphicDataTwo{} -> + "polymorphic_data_one" + end + end + + def polymorphic_fields(data) do + case data do + %PolymorphicDataOne{} -> + [:some_field] + + %PolymorphicDataTwo{} -> + [:some_other_field] + end + end + end + setup do Application.put_env(:jsonapi, :field_transformation, :underscore) Application.put_env(:jsonapi, :namespace, "/other-api") @@ -71,6 +103,20 @@ defmodule JSONAPI.ViewTest do assert PostView.type() == "posts" end + describe "resource_type/1" do + test "equals result of type/0 if resource is not polymorphic" do + assert PostView.type() == PostView.resource_type(%{}) + end + + test "equals result of polymorphic_type/1 if resource is polymorphic" do + assert PolymorphicView.polymorphic_type(%PolymorphicDataOne{}) == + PolymorphicView.resource_type(%PolymorphicDataOne{}) + + assert PolymorphicView.polymorphic_type(%PolymorphicDataTwo{}) == + PolymorphicView.resource_type(%PolymorphicDataTwo{}) + end + end + describe "namespace/0" do setup do Application.put_env(:jsonapi, :namespace, "/cake") @@ -314,4 +360,13 @@ defmodule JSONAPI.ViewTest do static_fun: "static_fun/2" } == DynamicView.attributes(data, conn) end + + test "attributes/2 can return polymorphic fields" do + data = %PolymorphicDataTwo{some_other_field: "foo"} + conn = %Plug.Conn{assigns: %{jsonapi_query: %JSONAPI.Config{}}} + + assert %{ + some_other_field: "foo" + } == PolymorphicView.attributes(data, conn) + end end From fa2d8809d29b034c1b85ba0957d2f212d1c909ae Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 10 Apr 2025 11:14:04 +0200 Subject: [PATCH 2/9] polymorphic_relationships --- lib/jsonapi/serializer.ex | 16 ++++++++-------- lib/jsonapi/view.ex | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/jsonapi/serializer.ex b/lib/jsonapi/serializer.ex index 395399aa..f22505cb 100644 --- a/lib/jsonapi/serializer.ex +++ b/lib/jsonapi/serializer.ex @@ -58,7 +58,7 @@ defmodule JSONAPI.Serializer do end def encode_data(view, data, conn, query_includes, options) do - valid_includes = get_includes(view, query_includes) + valid_includes = get_includes(view, query_includes, data) encoded_data = %{ id: view.id(data), @@ -80,7 +80,7 @@ defmodule JSONAPI.Serializer do @spec encode_relationships(Conn.t(), document(), tuple(), list()) :: tuple() def encode_relationships(conn, doc, {view, data, _, _} = view_info, options) do - view.relationships() + view.resource_relationships(data) |> Enum.filter(&assoc_loaded?(Map.get(data, get_data_key(&1)))) |> Enum.map_reduce(doc, &build_relationships(conn, view_info, &1, &2, options)) end @@ -273,13 +273,13 @@ defmodule JSONAPI.Serializer do defp assoc_loaded?(%{__struct__: Ecto.Association.NotLoaded}), do: false defp assoc_loaded?(_association), do: true - defp get_includes(view, query_includes) do - includes = get_default_includes(view) ++ get_query_includes(view, query_includes) + defp get_includes(view, query_includes, data) do + includes = get_default_includes(view, data) ++ get_query_includes(view, query_includes, data) Enum.uniq(includes) end - defp get_default_includes(view) do - rels = view.relationships() + defp get_default_includes(view, data) do + rels = view.resource_relationships(data) Enum.filter(rels, &include_rel_by_default/1) end @@ -290,8 +290,8 @@ defmodule JSONAPI.Serializer do include_by_default end - defp get_query_includes(view, query_includes) do - rels = view.relationships() + defp get_query_includes(view, query_includes, data) do + rels = view.resource_relationships(data) query_includes |> Enum.map(fn diff --git a/lib/jsonapi/view.ex b/lib/jsonapi/view.ex index 55f6fd66..8454ce7e 100644 --- a/lib/jsonapi/view.ex +++ b/lib/jsonapi/view.ex @@ -157,6 +157,9 @@ defmodule JSONAPI.View do @callback relationships() :: [ {atom(), t() | {t(), :include} | {atom(), t()} | {atom(), t(), :include}} ] + @callback polymorphic_relationships(data()) :: [ + {atom(), t() | {t(), :include} | {atom(), t()} | {atom(), t(), :include}} + ] @callback type() :: resource_type() @callback polymorphic_type(data()) :: resource_type() @callback url_for(data(), Conn.t() | nil) :: String.t() @@ -259,6 +262,9 @@ defmodule JSONAPI.View do @impl View def relationships, do: [] + @impl View + def polymorphic_relationships(_data), do: [] + cond do @resource_type -> @impl View @@ -314,6 +320,14 @@ defmodule JSONAPI.View do end end + def resource_relationships(data) do + if @polymorphic_resource do + polymorphic_relationships(data) + else + relationships() + end + end + defoverridable View def index(models, conn, _params, meta \\ nil, options \\ []), From 1a40b441d3f8d569344476c49f1627126dcd672f Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 6 May 2025 14:06:53 +0200 Subject: [PATCH 3/9] remove polymorphic_resource type, rename with suffix ? to indicate it is a boolean --- lib/jsonapi/view.ex | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/jsonapi/view.ex b/lib/jsonapi/view.ex index 8454ce7e..987ef160 100644 --- a/lib/jsonapi/view.ex +++ b/lib/jsonapi/view.ex @@ -140,7 +140,6 @@ defmodule JSONAPI.View do @type options :: keyword() @type resource_id :: String.t() @type resource_type :: String.t() - @type polymorphic_resource :: boolean() @callback attributes(data(), Conn.t() | nil) :: map() @callback id(data()) :: resource_id() | nil @@ -174,7 +173,7 @@ defmodule JSONAPI.View do {namespace, opts} = Keyword.pop(opts, :namespace) {path, opts} = Keyword.pop(opts, :path) {paginator, opts} = Keyword.pop(opts, :paginator) - {polymorphic_resource, _opts} = Keyword.pop(opts, :polymorphic_resource, false) + {polymorphic_resource?, _opts} = Keyword.pop(opts, :polymorphic_resource?, false) quote do alias JSONAPI.{Serializer, View} @@ -185,7 +184,7 @@ defmodule JSONAPI.View do @namespace unquote(namespace) @path unquote(path) @paginator unquote(paginator) - @polymorphic_resource unquote(polymorphic_resource) + @polymorphic_resource? unquote(polymorphic_resource?) @impl View def id(nil), do: nil @@ -214,14 +213,14 @@ defmodule JSONAPI.View do end cond do - !@polymorphic_resource -> + !@polymorphic_resource? -> @impl View def fields, do: raise("Need to implement fields/0") @impl View def polymorphic_fields(_data), do: nil - @polymorphic_resource -> + @polymorphic_resource? -> @impl View def fields, do: nil @@ -273,14 +272,14 @@ defmodule JSONAPI.View do @impl View def polymorphic_type(_data), do: nil - !@polymorphic_resource -> + !@polymorphic_resource? -> @impl View def type, do: raise("Need to implement type/0") @impl View def polymorphic_type(_data), do: nil - @polymorphic_resource -> + @polymorphic_resource? -> @impl View def type, do: nil @@ -305,7 +304,7 @@ defmodule JSONAPI.View do do: View.visible_fields(__MODULE__, data, conn) def resource_fields(data) do - if @polymorphic_resource do + if @polymorphic_resource? do polymorphic_fields(data) else fields() @@ -313,7 +312,7 @@ defmodule JSONAPI.View do end def resource_type(data) do - if @polymorphic_resource do + if @polymorphic_resource? do polymorphic_type(data) else type() @@ -321,7 +320,7 @@ defmodule JSONAPI.View do end def resource_relationships(data) do - if @polymorphic_resource do + if @polymorphic_resource? do polymorphic_relationships(data) else relationships() From 9448be001f4387702a8087af46a1cd536ab0b579 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 6 May 2025 14:29:02 +0200 Subject: [PATCH 4/9] match type spec; refactor types --- lib/jsonapi/view.ex | 22 ++++++++++------------ test/jsonapi/serializer_test.exs | 2 +- test/jsonapi/view_test.exs | 2 +- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/jsonapi/view.ex b/lib/jsonapi/view.ex index 987ef160..943e97ff 100644 --- a/lib/jsonapi/view.ex +++ b/lib/jsonapi/view.ex @@ -140,11 +140,13 @@ defmodule JSONAPI.View do @type options :: keyword() @type resource_id :: String.t() @type resource_type :: String.t() + @type resource_relationship :: [{atom(), t() | {t(), :include} | {atom(), t()} | {atom(), t(), :include}}] + @type resource_fields :: [field()] @callback attributes(data(), Conn.t() | nil) :: map() @callback id(data()) :: resource_id() | nil - @callback fields() :: [field()] - @callback polymorphic_fields(data()) :: [field()] + @callback fields() :: resource_fields() + @callback polymorphic_fields(data()) :: resource_fields() @callback get_field(field(), data(), Conn.t()) :: any() @callback hidden(data()) :: [field()] @callback links(data(), Conn.t()) :: links() @@ -153,14 +155,10 @@ defmodule JSONAPI.View do @callback pagination_links(data(), Conn.t(), Paginator.page(), Paginator.options()) :: Paginator.links() @callback path() :: String.t() | nil - @callback relationships() :: [ - {atom(), t() | {t(), :include} | {atom(), t()} | {atom(), t(), :include}} - ] - @callback polymorphic_relationships(data()) :: [ - {atom(), t() | {t(), :include} | {atom(), t()} | {atom(), t(), :include}} - ] - @callback type() :: resource_type() - @callback polymorphic_type(data()) :: resource_type() + @callback relationships() :: resource_relationship() + @callback polymorphic_relationships(data()) :: resource_relationship() + @callback type() :: resource_type() | nil + @callback polymorphic_type(data()) :: resource_type() | nil @callback url_for(data(), Conn.t() | nil) :: String.t() @callback url_for_pagination(data(), Conn.t(), Paginator.params()) :: String.t() @callback url_for_rel(term(), String.t(), Conn.t() | nil) :: String.t() @@ -218,11 +216,11 @@ defmodule JSONAPI.View do def fields, do: raise("Need to implement fields/0") @impl View - def polymorphic_fields(_data), do: nil + def polymorphic_fields(_data), do: [] @polymorphic_resource? -> @impl View - def fields, do: nil + def fields, do: [] @impl View def polymorphic_fields(_data), do: raise("Need to implement polymorphic_fields/1") diff --git a/test/jsonapi/serializer_test.exs b/test/jsonapi/serializer_test.exs index 7f02e2a3..c3f28e0a 100644 --- a/test/jsonapi/serializer_test.exs +++ b/test/jsonapi/serializer_test.exs @@ -193,7 +193,7 @@ defmodule JSONAPI.SerializerTest do end defmodule PolymorphicView do - use JSONAPI.View, polymorphic_resource: true + use JSONAPI.View, polymorphic_resource?: true def polymorphic_type(data) do case data do diff --git a/test/jsonapi/view_test.exs b/test/jsonapi/view_test.exs index aecf70a1..85e74c8e 100644 --- a/test/jsonapi/view_test.exs +++ b/test/jsonapi/view_test.exs @@ -64,7 +64,7 @@ defmodule JSONAPI.ViewTest do end defmodule PolymorphicView do - use JSONAPI.View, polymorphic_resource: true + use JSONAPI.View, polymorphic_resource?: true def polymorphic_type(data) do case data do From cf2b3747d76cb34b898fb7a1db15497bc67b5a6f Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 6 May 2025 15:03:51 +0200 Subject: [PATCH 5/9] add as callbacks --- lib/jsonapi/view.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/jsonapi/view.ex b/lib/jsonapi/view.ex index 943e97ff..a912587e 100644 --- a/lib/jsonapi/view.ex +++ b/lib/jsonapi/view.ex @@ -147,6 +147,7 @@ defmodule JSONAPI.View do @callback id(data()) :: resource_id() | nil @callback fields() :: resource_fields() @callback polymorphic_fields(data()) :: resource_fields() + @callback resource_fields(data()) :: resource_fields() @callback get_field(field(), data(), Conn.t()) :: any() @callback hidden(data()) :: [field()] @callback links(data(), Conn.t()) :: links() @@ -157,8 +158,10 @@ defmodule JSONAPI.View do @callback path() :: String.t() | nil @callback relationships() :: resource_relationship() @callback polymorphic_relationships(data()) :: resource_relationship() + @callback resource_relationship(data()) :: resource_relationship() @callback type() :: resource_type() | nil @callback polymorphic_type(data()) :: resource_type() | nil + @callback resource_type(data()) :: resource_type() @callback url_for(data(), Conn.t() | nil) :: String.t() @callback url_for_pagination(data(), Conn.t(), Paginator.params()) :: String.t() @callback url_for_rel(term(), String.t(), Conn.t() | nil) :: String.t() @@ -301,6 +304,7 @@ defmodule JSONAPI.View do def visible_fields(data, conn), do: View.visible_fields(__MODULE__, data, conn) + @impl View def resource_fields(data) do if @polymorphic_resource? do polymorphic_fields(data) @@ -309,6 +313,7 @@ defmodule JSONAPI.View do end end + @impl View def resource_type(data) do if @polymorphic_resource? do polymorphic_type(data) @@ -317,6 +322,7 @@ defmodule JSONAPI.View do end end + @impl View def resource_relationships(data) do if @polymorphic_resource? do polymorphic_relationships(data) From 8d4b0661d510bd6f94d95d4a422ff94daed733cf Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 6 May 2025 15:08:31 +0200 Subject: [PATCH 6/9] fix names --- lib/jsonapi/view.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/jsonapi/view.ex b/lib/jsonapi/view.ex index a912587e..44ba3e5d 100644 --- a/lib/jsonapi/view.ex +++ b/lib/jsonapi/view.ex @@ -140,7 +140,7 @@ defmodule JSONAPI.View do @type options :: keyword() @type resource_id :: String.t() @type resource_type :: String.t() - @type resource_relationship :: [{atom(), t() | {t(), :include} | {atom(), t()} | {atom(), t(), :include}}] + @type resource_relationships :: [{atom(), t() | {t(), :include} | {atom(), t()} | {atom(), t(), :include}}] @type resource_fields :: [field()] @callback attributes(data(), Conn.t() | nil) :: map() @@ -156,9 +156,9 @@ defmodule JSONAPI.View do @callback pagination_links(data(), Conn.t(), Paginator.page(), Paginator.options()) :: Paginator.links() @callback path() :: String.t() | nil - @callback relationships() :: resource_relationship() - @callback polymorphic_relationships(data()) :: resource_relationship() - @callback resource_relationship(data()) :: resource_relationship() + @callback relationships() :: resource_relationships() + @callback polymorphic_relationships(data()) :: resource_relationships() + @callback resource_relationships(data()) :: resource_relationships() @callback type() :: resource_type() | nil @callback polymorphic_type(data()) :: resource_type() | nil @callback resource_type(data()) :: resource_type() From ec2cca543858e468ea37334454a6b955beb7fcb3 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 5 Jun 2025 17:42:17 +0200 Subject: [PATCH 7/9] fix credo linting --- lib/jsonapi/serializer.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/jsonapi/serializer.ex b/lib/jsonapi/serializer.ex index f22505cb..353bdebe 100644 --- a/lib/jsonapi/serializer.ex +++ b/lib/jsonapi/serializer.ex @@ -80,7 +80,8 @@ defmodule JSONAPI.Serializer do @spec encode_relationships(Conn.t(), document(), tuple(), list()) :: tuple() def encode_relationships(conn, doc, {view, data, _, _} = view_info, options) do - view.resource_relationships(data) + data + |> view.resource_relationships() |> Enum.filter(&assoc_loaded?(Map.get(data, get_data_key(&1)))) |> Enum.map_reduce(doc, &build_relationships(conn, view_info, &1, &2, options)) end From 53d343f8627c867d655dafcd8ad4a9964dcf43c7 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 5 Jun 2025 17:51:44 +0200 Subject: [PATCH 8/9] update docs on how to use polymorphic resources --- lib/jsonapi/view.ex | 76 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/lib/jsonapi/view.ex b/lib/jsonapi/view.ex index 44ba3e5d..2b265d1c 100644 --- a/lib/jsonapi/view.ex +++ b/lib/jsonapi/view.ex @@ -114,6 +114,82 @@ defmodule JSONAPI.View do and then use the `[user: {UserView, :include}]` syntax in your `includes` function. This tells the serializer to *always* include if its loaded. + ## Polymorphic Resources + + Polymorphic resources allow you to serialize different types of data with the same view module. + This is useful when you have a collection of resources that share some common attributes but + have different types, fields, or relationships based on the specific data being serialized. + + To enable polymorphic resources, set `polymorphic_resource?: true` when using the JSONAPI.View: + + defmodule MediaView do + use JSONAPI.View, polymorphic_resource?: true + + def polymorphic_type(%{type: "image"}), do: "image" + def polymorphic_type(%{type: "video"}), do: "video" + def polymorphic_type(%{type: "audio"}), do: "audio" + + def polymorphic_fields(%{type: "image"}), do: [:id, :url, :width, :height, :alt_text] + def polymorphic_fields(%{type: "video"}), do: [:id, :url, :duration, :thumbnail] + def polymorphic_fields(%{type: "audio"}), do: [:id, :url, :duration, :bitrate] + + def polymorphic_relationships(%{type: "image"}), do: [album: AlbumView] + def polymorphic_relationships(%{type: "video"}), do: [playlist: PlaylistView, author: UserView] + def polymorphic_relationships(%{type: "audio"}), do: [album: AlbumView, artist: ArtistView] + end + + ### Required Callbacks for Polymorphic Resources + + When using polymorphic resources, you must implement these callbacks instead of their non-polymorphic counterparts: + + - `polymorphic_type/1` - Returns the JSONAPI type string based on the data + - `polymorphic_fields/1` - Returns the list of fields to serialize based on the data + + ### Optional Callbacks for Polymorphic Resources + + - `polymorphic_relationships/1` - Returns relationships specific to the data type (defaults to empty list) + + ### Example Usage + + With the above `MediaView`, you can serialize different media types: + + # Image data + image = %{id: 1, type: "image", url: "/image.jpg", width: 800, height: 600, alt_text: "A photo"} + MediaView.show(image, conn) + # => %{data: %{id: "1", type: "image", attributes: %{url: "/image.jpg", width: 800, height: 600, alt_text: "A photo"}}} + + # Video data + video = %{id: 2, type: "video", url: "/video.mp4", duration: 120, thumbnail: "/thumb.jpg"} + MediaView.show(video, conn) + # => %{data: %{id: "2", type: "video", attributes: %{url: "/video.mp4", duration: 120, thumbnail: "/thumb.jpg"}}} + + ### Custom Field Functions + + You can still define custom field functions that work across all polymorphic types: + + defmodule MediaView do + use JSONAPI.View, polymorphic_resource?: true + + def file_size(data, _conn) do + # Custom logic to calculate file size + calculate_file_size(data.url) + end + + def polymorphic_fields(%{type: "image"}), do: [:id, :url, :file_size, :width, :height] + def polymorphic_fields(%{type: "video"}), do: [:id, :url, :file_size, :duration] + # ... other polymorphic implementations + end + + ### Notes + + - When `polymorphic_resource?: true` is set, the regular `type/0`, `fields/0`, and `relationships/0` + functions are not used and will return default values (nil or empty list) + - The polymorphic callbacks receive the actual data as their first argument, allowing you to + determine the appropriate type, fields, and relationships dynamically + - All other view functionality (links, meta, hidden fields, etc.) works the same way + - **Important**: Polymorphic resources currently do not work for deserializing data from POST + requests yet. They are only supported for serialization (rendering responses) + ## Options * `:host` (binary) - Allows the `host` to be overridden for generated URLs. Defaults to `host` of the supplied `conn`. From 63c43ab56580ef6f380ad394e6c48be4fce860f8 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 9 Aug 2025 00:18:48 +0200 Subject: [PATCH 9/9] no need for these to be callbacks --- lib/jsonapi/view.ex | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/jsonapi/view.ex b/lib/jsonapi/view.ex index 2b265d1c..12fd8e73 100644 --- a/lib/jsonapi/view.ex +++ b/lib/jsonapi/view.ex @@ -223,7 +223,6 @@ defmodule JSONAPI.View do @callback id(data()) :: resource_id() | nil @callback fields() :: resource_fields() @callback polymorphic_fields(data()) :: resource_fields() - @callback resource_fields(data()) :: resource_fields() @callback get_field(field(), data(), Conn.t()) :: any() @callback hidden(data()) :: [field()] @callback links(data(), Conn.t()) :: links() @@ -234,10 +233,8 @@ defmodule JSONAPI.View do @callback path() :: String.t() | nil @callback relationships() :: resource_relationships() @callback polymorphic_relationships(data()) :: resource_relationships() - @callback resource_relationships(data()) :: resource_relationships() @callback type() :: resource_type() | nil @callback polymorphic_type(data()) :: resource_type() | nil - @callback resource_type(data()) :: resource_type() @callback url_for(data(), Conn.t() | nil) :: String.t() @callback url_for_pagination(data(), Conn.t(), Paginator.params()) :: String.t() @callback url_for_rel(term(), String.t(), Conn.t() | nil) :: String.t() @@ -380,7 +377,6 @@ defmodule JSONAPI.View do def visible_fields(data, conn), do: View.visible_fields(__MODULE__, data, conn) - @impl View def resource_fields(data) do if @polymorphic_resource? do polymorphic_fields(data) @@ -389,7 +385,6 @@ defmodule JSONAPI.View do end end - @impl View def resource_type(data) do if @polymorphic_resource? do polymorphic_type(data) @@ -398,7 +393,6 @@ defmodule JSONAPI.View do end end - @impl View def resource_relationships(data) do if @polymorphic_resource? do polymorphic_relationships(data)