Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Format required checks for relationships #299

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
66 changes: 62 additions & 4 deletions lib/jsonapi/error_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ defmodule JSONAPI.ErrorView do
import Plug.Conn, only: [send_resp: 3, halt: 1, put_resp_content_type: 2]

@crud_message "Check out http://jsonapi.org/format/#crud for more info."
@relationship_resource_linkage_message "Check out https://jsonapi.org/format/#document-resource-object-linkage for more info."

@spec build_error(binary(), pos_integer(), binary() | nil, binary() | nil) :: map()
@type error_attrs :: map()

@spec build_error(binary(), pos_integer(), binary() | nil, binary() | nil) :: error_attrs()
def build_error(title, status, detail, pointer \\ nil, meta \\ nil) do
error = %{
detail: detail,
Expand Down Expand Up @@ -93,12 +96,57 @@ defmodule JSONAPI.ErrorView do
|> serialize_error
end

@spec relationships_missing_object :: map()
def relationships_missing_object do
"Relationships parameter is not an object"
|> build_error(
400,
"Check out https://jsonapi.org/format/#document-resource-object-relationships for more info.",
"/data/relationships"
)
|> serialize_error
end

@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(
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"
)
end

@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(
400,
@relationship_resource_linkage_message,
"/data/relationships/#{relationship_name}/data/id"
)
end

@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(
400,
@relationship_resource_linkage_message,
"/data/relationships/#{relationship_name}/data/type"
)
end

@spec send_error(Plug.Conn.t(), term()) :: term()
def send_error(conn, %{errors: [%{status: status}]} = error),
do: send_error(conn, status, error)

def send_error(conn, %{errors: errors} = error) when is_list(errors) do
status = Enum.max_by(errors, &Map.get(&1, :status))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best I can tell this was not called on the latest stable version prior to this PR. I think this is a safe change as long as it works in the new workflows.

status =
errors
|> Enum.max_by(&Map.get(&1, :status))
|> Map.get(:status)

send_error(conn, status, error)
end

Expand All @@ -120,12 +168,22 @@ defmodule JSONAPI.ErrorView do
|> halt
end

@spec serialize_error(map()) :: map()
@spec serialize_error(error_attrs()) :: map()
def serialize_error(error) do
error = Map.take(error, [:detail, :id, :links, :meta, :source, :status, :title])
error = extract_error(error)
%{errors: [error]}
end

@spec serialize_errors(list()) :: map()
def serialize_errors(errors) do
extracted = Enum.map(errors, &extract_error/1)
%{errors: extracted}
end

defp extract_error(error) do
Map.take(error, [:detail, :id, :links, :meta, :source, :status, :title])
end

defp append_field(error, _field, nil), do: error
defp append_field(error, :meta, value), do: Map.put(error, :meta, %{meta: value})
defp append_field(error, :source, value), do: Map.put(error, :source, %{pointer: value})
Expand Down
54 changes: 54 additions & 0 deletions lib/jsonapi/plugs/format_required.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,60 @@ defmodule JSONAPI.FormatRequired do

def call(%{method: method} = conn, _opts) when method in ~w[DELETE GET HEAD], do: conn

def call(
%{method: method, params: %{"data" => %{"type" => _, "relationships" => relationships}}} =
conn,
_
)
when method in ~w[POST PATCH] and not is_map(relationships) do
send_error(conn, relationships_missing_object())
end

def call(
%{
method: method,
params: %{"data" => %{"type" => _, "relationships" => relationships}}
} = conn,
_
)
when method in ~w[POST PATCH] and is_map(relationships) do
errors =
Enum.reduce(relationships, [], fn
{_relationship_name, %{"data" => %{"type" => _type, "id" => _}}}, acc ->
acc

{relationship_name, %{"data" => %{"type" => _type}}}, acc ->
error = missing_relationship_data_id_param_error_attrs(relationship_name)
[error | acc]

{relationship_name, %{"data" => %{"id" => _type}}}, acc ->
error = missing_relationship_data_type_param_error_attrs(relationship_name)
[error | acc]

{relationship_name, %{"data" => %{}}}, acc ->
id_error = missing_relationship_data_id_param_error_attrs(relationship_name)
type_error = missing_relationship_data_type_param_error_attrs(relationship_name)
[id_error | [type_error | acc]]

{_relationship_name, %{"data" => _}}, acc ->
# Allow things other than resource identifier objects per https://jsonapi.org/format/#document-resource-object-linkage
matt-glover marked this conversation as resolved.
Show resolved Hide resolved
# - null for empty to-one relationships.
# - an empty array ([]) for empty to-many relationships.
# - an array of resource identifier objects for non-empty to-many relationships.
acc

{relationship_name, _}, acc ->
error = missing_relationship_data_param_error_attrs(relationship_name)
[error | acc]
end)

if Enum.empty?(errors) do
conn
else
send_error(conn, serialize_errors(errors))
end
end

def call(%{method: "POST", params: %{"data" => %{"type" => _}}} = conn, _), do: conn

def call(%{method: method, params: %{"data" => [%{"type" => _} | _]}} = conn, _)
Expand Down
Loading