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 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
60 changes: 58 additions & 2 deletions lib/jsonapi/error_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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()
def build_error(title, status, detail, pointer \\ nil, meta \\ nil) do
Expand Down Expand Up @@ -93,12 +94,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(binary()) :: map()
def missing_relationship_data_param_error(relationship_name) do
matt-glover marked this conversation as resolved.
Show resolved Hide resolved
"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(binary()) :: map()
def missing_relationship_data_id_param_error(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(binary()) :: map()
def missing_relationship_data_type_param_error(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 @@ -122,10 +168,20 @@ defmodule JSONAPI.ErrorView do

@spec serialize_error(map()) :: 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
51 changes: 51 additions & 0 deletions lib/jsonapi/plugs/format_required.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,57 @@ 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(relationship_name)
[error | acc]

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

{relationship_name, %{"data" => %{}}}, acc ->
id_error = missing_relationship_data_id_param_error(relationship_name)
type_error = missing_relationship_data_type_param_error(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
acc

{relationship_name, _}, acc ->
error = missing_relationship_data_param_error(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