diff --git a/lib/jsonapi.ex b/lib/jsonapi.ex index b9446553..d386dddb 100644 --- a/lib/jsonapi.ex +++ b/lib/jsonapi.ex @@ -3,6 +3,8 @@ defmodule JSONAPI do A module for working with the JSON API specification in Elixir """ + @mime_type "application/vnd.api+json" + @doc """ Returns the configured JSON encoding library for JSONAPI. To customize the JSON library, including the following @@ -26,8 +28,6 @@ defmodule JSONAPI do end end - @mime_type "application/vnd.api+json" - @doc """ This returns the MIME type for JSONAPIs """ diff --git a/lib/jsonapi/error_view.ex b/lib/jsonapi/error_view.ex index e76fe41e..5f8cf4b7 100644 --- a/lib/jsonapi/error_view.ex +++ b/lib/jsonapi/error_view.ex @@ -109,8 +109,8 @@ defmodule JSONAPI.ErrorView do @spec missing_relationship_data_param_error_attrs(binary()) :: error_attrs() def missing_relationship_data_param_error_attrs(relationship_name) do - "Missing data member in relationship" - |> build_error( + build_error( + "Missing data member in relationship", 400, "Check out https://jsonapi.org/format/#crud-creating and https://jsonapi.org/format/#crud-updating-resource-relationships for more info.", "/data/relationships/#{relationship_name}/data" @@ -119,8 +119,8 @@ defmodule JSONAPI.ErrorView do @spec missing_relationship_data_id_param_error_attrs(binary()) :: error_attrs() def missing_relationship_data_id_param_error_attrs(relationship_name) do - "Missing id in relationship data parameter" - |> build_error( + build_error( + "Missing id in relationship data parameter", 400, @relationship_resource_linkage_message, "/data/relationships/#{relationship_name}/data/id" @@ -129,8 +129,8 @@ defmodule JSONAPI.ErrorView do @spec missing_relationship_data_type_param_error_attrs(binary()) :: error_attrs() def missing_relationship_data_type_param_error_attrs(relationship_name) do - "Missing type in relationship data parameter" - |> build_error( + build_error( + "Missing type in relationship data parameter", 400, @relationship_resource_linkage_message, "/data/relationships/#{relationship_name}/data/type" diff --git a/lib/jsonapi/plugs/query_parser.ex b/lib/jsonapi/plugs/query_parser.ex index ab12ccb2..81f7e291 100644 --- a/lib/jsonapi/plugs/query_parser.ex +++ b/lib/jsonapi/plugs/query_parser.ex @@ -1,11 +1,4 @@ defmodule JSONAPI.QueryParser do - @behaviour Plug - alias JSONAPI.{Config, Deprecation} - alias JSONAPI.Exceptions.InvalidQuery - alias Plug.Conn - import JSONAPI.Utils.IncludeTree - import JSONAPI.Utils.String, only: [underscore: 1] - @moduledoc """ Implements a fully JSONAPI V1 spec for parsing a complex query string via the `query_params` field from a `Plug.Conn` struct and returning Elixir datastructures. @@ -82,6 +75,14 @@ defmodule JSONAPI.QueryParser do For more details please see `JSONAPI.UnderscoreParameters`. """ + @behaviour Plug + + import JSONAPI.Utils.IncludeTree + import JSONAPI.Utils.String, only: [underscore: 1] + + alias JSONAPI.Exceptions.InvalidQuery + alias JSONAPI.{Config, Deprecation} + alias Plug.Conn @impl Plug def init(opts) do @@ -119,7 +120,7 @@ defmodule JSONAPI.QueryParser do Enum.reduce(filter, config, fn {key, val}, acc -> check_filter_validity!(opts_filter, key, config) - %{acc | filter: Keyword.put(acc.filter, String.to_atom(key), val)} + %{acc | filter: Keyword.put(acc.filter, String.to_existing_atom(key), val)} end) end @@ -183,7 +184,7 @@ defmodule JSONAPI.QueryParser do raise InvalidQuery, resource: config.view.type(), param: field, param_type: :sort end - build_sort(direction, String.to_atom(field)) + build_sort(direction, String.to_existing_atom(field)) end) |> List.flatten() @@ -210,26 +211,28 @@ defmodule JSONAPI.QueryParser do |> Enum.filter(&(&1 !== "")) |> Enum.map(&underscore/1) - Enum.reduce(includes, [], fn inc, acc -> - check_include_validity!(inc, config) + Enum.reduce(includes, [], &include_reducer(config, valid_includes, &1, &2)) + end + + defp include_reducer(config, valid_includes, inc, acc) do + check_include_validity!(inc, config) - if inc =~ ~r/\w+\.\w+/ do - acc ++ handle_nested_include(inc, valid_includes, config) - else - inc = - try do - String.to_existing_atom(inc) - rescue - ArgumentError -> raise_invalid_include_query(inc, config.view.type()) - end - - if Enum.any?(valid_includes, fn {key, _val} -> key == inc end) do - acc ++ [inc] - else - raise_invalid_include_query(inc, config.view.type()) + if inc =~ ~r/\w+\.\w+/ do + acc ++ handle_nested_include(inc, valid_includes, config) + else + inc = + try do + String.to_existing_atom(inc) + rescue + ArgumentError -> raise_invalid_include_query(inc, config.view.type()) end + + if Enum.any?(valid_includes, fn {key, _val} -> key == inc end) do + Enum.reverse([inc | acc]) + else + raise_invalid_include_query(inc, config.view.type()) end - end) + end end defp check_include_validity!(key, %Config{opts: opts, view: view}) do diff --git a/lib/jsonapi/serializer.ex b/lib/jsonapi/serializer.ex index 0ea777e8..caaa4c3e 100644 --- a/lib/jsonapi/serializer.ex +++ b/lib/jsonapi/serializer.ex @@ -2,12 +2,11 @@ defmodule JSONAPI.Serializer do @moduledoc """ Serialize a map of data into a properly formatted JSON API response object """ + require Logger alias JSONAPI.{Config, Utils, View} alias Plug.Conn - require Logger - @type document :: map() @doc """ @@ -51,7 +50,7 @@ defmodule JSONAPI.Serializer do def encode_data(view, data, conn, query_includes, options) when is_list(data) do Enum.map_reduce(data, [], fn d, acc -> {to_include, encoded_data} = encode_data(view, d, conn, query_includes, options) - {to_include, acc ++ [encoded_data]} + {to_include, Enum.reverse([encoded_data | acca])} end) end @@ -115,7 +114,7 @@ defmodule JSONAPI.Serializer do if is_list(query_includes) do query_includes |> Enum.reduce([], fn - {^relationship_name, value}, acc -> acc ++ [value] + {^relationship_name, value}, acc -> Enum.reverse([value | acc]) _, acc -> acc end) |> List.flatten() @@ -126,7 +125,7 @@ defmodule JSONAPI.Serializer do {rel_included, encoded_rel} = encode_data(rel_view, rel_data, conn, rel_query_includes, options) - {rel_included ++ [encoded_rel], acc} + {Enum.reverse([encoded_rel | rel_included]), acc} else {nil, acc} end @@ -208,15 +207,15 @@ defmodule JSONAPI.Serializer do end defp merge_base_links(%{links: links} = doc, data, view, conn) do - view_links = Map.merge(view.links(data, conn), links) + view_links = data |> view.links(conn) |> Map.merge(links) Map.merge(doc, %{links: view_links}) end defp merge_links(doc, data, view, conn, page, false, options) when is_list(data) do links = - Map.merge(view.pagination_links(data, conn, page, options), %{ - self: view.url_for_pagination(data, conn, page) - }) + data + |> view.pagination_links(conn, page, options) + |> Map.merge(%{self: view.url_for_pagination(data, conn, page)}) doc |> Map.merge(%{links: links}) diff --git a/lib/jsonapi/utils/data_to_params.ex b/lib/jsonapi/utils/data_to_params.ex index c442b773..3d50ddac 100644 --- a/lib/jsonapi/utils/data_to_params.ex +++ b/lib/jsonapi/utils/data_to_params.ex @@ -80,8 +80,8 @@ defmodule JSONAPI.Utils.DataToParams do defp update_list_relationship(existing, id) do case existing do - val when is_list(val) -> {val, val ++ [id]} - val when is_binary(val) -> {val, [val] ++ [id]} + val when is_list(val) -> {val, Enum.reverse([id | val])} + val when is_binary(val) -> {val, [val, id]} _ -> {nil, id} end end diff --git a/lib/jsonapi/utils/string.ex b/lib/jsonapi/utils/string.ex index 6877d9c4..4f3d1cfd 100644 --- a/lib/jsonapi/utils/string.ex +++ b/lib/jsonapi/utils/string.ex @@ -134,8 +134,7 @@ defmodule JSONAPI.Utils.String do # If there are multiple words, perform the camelizing [h | t] -> - [String.downcase(h) | camelize_list(t)] - |> Enum.join() + Enum.join([String.downcase(h) | camelize_list(t)]) end end end diff --git a/lib/jsonapi/view.ex b/lib/jsonapi/view.ex index 5198ca2a..cbbf46e5 100644 --- a/lib/jsonapi/view.ex +++ b/lib/jsonapi/view.ex @@ -392,7 +392,7 @@ defmodule JSONAPI.View do do: Application.get_env(:jsonapi, :host, host) defp port(%Conn{port: 0} = conn), - do: port(%{conn | port: URI.default_port(scheme(conn))}) + do: port(%{conn | port: conn |> scheme() |> URI.default_port()}) defp port(%Conn{port: port}), do: Application.get_env(:jsonapi, :port, port) diff --git a/test/jsonapi/plugs/content_type_negotiation_test.exs b/test/jsonapi/plugs/content_type_negotiation_test.exs index 91de6a47..975f1e7b 100644 --- a/test/jsonapi/plugs/content_type_negotiation_test.exs +++ b/test/jsonapi/plugs/content_type_negotiation_test.exs @@ -2,10 +2,10 @@ defmodule JSONAPI.ContentTypeNegotiationTest do use ExUnit.Case use Plug.Test - alias JSONAPI.ContentTypeNegotiation - import JSONAPI, only: [mime_type: 0] + alias JSONAPI.ContentTypeNegotiation + test "passes request through" do conn = :post diff --git a/test/jsonapi/plugs/deserializer_test.exs b/test/jsonapi/plugs/deserializer_test.exs index 7da85120..6fb8756e 100644 --- a/test/jsonapi/plugs/deserializer_test.exs +++ b/test/jsonapi/plugs/deserializer_test.exs @@ -2,6 +2,8 @@ defmodule JSONAPI.DeserializerTest do use ExUnit.Case use Plug.Test + @ct JSONAPI.mime_type() + defmodule ExamplePlug do use Plug.Builder plug(Plug.Parsers, parsers: [:json], json_decoder: Jason) @@ -13,11 +15,10 @@ defmodule JSONAPI.DeserializerTest do end end - @ct JSONAPI.mime_type() - test "Ignores bodyless requests" do conn = - Plug.Test.conn("GET", "/") + "GET" + |> Plug.Test.conn("/") |> put_req_header("content-type", @ct) |> put_req_header("accept", @ct) @@ -29,7 +30,8 @@ defmodule JSONAPI.DeserializerTest do req_body = Jason.encode!(%{"some-nonsense" => "yup"}) conn = - Plug.Test.conn("POST", "/", req_body) + "POST" + |> Plug.Test.conn("/", req_body) |> put_req_header("content-type", @ct) |> put_req_header("accept", @ct) @@ -46,10 +48,10 @@ defmodule JSONAPI.DeserializerTest do ] }) - conn = - Plug.Test.conn("POST", "/", req_body) - |> put_req_header("content-type", @ct) - |> put_req_header("accept", @ct) + "POST" + |> Plug.Test.conn("/", req_body) + |> put_req_header("content-type", @ct) + |> put_req_header("accept", @ct) result = ExamplePlug.call(conn, []) @@ -85,7 +87,8 @@ defmodule JSONAPI.DeserializerTest do }) conn = - Plug.Test.conn("POST", "/", req_body) + "POST" + |> Plug.Test.conn("/", req_body) |> put_req_header("content-type", @ct) |> put_req_header("accept", @ct) @@ -135,7 +138,8 @@ defmodule JSONAPI.DeserializerTest do }) conn = - Plug.Test.conn("POST", "/", req_body) + "POST" + |> Plug.Test.conn("/", req_body) |> put_req_header("content-type", @ct) |> put_req_header("accept", @ct) @@ -192,7 +196,8 @@ defmodule JSONAPI.DeserializerTest do }) conn = - Plug.Test.conn("POST", "/", req_body) + "POST" + |> Plug.Test.conn("/", req_body) |> put_req_header("content-type", @ct) |> put_req_header("accept", @ct) diff --git a/test/jsonapi/plugs/query_parser_test.exs b/test/jsonapi/plugs/query_parser_test.exs index 84cccdf1..0462301a 100644 --- a/test/jsonapi/plugs/query_parser_test.exs +++ b/test/jsonapi/plugs/query_parser_test.exs @@ -3,8 +3,9 @@ defmodule JSONAPI.QueryParserTest do use Plug.Test import JSONAPI.QueryParser - alias JSONAPI.Exceptions.InvalidQuery + alias JSONAPI.Config + alias JSONAPI.Exceptions.InvalidQuery defmodule MyView do use JSONAPI.View diff --git a/test/jsonapi/serializer_test.exs b/test/jsonapi/serializer_test.exs index 14cb113c..7a556728 100644 --- a/test/jsonapi/serializer_test.exs +++ b/test/jsonapi/serializer_test.exs @@ -25,7 +25,7 @@ defmodule JSONAPI.SerializerTest do @behaviour JSONAPI.Paginator - @impl true + @impl JSONAPI.Paginator def paginate(data, view, conn, page, options) do number = page @@ -37,9 +37,7 @@ defmodule JSONAPI.SerializerTest do |> Map.get("size", "0") |> String.to_integer() - total_pages = - options - |> Keyword.get(:total_pages, 0) + total_pages = Keyword.get(options, :total_pages, 0) %{ first: view.url_for_pagination(data, conn, %{page | "page" => "1"}), @@ -378,12 +376,11 @@ defmodule JSONAPI.SerializerTest do } conn = - %Plug.Conn{ + Plug.Conn.fetch_query_params(%Plug.Conn{ assigns: %{ jsonapi_query: %Config{} } - } - |> Plug.Conn.fetch_query_params() + }) encoded = Serializer.serialize(CommentaryView, data, conn) @@ -411,14 +408,13 @@ defmodule JSONAPI.SerializerTest do } conn = - %Plug.Conn{ + Plug.Conn.fetch_query_params(%Plug.Conn{ assigns: %{ jsonapi_query: %Config{ include: [best_comments: :user] } } - } - |> Plug.Conn.fetch_query_params() + }) encoded = Serializer.serialize(PostView, data, conn) @@ -438,14 +434,13 @@ defmodule JSONAPI.SerializerTest do } conn = - %Plug.Conn{ + Plug.Conn.fetch_query_params(%Plug.Conn{ assigns: %{ jsonapi_query: %Config{ include: [:company] } } - } - |> Plug.Conn.fetch_query_params() + }) encoded = Serializer.serialize(UserView, data, conn) @@ -465,14 +460,13 @@ defmodule JSONAPI.SerializerTest do } conn = - %Plug.Conn{ + Plug.Conn.fetch_query_params(%Plug.Conn{ assigns: %{ jsonapi_query: %Config{ include: [company: :industry] } } - } - |> Plug.Conn.fetch_query_params() + }) encoded = Serializer.serialize(UserView, data, conn) @@ -503,14 +497,13 @@ defmodule JSONAPI.SerializerTest do } conn = - %Plug.Conn{ + Plug.Conn.fetch_query_params(%Plug.Conn{ assigns: %{ jsonapi_query: %Config{ include: [company: [industry: :tags]] } } - } - |> Plug.Conn.fetch_query_params() + }) encoded = Serializer.serialize(UserView, data, conn) @@ -823,9 +816,7 @@ defmodule JSONAPI.SerializerTest do end test "extrapolates relationship config with default include" do - configs = - PostView.relationships() - |> Enum.map(&Serializer.extrapolate_relationship_config/1) + configs = Enum.map(PostView.relationships(), &Serializer.extrapolate_relationship_config/1) assert configs == [ {:author, :author, JSONAPI.SerializerTest.UserView, true}, @@ -834,9 +825,7 @@ defmodule JSONAPI.SerializerTest do end test "extrapolates relationship config with rewritten name" do - configs = - CommentaryView.relationships() - |> Enum.map(&Serializer.extrapolate_relationship_config/1) + configs = Enum.map(CommentaryView.relationships(), &Serializer.extrapolate_relationship_config/1) assert configs == [ {:commenter, :user1, JSONAPI.SerializerTest.UserView, true}, diff --git a/test/jsonapi_test.exs b/test/jsonapi_test.exs index ba6e6794..a46ae2fa 100644 --- a/test/jsonapi_test.exs +++ b/test/jsonapi_test.exs @@ -110,7 +110,7 @@ defmodule JSONAPITest do |> Plug.Conn.fetch_query_params() |> MyPostPlug.call([]) - json = conn.resp_body |> Jason.decode!() + json = Jason.decode!(conn.resp_body) assert Map.has_key?(json, "data") data_list = Map.get(json, "data") @@ -127,7 +127,7 @@ defmodule JSONAPITest do relationships = Map.get(data, "relationships") assert map_size(relationships) == 2 - assert Enum.sort(Map.keys(relationships)) == ["author", "other_user"] + assert relationships |> Map.keys() |> Enum.sort() == ["author", "other_user"] author_rel = Map.get(relationships, "author") assert get_in(author_rel, ["data", "type"]) == "user" @@ -153,7 +153,7 @@ defmodule JSONAPITest do |> Plug.Conn.fetch_query_params() |> MyPostPlug.call([]) - json = conn.resp_body |> Jason.decode!() + json = Jason.decode!(conn.resp_body) assert Map.has_key?(json, "data") data_list = Map.get(json, "data") @@ -165,7 +165,7 @@ defmodule JSONAPITest do relationships = Map.get(data, "relationships") assert map_size(relationships) == 2 - assert Enum.sort(Map.keys(relationships)) == ["author", "other_user"] + assert relationships |> Map.keys() |> Enum.sort() == ["author", "other_user"] author_rel = Map.get(relationships, "author") assert get_in(author_rel, ["data", "type"]) == "user" @@ -200,7 +200,7 @@ defmodule JSONAPITest do |> Plug.Conn.fetch_query_params() |> MyPostPlug.call([]) - json = conn.resp_body |> Jason.decode!() + json = Jason.decode!(conn.resp_body) assert Map.has_key?(json, "data") data_list = Map.get(json, "data") @@ -212,7 +212,7 @@ defmodule JSONAPITest do relationships = Map.get(data, "relationships") assert map_size(relationships) == 2 - assert Enum.sort(Map.keys(relationships)) == ["author", "other_user"] + assert relationships |> Map.keys() |> Enum.sort() == ["author", "other_user"] author_rel = Map.get(relationships, "author") assert get_in(author_rel, ["data", "type"]) == "user" @@ -266,7 +266,7 @@ defmodule JSONAPITest do |> Plug.Conn.fetch_query_params() |> MyPostPlug.call([]) - json = conn.resp_body |> Jason.decode!() + json = Jason.decode!(conn.resp_body) assert Map.has_key?(json, "data") data_list = Map.get(json, "data") @@ -278,7 +278,7 @@ defmodule JSONAPITest do relationships = Map.get(data, "relationships") assert map_size(relationships) == 2 - assert Enum.sort(Map.keys(relationships)) == ["author", "other_user"] + assert relationships |> Map.keys() |> Enum.sort() == ["author", "other_user"] author_rel = Map.get(relationships, "author") assert get_in(author_rel, ["data", "type"]) == "user" @@ -412,7 +412,7 @@ defmodule JSONAPITest do |> Plug.Conn.fetch_query_params() |> MyPostPlug.call([]) - json = conn.resp_body |> Jason.decode!() + json = Jason.decode!(conn.resp_body) refute Map.has_key?(json, "meta") end @@ -425,7 +425,7 @@ defmodule JSONAPITest do |> Plug.Conn.fetch_query_params() |> MyPostPlug.call([]) - json = conn.resp_body |> Jason.decode!() + json = Jason.decode!(conn.resp_body) refute Map.has_key?(json, "meta") end diff --git a/test/utils/data_to_params_test.exs b/test/utils/data_to_params_test.exs index 158cc781..555191b8 100644 --- a/test/utils/data_to_params_test.exs +++ b/test/utils/data_to_params_test.exs @@ -1,6 +1,8 @@ defmodule JSONAPI.DataToParamsTest do use ExUnit.Case + alias JSONAPI.Utils.DataToParams + test "converts attributes and relationships to flattened data structure" do incoming = %{ "data" => %{ @@ -23,7 +25,7 @@ defmodule JSONAPI.DataToParamsTest do } } - result = JSONAPI.Utils.DataToParams.process(incoming) + result = DataToParams.process(incoming) assert result == %{ "id" => "1", @@ -53,7 +55,7 @@ defmodule JSONAPI.DataToParamsTest do } } - result = JSONAPI.Utils.DataToParams.process(incoming) + result = DataToParams.process(incoming) assert result == %{ "id" => "1", @@ -82,7 +84,7 @@ defmodule JSONAPI.DataToParamsTest do } } - result = JSONAPI.Utils.DataToParams.process(incoming) + result = DataToParams.process(incoming) assert result == %{ "id" => "1", @@ -115,7 +117,7 @@ defmodule JSONAPI.DataToParamsTest do ] } - result = JSONAPI.Utils.DataToParams.process(incoming) + result = DataToParams.process(incoming) assert result == %{ "friend" => [ @@ -182,7 +184,7 @@ defmodule JSONAPI.DataToParamsTest do ] } - result = JSONAPI.Utils.DataToParams.process(incoming) + result = DataToParams.process(incoming) assert result == %{ "friend" => [ @@ -220,7 +222,7 @@ defmodule JSONAPI.DataToParamsTest do ] } - result = JSONAPI.Utils.DataToParams.process(incoming) + result = DataToParams.process(incoming) assert result == [ %{"id" => "1", "type" => "user"}, @@ -239,7 +241,7 @@ defmodule JSONAPI.DataToParamsTest do "included" => nil } - result = JSONAPI.Utils.DataToParams.process(incoming) + result = DataToParams.process(incoming) assert result == %{ "id" => "1", @@ -255,7 +257,7 @@ defmodule JSONAPI.DataToParamsTest do } } - result = JSONAPI.Utils.DataToParams.process(incoming) + result = DataToParams.process(incoming) assert result == %{ "id" => "1", @@ -268,7 +270,7 @@ defmodule JSONAPI.DataToParamsTest do "data" => nil } - result = JSONAPI.Utils.DataToParams.process(incoming) + result = DataToParams.process(incoming) assert result == nil end