Skip to content

Commit

Permalink
Fully Support Sparse Fieldsets
Browse files Browse the repository at this point in the history
Whilst the `QueryParser` was correctly identifying requested fieldsets,
nothing was done to actually support this.

This change prunes returned fields to those requested should it be the
case.

Note that this change also includes a few more typespecs for functions I
touched or read.

Resolves #120
Closes #156
  • Loading branch information
jherdman committed Feb 6, 2019
1 parent a4655c4 commit e6bf362
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 7 deletions.
5 changes: 4 additions & 1 deletion lib/jsonapi/plugs/query_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,12 @@ defmodule JSONAPI.QueryParser do
For more details please see `JSONAPI.UnderscoreParameters`.
"""

@impl Plug
def init(opts) do
build_config(opts)
end

@impl Plug
def call(conn, opts) do
query_params_config_struct =
conn
Expand Down Expand Up @@ -111,7 +113,8 @@ defmodule JSONAPI.QueryParser do
end
end

def parse_fields(config, map) when map_size(map) == 0, do: config
@spec parse_fields(Config.t(), map()) :: Config.t() | no_return()
def parse_fields(%Config{} = config, fields) when fields == %{}, do: config

def parse_fields(%Config{} = config, fields) do
Enum.reduce(fields, config, fn {type, value}, acc ->
Expand Down
66 changes: 61 additions & 5 deletions lib/jsonapi/view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ defmodule JSONAPI.View do
## Fields
By default, the resulting JSON document consists of fields, defined in fields/0
By default, the resulting JSON document consists of fields, defined in `fields/0`
function. You can define custom fields or override current fields by defining
inside the view function `field_name/2` that takes data and conn as arguments.
inside the view function `field_name/2` that takes `data` and `conn` as arguments.
defmodule UserView do
use JSONAPI.View
Expand All @@ -51,6 +51,32 @@ defmodule JSONAPI.View do
def relationships, do: []
end
Fields may be omitted manually using the `hidden/1` function.
defmodule UserView do
use JSONAPI.View
def fields, do: [:id, :username, :email]
def type, do: "user"
def hidden(_data) do
[:email] # will be removed from the response
end
end
In order to use [sparse fieldsets](https://jsonapi.org/format/#fetching-sparse-fieldsets)
you must include the `JSONAPI.QueryParser` plug.
### Advanced Fields Usage
Note that the overriddable `attributes/2` method can be used for more control
over which fields are serialized from your view. It is **strongly** recommended
that you exhaustively try a combination of `fields/0`, and `hidden/1` first as
this method will likely be deprecated in a future release. Should you desire to
do so, you may wish to call `visisble_fields/2` within your implementation in
order to get the list of fields that will be sent to the client.
## Relationships
Currently the relationships callback expects that a map is returned
Expand Down Expand Up @@ -84,6 +110,9 @@ defmodule JSONAPI.View do
The default behaviour for `host` and `scheme` is to derive it from the `conn` provided, while the
default style for presentation in names is to be underscored and not dashed.
"""

alias Plug.Conn

defmacro __using__(opts \\ []) do
{type, opts} = Keyword.pop(opts, :type)
{namespace, _opts} = Keyword.pop(opts, :namespace, "")
Expand All @@ -104,10 +133,37 @@ defmodule JSONAPI.View do
def type, do: raise("Need to implement type/0")
end

def attributes(data, conn) do
hidden = hidden(data)
defp requested_fields_for_type(%Conn{assigns: %{jsonapi_query: %{fields: fields}}} = conn) do
fields[type()]
end

visible_fields = fields() -- hidden
defp requested_fields_for_type(_conn), do: nil

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

defp fields_for_type(requested_fields, fields) do
fields
|> MapSet.new()
|> MapSet.intersection(MapSet.new(requested_fields))
|> MapSet.to_list()
end

@spec visible_fields(map(), conn :: nil | Conn.t()) :: list(atom)
def visible_fields(data, conn) do
all_fields =
conn
|> requested_fields_for_type()
|> fields_for_type(fields())

hidden_fields = hidden(data)

all_fields -- hidden_fields
end

@spec attributes(map(), conn :: nil | Conn.t()) :: map()
def attributes(data, conn) do
visible_fields = visible_fields(data, conn)

Enum.reduce(visible_fields, %{}, fn field, intermediate_map ->
value =
Expand Down
31 changes: 30 additions & 1 deletion test/jsonapi/view_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,28 @@ defmodule JSONAPI.ViewTest do
assert data.meta.total_pages == 100
end

test "attributes/2 does not display hidden fields with deprecated hidden/0" do
test "visible_fields/2 returns all field names by default" do
data = %{age: 100, first_name: "Jason", last_name: "S", password: "securepw"}

assert [:age, :first_name, :last_name, :full_name] ==
UserView.visible_fields(data, %Plug.Conn{})
end

test "visible_fields/2 removes any hidden field names" do
data = %{title: "Hidden body", body: "Something"}

assert [:title] == PostView.visible_fields(data, %Plug.Conn{})
end

test "visible_fields/2 trims returned field names to only those requested" do
data = %{body: "Chunky", title: "Bacon"}
config = %JSONAPI.Config{fields: %{PostView.type() => [:body]}}
conn = %Plug.Conn{assigns: %{jsonapi_query: config}}

assert [:body] == PostView.visible_fields(data, conn)
end

test "attributes/2 does not display hidden fields" do
expected_map = %{age: 100, first_name: "Jason", last_name: "S", full_name: "Jason S"}

assert expected_map ==
Expand All @@ -161,4 +182,12 @@ defmodule JSONAPI.ViewTest do
nil
)
end

test "attributes/2 can return only requested fields" do
data = %{body: "Chunky", title: "Bacon"}
config = %JSONAPI.Config{fields: %{PostView.type() => [:body]}}
conn = %Plug.Conn{assigns: %{jsonapi_query: config}}

assert %{body: "Chunky"} == PostView.attributes(data, conn)
end
end
19 changes: 19 additions & 0 deletions test/jsonapi_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,25 @@ defmodule JSONAPITest do
assert Map.has_key?(json, "links")
end

test "handles sparse fields properly" do
conn =
:get
|> conn("/posts?include=other_user.company&fields[mytype]=text,excerpt")
|> Plug.Conn.assign(:data, [@default_data])
|> MyPostPlug.call([])

assert %{
"data" => [
%{
"attributes" => %{
"text" => "Hello",
"excerpt" => "He"
}
}
]
} = Jason.decode!(conn.resp_body)
end

test "omits explicit nil meta values as per http://jsonapi.org/format/#document-meta" do
conn =
:get
Expand Down

0 comments on commit e6bf362

Please sign in to comment.