Packages

Production-grade Elixir client for the SaltEdge API v6 (AIS · PIS · Data Enrichment)

Current section

Files

Jump to
salt_edge_client lib salt_edge_client error.ex
Raw

lib/salt_edge_client/error.ex

defmodule SaltEdgeClient.Error do
@moduledoc """
Structured error type returned by all SaltEdge API calls.
Every API function returns either `{:ok, result}` or `{:error, %SaltEdgeClient.Error{}}`.
## Fields
- `:status` — HTTP status code (integer), or `:network` / `:decode` / `:timeout`
- `:class` — SaltEdge error class string (e.g. `"CustomerNotFound"`)
- `:message` — Human-readable error message
- `:request_id` — SaltEdge request ID for support tickets
- `:raw` — Raw response body (binary) for debugging
## Pattern matching
case SaltEdgeClient.AIS.Customers.show("bad-id") do
{:ok, customer} -> customer
{:error, %SaltEdgeClient.Error{status: 404, class: "CustomerNotFound"}} -> :not_found
{:error, %SaltEdgeClient.Error{status: s}} when s >= 500 -> :server_error
{:error, %SaltEdgeClient.Error{status: :network}} -> :network_error
end
"""
@type status :: pos_integer() | :network | :decode | :timeout
@type t :: %__MODULE__{
status: status(),
class: String.t() | nil,
message: String.t(),
request_id: String.t() | nil,
raw: binary() | nil
}
defstruct status: nil,
class: nil,
message: "",
request_id: nil,
raw: nil
@doc "Builds an `%Error{}` from an HTTP status code and raw response body."
@spec from_response(pos_integer(), binary()) :: t()
def from_response(status, body) when is_integer(status) do
base = %__MODULE__{status: status, message: status_text(status), raw: body}
try_parse_body(base, body)
end
@doc "Builds a network-level error (DNS, TLS, connect timeout, etc.)."
@spec network(term()) :: t()
def network(reason) do
%__MODULE__{status: :network, message: "Network error: #{inspect(reason)}"}
end
@doc "Builds a JSON decode error."
@spec decode(binary(), term()) :: t()
def decode(body, reason) do
%__MODULE__{status: :decode, message: "Decode error: #{inspect(reason)}", raw: body}
end
@doc "Returns `true` if the error is a 404."
@spec not_found?(t()) :: boolean()
def not_found?(%__MODULE__{status: 404}), do: true
def not_found?(_), do: false
@doc "Returns `true` if the error is a 429 rate-limit."
@spec rate_limited?(t()) :: boolean()
def rate_limited?(%__MODULE__{status: 429}), do: true
def rate_limited?(_), do: false
@doc "Returns `true` if the error is a 5xx server error."
@spec server_error?(t()) :: boolean()
def server_error?(%__MODULE__{status: s}) when is_integer(s) and s >= 500, do: true
def server_error?(_), do: false
@doc "Returns `true` if the error is retryable (network, 429, or 5xx)."
@spec retryable?(t()) :: boolean()
def retryable?(%__MODULE__{status: :network}), do: true
def retryable?(err), do: rate_limited?(err) or server_error?(err)
@doc "Returns `true` if the error has the given SaltEdge error class string."
@spec has_class?(t(), String.t()) :: boolean()
def has_class?(%__MODULE__{class: c}, class), do: c == class
# ── Private ─────────────────────────────────────────────────────────────────
# Guard: parse JSON body only when Jason is available (in the full Mix project).
# Falls back silently when running outside of a Mix env (e.g. bare elixirc tests).
defp try_parse_body(base, body) when is_binary(body) and byte_size(body) > 0 do
if Code.ensure_loaded?(Jason) do
case Jason.decode(body) do
{:ok, %{"error" => err}} ->
%{
base
| class: err["class"],
message: err["message"] || base.message,
request_id: err["request_id"]
}
_ ->
base
end
else
base
end
end
defp try_parse_body(base, _body), do: base
defp status_text(200), do: "OK"
defp status_text(201), do: "Created"
defp status_text(400), do: "Bad Request"
defp status_text(401), do: "Unauthorized"
defp status_text(403), do: "Forbidden"
defp status_text(404), do: "Not Found"
defp status_text(409), do: "Conflict"
defp status_text(422), do: "Unprocessable Entity"
defp status_text(429), do: "Too Many Requests"
defp status_text(500), do: "Internal Server Error"
defp status_text(502), do: "Bad Gateway"
defp status_text(503), do: "Service Unavailable"
defp status_text(n) when is_integer(n) and n >= 100, do: "HTTP #{n}"
defp status_text(_), do: "Unknown"
end