# `PaperTiger.Resource`
[🔗](https://github.com/EnaiaInc/paper_tiger/blob/v1.2.2/lib/paper_tiger/resource.ex#L1)

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

# `error_response`

```elixir
@spec error_response(Plug.Conn.t(), PaperTiger.Error.t()) :: Plug.Conn.t()
```

Sends an error response using a PaperTiger.Error struct.

# `generate_id`

```elixir
@spec generate_id(String.t(), String.t() | nil) :: String.t()
```

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"

# `get_integer`

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

# `get_optional_integer`

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

# `json_response`

```elixir
@spec json_response(Plug.Conn.t(), integer(), map() | struct()) :: Plug.Conn.t()
```

Sends a JSON response with the given status code and body.

# `maybe_store_idempotency`

```elixir
@spec maybe_store_idempotency(Plug.Conn.t(), map()) :: :ok
```

Stores a response for idempotency if an idempotency key is present.

# `merge_updates`

```elixir
@spec merge_updates(map(), map(), [atom()]) :: map()
```

Merges update parameters into an existing resource.

Filters out nil values and immutable fields.

# `normalize_integer_map`

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}

# `parse_expand_params`

```elixir
@spec parse_expand_params(map()) :: [String.t()]
```

Parses expand[] parameters from request params.

## Examples

    parse_expand_params(%{expand: ["customer", "subscription"]})
    => ["customer", "subscription"]

    parse_expand_params(%{})
    => []

# `parse_pagination_params`

```elixir
@spec parse_pagination_params(map()) :: map()
```

Extracts pagination parameters from request params.

## Examples

    parse_pagination_params(%{limit: "10", starting_after: "cus_123"})
    => %{limit: 10, starting_after: "cus_123"}

# `to_boolean`

Converts a value to boolean.

## Examples

    to_boolean(true) => true
    to_boolean("true") => true
    to_boolean("false") => false
    to_boolean(nil) => false

# `to_integer`

Converts a value to integer.

## Examples

    to_integer(123) => 123
    to_integer("123") => 123
    to_integer(nil) => 0

# `validate_params`

```elixir
@spec validate_params(map(), [atom()]) ::
  {:ok, map()} | {:error, :invalid_params, atom()}
```

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}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
