Shared utilities for resource handlers.
Provides helper functions for:
- JSON responses
- Error handling
- ID generation
- Parameter extraction
- Expand parameter parsing
- Idempotency handling
Usage
defmodule PaperTiger.Resources.Customer do
import PaperTiger.Resource
def create(conn) do
with {:ok, params} <- validate_params(conn.params, [:email]),
customer <- build_customer(params),
{:ok, customer} <- Store.Customers.insert(customer) do
json_response(conn, 200, customer)
else
{:error, :invalid_params, field} ->
error_response(conn, PaperTiger.Error.invalid_request("Missing param", field))
end
end
end
Summary
Functions
Handles a standard delete request.
Sends an error response using a PaperTiger.Error struct.
Generates a Stripe-style ID with the given prefix, or uses a custom ID if provided.
Gets an integer param from a map, with default value.
Gets an optional integer param, returning nil if not present.
Sends a JSON response with the given status code and body.
Handles a standard list request with no filters beyond pagination.
Hydrates a resource according to the expand[] params of a request.
Stores a response for idempotency if an idempotency key is present.
Merges update parameters into an existing resource.
Sends the error for a required parameter that validate_params/2 found missing.
Normalizes a map by converting string values that look like integers to actual integers.
Gets a param by atom key, also accepting the string form of the key.
Parses expand[] parameters from request params.
Extracts pagination parameters from request params.
Sends the result of PaperTiger.Search.run/3 as an HTTP response.
Handles a standard retrieve request.
Converts a value to boolean.
Converts a value to integer.
Handles a standard update request.
Validates that required parameters are present.
Functions
@spec delete_response(Plug.Conn.t(), module(), String.t(), String.t()) :: Plug.Conn.t()
Handles a standard delete request.
Deletes id from store and sends Stripe's deletion confirmation for
object. Unknown IDs get the resource_missing 404.
@spec error_response(Plug.Conn.t(), PaperTiger.Error.t()) :: Plug.Conn.t()
Sends an error response using a PaperTiger.Error struct.
Generates a Stripe-style ID with the given prefix, or uses a custom ID if provided.
When custom_id is provided and starts with the expected prefix, it's used as-is.
This enables deterministic IDs for seeding and testing scenarios.
Examples
generate_id("cus") => "cus_1234567890abcdef"
generate_id("sub") => "sub_abcdef1234567890"
generate_id("cus", "cus_seed_user_1") => "cus_seed_user_1"
generate_id("cus", nil) => "cus_1234567890abcdef"
Gets an integer param from a map, with default value.
Handles form-encoded string values by coercing to integer.
Examples
get_integer(params, :amount) # => 0 if missing
get_integer(params, :amount, 100) # => 100 if missing
get_integer(%{amount: "500"}, :amount) # => 500
Gets an optional integer param, returning nil if not present.
Unlike get_integer/3 which defaults to 0, this returns nil when the key is not present, allowing callers to distinguish between "not provided" and "explicitly set to 0".
Examples
get_optional_integer(%{trial_end: 1234}, :trial_end) # => 1234
get_optional_integer(%{trial_end: "1234"}, :trial_end) # => 1234
get_optional_integer(%{}, :trial_end) # => nil
get_optional_integer(%{trial_end: 0}, :trial_end) # => 0
@spec json_response(Plug.Conn.t(), integer(), map() | struct()) :: Plug.Conn.t()
Sends a JSON response with the given status code and body.
@spec list_response(Plug.Conn.t(), module()) :: Plug.Conn.t()
Handles a standard list request with no filters beyond pagination.
Hydrates a resource according to the expand[] params of a request.
Resources with no expandable fields pass through unchanged.
@spec maybe_store_idempotency(Plug.Conn.t(), map()) :: :ok
Stores a response for idempotency if an idempotency key is present.
Merges update parameters into an existing resource.
Filters out nil values and immutable fields.
@spec missing_param_response(Plug.Conn.t(), atom() | String.t()) :: Plug.Conn.t()
Sends the error for a required parameter that validate_params/2 found missing.
Normalizes a map by converting string values that look like integers to actual integers.
Useful for handling form-encoded nested maps where integer values become strings.
Examples
normalize_integer_map(%{paid_at: "1724870574", voided_at: nil})
# => %{paid_at: 1724870574, voided_at: nil}
Gets a param by atom key, also accepting the string form of the key.
Nested form-encoded maps may arrive with string keys, so this checks both.
A missing key or a nil value yields default; false is a real value and
is returned as-is.
Examples
param(%{code: "X"}, :code) # => "X"
param(%{"code" => "X"}, :code) # => "X"
param(%{}, :code, "fallback") # => "fallback"
param(%{trial: false}, :trial, true) # => false
param(nil, :code) # => nil
Parses expand[] parameters from request params.
Examples
parse_expand_params(%{expand: ["customer", "subscription"]})
=> ["customer", "subscription"]
parse_expand_params(%{})
=> []
Extracts pagination parameters from request params.
Examples
parse_pagination_params(%{limit: "10", starting_after: "cus_123"})
=> %{limit: 10, starting_after: "cus_123"}
@spec respond_to_search({:ok, map()} | {:error, PaperTiger.Error.t()}, Plug.Conn.t()) :: Plug.Conn.t()
Sends the result of PaperTiger.Search.run/3 as an HTTP response.
@spec retrieve_response(Plug.Conn.t(), module(), String.t(), String.t()) :: Plug.Conn.t()
Handles a standard retrieve request.
Looks up id in store, hydrates the result according to the request's
expand[] params, and sends it. Unknown IDs get Stripe's resource_missing
404 for object.
Converts a value to boolean.
Examples
to_boolean(true) => true
to_boolean("true") => true
to_boolean("false") => false
to_boolean(nil) => false
Converts a value to integer.
Examples
to_integer(123) => 123
to_integer("123") => 123
to_integer(nil) => 0
to_integer("abc", 1) => 1
@spec update_response(Plug.Conn.t(), module(), String.t(), String.t(), [atom()]) :: Plug.Conn.t()
Handles a standard update request.
Merges the request params into the stored resource with merge_updates/3,
skipping immutable_fields, writes it back to store, and sends the
hydrated result. Unknown IDs get the resource_missing 404 for object.
Validates that required parameters are present.
Examples
validate_params(%{email: "test@example.com"}, [:email])
=> {:ok, %{email: "test@example.com"}}
validate_params(%{}, [:email])
=> {:error, :invalid_params, :email}