- Add 17 missing @spec annotations (layouts, error_json, error_html, skewt_svg) - Move 12+ nested alias/import/require to module top level - Add phx-change/id attributes to 11 raw HTML <form> tags - Remove 4 unused LiveView assigns (:bounds, :data_provider) - Add 3 missing doctest references (HrrrNativeClient, BulkFetch, Accounts) - Break 2 long lines (path_compute.ex:382) - Strengthen weak test assertions (is_binary→byte_size, is_list→!=[]) - Replace Module.concat with Module.safe_concat (2 occurrences) - Replace length/1 > 0 with list != [] (9 occurrences) - Remove no-op assert true, fix no-assertion tests Remaining: 24 socket.assigns introspection warnings (deliberate test pattern for observable behavior testing), 1 formatter-resistant long line, 3 app-code usage warnings.
86 lines
2.9 KiB
Elixir
86 lines
2.9 KiB
Elixir
defmodule MicrowavepropWeb.Api.ErrorJSON do
|
|
@moduledoc """
|
|
RFC 9457 problem+json error responses for `/api/v1`.
|
|
|
|
All error bodies share the shape:
|
|
|
|
{
|
|
"type": "about:blank",
|
|
"title": "<short slug>",
|
|
"status": <int>,
|
|
"detail": "<human-readable explanation>",
|
|
"errors": <optional changeset error map>
|
|
}
|
|
"""
|
|
|
|
import Plug.Conn
|
|
|
|
alias Ecto.Changeset
|
|
alias Plug.Conn.Status
|
|
|
|
@content_type "application/problem+json"
|
|
|
|
@doc "Halts the connection with a problem+json body."
|
|
@spec send_problem(Plug.Conn.t(), pos_integer(), String.t(), String.t(), map()) :: Plug.Conn.t()
|
|
def send_problem(conn, status, title, detail, extra \\ %{}) do
|
|
body =
|
|
%{
|
|
type: "about:blank",
|
|
title: title,
|
|
status: status,
|
|
detail: detail
|
|
}
|
|
|> Map.merge(extra)
|
|
|> Jason.encode!()
|
|
|
|
conn
|
|
|> put_resp_content_type(@content_type)
|
|
|> send_resp(status, body)
|
|
|> halt()
|
|
end
|
|
|
|
@doc "Halts with a 422 problem+json including a flattened changeset error map."
|
|
@spec send_changeset(Plug.Conn.t(), Changeset.t()) :: Plug.Conn.t()
|
|
def send_changeset(conn, %Changeset{} = changeset) do
|
|
send_problem(
|
|
conn,
|
|
422,
|
|
"validation_failed",
|
|
"One or more fields are invalid.",
|
|
%{errors: translate_errors(changeset)}
|
|
)
|
|
end
|
|
|
|
@doc "Translates a changeset's errors into a flat map of field -> [messages]."
|
|
@spec translate_errors(Changeset.t()) :: map()
|
|
def translate_errors(%Changeset{} = changeset) do
|
|
Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
|
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
|
String.replace(acc, "%{#{key}}", to_string(value))
|
|
end)
|
|
end)
|
|
end
|
|
|
|
## Default Phoenix error renderer hooks ----------------------------
|
|
|
|
@doc false
|
|
@spec render(String.t(), map()) :: map()
|
|
def render("400.json", _assigns), do: error_body(400, "bad_request", "Malformed request.")
|
|
def render("401.json", _assigns), do: error_body(401, "unauthorized", "Authentication is required.")
|
|
def render("403.json", _assigns), do: error_body(403, "forbidden", "You may not access this resource.")
|
|
def render("404.json", _assigns), do: error_body(404, "not_found", "Resource not found.")
|
|
def render("405.json", _assigns), do: error_body(405, "method_not_allowed", "HTTP method not allowed.")
|
|
def render("415.json", _assigns), do: error_body(415, "unsupported_media_type", "Unsupported media type.")
|
|
def render("429.json", _assigns), do: error_body(429, "too_many_requests", "Rate limit exceeded.")
|
|
def render("500.json", _assigns), do: error_body(500, "internal_server_error", "Something went wrong.")
|
|
|
|
def render(template, _assigns) do
|
|
[code | _] = String.split(template, ".")
|
|
status = String.to_integer(code)
|
|
error_body(status, status |> Status.reason_atom() |> Atom.to_string(), "")
|
|
end
|
|
|
|
defp error_body(status, title, detail) do
|
|
%{type: "about:blank", title: title, status: status, detail: detail}
|
|
end
|
|
end
|