# `DripDrop.Helpers`
[🔗](https://github.com/agoodway/dripdrop/blob/v0.1.0/lib/dripdrop/helpers.ex#L1)

Shared helpers for data shaping, module resolution, and small parsing tasks.

Domain modules should use these helpers when the logic is generic enough to
apply across dispatch, hooks, templates, and policies.

# `atom_or_string`

```elixir
@spec atom_or_string(atom() | binary()) :: atom() | binary()
```

Returns the existing atom for a binary, falling back to the original binary
when the atom is unknown. Used to normalize map keys from external input
without growing the global atom table.

# `atomize_existing_keys_strict`

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

Atomizes string keys in a map using `String.to_existing_atom/1`. Falls back
to returning the **original map unchanged** when *any* key is unknown — this
preserves all-or-nothing semantics so callers downstream can rely on a
single key shape (atom-only or string-only) rather than a mixed map.

# `email_address`

```elixir
@spec email_address(term()) :: binary() | nil
```

Extracts and lowercases the first email address in a value.

# `email_domain`

```elixir
@spec email_domain(term()) :: binary() | nil
```

Extracts and lowercases the domain of the first email address in a value.

# `existing_atom`

```elixir
@spec existing_atom(atom() | binary() | nil, term()) ::
  {:ok, atom()} | {:error, term()}
```

Converts an atom or existing atom string into an atom.

# `fetch_string_or_atom_key`

```elixir
@spec fetch_string_or_atom_key(map() | nil, atom() | binary(), term()) :: term()
```

Fetches `key` from `map`, transparently accepting either a string or atom
key. Looks up the literal key first; on miss, tries `atom_or_string/1` to
resolve a matching atom key without growing the atom table. Returns
`default` when neither shape is present.

Use anywhere config / credential / payload maps may arrive with string OR
atom keys, instead of writing `Map.get(map, key) || Map.get(map, String.to_atom(key))`
(which is unsafe — `String.to_atom` grows the atom table).

# `get_path`

```elixir
@spec get_path(term(), binary() | nil) :: term()
```

Reads a dotted path from a map with string or existing-atom keys.

# `http_method`

```elixir
@spec http_method(atom() | binary() | nil, atom()) ::
  :get | :post | :put | :patch | :delete
```

Same as `http_method!/1`, but returns `default` for unknown input instead
of raising. Use this for untrusted input (request payloads, user maps).

# `http_method!`

```elixir
@spec http_method!(atom() | binary()) :: :get | :post | :put | :patch | :delete
```

Coerces an HTTP method into the lowercase atom Req and Plug expect.
Raises when the value is not one of `GET / POST / PUT / PATCH / DELETE`.

Use this for trusted input (e.g. enum-validated DB columns) where any
failure represents a real invariant break.

# `module_from_string`

```elixir
@spec module_from_string(module() | binary() | nil, term(), term()) ::
  {:ok, module()} | {:error, term()}
```

Resolves a module from an atom or existing Elixir module name.

# `recipient_domain`

```elixir
@spec recipient_domain(term()) :: binary() | nil
```

Extracts the recipient domain from a value. Accepts either a payload-like
map with `to`/`recipient` keys (string or atom) or a plain email-like
string. Returns the lowercased domain part, or `nil` when no email-shaped
value is present.

Mirrors the sender-side extraction (`email_domain/1` invoked against a
`from`/`reply_to` field) for the recipient side, used by the
per-recipient-domain rate-limit scope to bucket sends by recipient ISP.

# `scheduled_for`

```elixir
@spec scheduled_for(Ecto.Schema.t()) :: DateTime.t()
```

Calculates the next scheduled timestamp for a timing struct.

# `slugify_key`

```elixir
@spec slugify_key(atom() | binary() | nil) :: atom() | binary() | nil
```

Normalizes a string key (trim, downcase, replace `-` with `_`). Atoms pass
through unchanged. Returns `nil` for `nil`. Used for channel/provider keys
and other slug-style identifiers.

# `stringify_keys`

```elixir
@spec stringify_keys(term()) :: term()
```

Recursively converts map keys to strings.

---

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