Skip to content
Merged
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
4 changes: 2 additions & 2 deletions lib/jsonapi/plugs/query_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,9 @@ defmodule JSONAPI.QueryParser do
@spec get_valid_fields_for_type(Config.t(), String.t()) :: list(atom())
def get_valid_fields_for_type(%Config{view: view}, type) do
if type == view.type() do
view.fields()
view.valid_attrs_and_rels()
else
get_view_for_type(view, type).fields()
get_view_for_type(view, type).valid_attrs_and_rels()
end
end

Expand Down
6 changes: 3 additions & 3 deletions lib/jsonapi/serializer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,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
data
|> view.resource_relationships()
|> view.visible_relationships(conn)
|> Enum.filter(&assoc_loaded?(Map.get(data, get_data_key(&1))))
|> Enum.map_reduce(doc, &build_relationships(conn, view_info, &1, &2, options))
end
Expand Down Expand Up @@ -314,7 +314,7 @@ defmodule JSONAPI.Serializer do
end

defp get_default_includes(view, data) do
rels = view.resource_relationships(data)
rels = view.visible_relationships(data, nil)

Enum.filter(rels, &include_rel_by_default/1)
end
Expand All @@ -326,7 +326,7 @@ defmodule JSONAPI.Serializer do
end

defp get_query_includes(view, query_includes, data) do
rels = view.resource_relationships(data)
rels = view.visible_relationships(data, nil)

query_includes
|> Enum.map(fn
Expand Down
44 changes: 44 additions & 0 deletions lib/jsonapi/view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ defmodule JSONAPI.View do
2-arity function inside the view that takes `data` and `conn` as arguments and has
the same name as the field it will be producing. Refer to our `fullname/2` example below.

For historical reasons, the `View` module's "fields" are actually exclusively
attributes. That is, although JSON:API defines fields to be both attributes
and relationships, you should read the `View` module's macros as `field` ->
`attribute` and `relationship` -> `relationship`.

defmodule UserView do
use JSONAPI.View

Expand Down Expand Up @@ -233,6 +238,7 @@ defmodule JSONAPI.View do
@callback path() :: String.t() | nil
@callback relationships() :: resource_relationships()
@callback polymorphic_relationships(data()) :: resource_relationships()
@callback visible_relationships(data(), Conn.t() | nil) :: resource_relationships()
@callback type() :: resource_type() | nil
@callback polymorphic_type(data()) :: resource_type() | nil
@callback url_for(data(), Conn.t() | nil) :: String.t()
Expand Down Expand Up @@ -377,6 +383,10 @@ defmodule JSONAPI.View do
def visible_fields(data, conn),
do: View.visible_fields(__MODULE__, data, conn)

@impl View
def visible_relationships(data, conn),
do: View.visible_relationships(__MODULE__, data, conn)

def resource_fields(data) do
if @polymorphic_resource? do
polymorphic_fields(data)
Expand All @@ -401,6 +411,24 @@ defmodule JSONAPI.View do
end
end

@doc """
Get valid attribute and relationship names (collectively known in
JSON:API as "fields", but the name "fields" is taken and left intact for
backwards compatibility).

This function can be used before knowing the data being serialized and as
a consequence it cannot perform polymorphic logic; this is the only way
to operate from the QueryParser Plug.
"""
@spec valid_attrs_and_rels() :: [atom()]
def valid_attrs_and_rels do
if @polymorphic_resource? do
[]
else
fields() ++ Enum.map(relationships(), fn {name, _} -> name end)
end
end

defoverridable View

def index(models, conn, _params, meta \\ nil, options \\ []),
Expand Down Expand Up @@ -537,6 +565,22 @@ defmodule JSONAPI.View do
all_fields -- hidden_fields
end

@spec visible_relationships(t(), data(), Conn.t() | nil) :: resource_relationships()
def visible_relationships(view, data, conn) do
view
|> requested_fields_for_type(data, conn)
|> net_relationships_for_type(view.resource_relationships(data))
end

defp net_relationships_for_type(requested_fields, relationships) when requested_fields in [nil, %{}],
do: relationships

defp net_relationships_for_type(requested_fields, relationships) do
Enum.filter(relationships, fn {name, _} ->
Enum.any?(requested_fields, &Kernel.==(&1, name))
end)
end

defp net_fields_for_type(requested_fields, fields) when requested_fields in [nil, %{}],
do: fields

Expand Down
9 changes: 7 additions & 2 deletions test/jsonapi/plugs/query_parser_test.exs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
defmodule JSONAPI.QueryParserTest do
use ExUnit.Case
use Plug.Test

import JSONAPI.QueryParser
import Plug.{Conn, Test}

alias JSONAPI.Config
alias JSONAPI.Exceptions.InvalidQuery
Expand Down Expand Up @@ -153,7 +153,7 @@ defmodule JSONAPI.QueryParserTest do
assert parse_include(config, "author,comments.user").include == [:author, {:comments, :user}]
end

test "parse_fields/2 turns a fields map into a map of validated fields" do
test "parse_fields/2 turns a fields map containing an attribute into a map of validated fields" do
config = struct(Config, view: MyView)
assert parse_fields(config, %{"mytype" => "id,text"}).fields == %{"mytype" => [:id, :text]}
end
Expand All @@ -163,6 +163,11 @@ defmodule JSONAPI.QueryParserTest do
assert parse_fields(config, %{"mytype" => ""}).fields == %{"mytype" => []}
end

test "parse_fields/2 accepts relationship fields" do
config = struct(Config, view: MyView)
assert parse_fields(config, %{"mytype" => "id,author"}).fields == %{"mytype" => [:id, :author]}
end

test "parse_fields/2 raises on invalid parsing" do
config = struct(Config, view: MyView)

Expand Down
26 changes: 26 additions & 0 deletions test/jsonapi/serializer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,32 @@ defmodule JSONAPI.SerializerTest do
assert Enum.count(encoded[:included]) == 4
end

test "serialize handles sparse fieldsets" do
data = %{
id: 1,
text: "Hello",
body: "Hello world",
author: %{id: 2, username: "jason"},
best_comments: [
%{id: 5, text: "greatest comment ever", user: %{id: 4, username: "jack"}},
%{id: 6, text: "not so great", user: %{id: 2, username: "jason"}}
]
}

conn =
%Plug.Conn{}
|> Plug.Conn.fetch_query_params()
|> Plug.Conn.assign(:jsonapi_query, %Config{fields: %{PostView.type() => [:author, :text]}})

encoded = Serializer.serialize(PostView, data, conn)

assert is_nil(encoded[:data][:attributes][:body])
assert %{text: "Hello"} = encoded[:data][:attributes]

assert is_nil(encoded[:data][:relationships][:best_comments])
assert %{author: %{data: %{id: "2", type: "user"}}} = encoded[:data][:relationships]
end

test "serialize handles a list" do
data = %{
id: 1,
Expand Down
22 changes: 22 additions & 0 deletions test/jsonapi/view_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ defmodule JSONAPI.ViewTest do
end

def hidden(_), do: []

def relationships do
[{:comments, JSONAPI.ViewTest.CommentView}]
end
end

defmodule CommentView do
Expand Down Expand Up @@ -41,6 +45,8 @@ defmodule JSONAPI.ViewTest do

defmodule CarView do
use JSONAPI.View, type: "cars", namespace: ""

def fields, do: []
end

defmodule DynamicView do
Expand Down Expand Up @@ -369,4 +375,20 @@ defmodule JSONAPI.ViewTest do
some_other_field: "foo"
} == PolymorphicView.attributes(data, conn)
end

test "visible_relationships/2 returns all relationship fields by default" do
data = %{comments: [%{body: "hello"}]}
config = %JSONAPI.Config{}
conn = %Plug.Conn{assigns: %{jsonapi_query: config}}

assert [{:comments, CommentView}] = PostView.visible_relationships(data, conn)
end

test "visible_relationships/2 can omit relationships based on requested fields" do
data = %{body: "Chunky", title: "Bacon", comments: [%{body: "hello"}]}
config = %JSONAPI.Config{fields: %{PostView.type() => [:body]}}
conn = %Plug.Conn{assigns: %{jsonapi_query: config}}

assert [] = PostView.visible_relationships(data, conn)
end
end
Loading