# `SafeAtom`
[🔗](https://github.com/ivan-podgurskiy/safe_atom/blob/v0.1.0/lib/safe_atom.ex#L1)

Safe, whitelist-based casting of values to atoms.

`SafeAtom` returns only atoms that are explicitly present in the `:allowed`
list.

Binary input is never converted with `String.to_atom/1` or
`String.to_existing_atom/1`. Instead, binary values are compared with the
string representation of atoms already present in `:allowed`.

This avoids creating new atoms from external input and avoids querying the VM
atom table for arbitrary binary values.

## Examples

    iex> SafeAtom.cast("user", allowed: [:user, :guest])
    {:ok, :user}

    iex> SafeAtom.cast(:user, allowed: [:user, :guest])
    {:ok, :user}

    iex> SafeAtom.cast("admin", allowed: [:user, :guest])
    {:error, :not_allowed}

    iex> SafeAtom.cast(:admin, allowed: [:user, :guest])
    {:error, :not_allowed}

    iex> SafeAtom.cast("anything", allowed: [])
    {:error, :not_allowed}

    iex> SafeAtom.cast("user", [])
    {:error, :missing_allowed}

    iex> SafeAtom.cast("user", allowed: ["user"])
    {:error, :invalid_allowed}

    iex> SafeAtom.cast(123, allowed: [:user])
    {:error, :invalid_value}

    iex> SafeAtom.cast(nil, allowed: [:user])
    {:error, :not_allowed}

    iex> SafeAtom.cast(nil, allowed: [nil])
    {:ok, nil}

## Error reasons

  * `:missing_allowed` - the `:allowed` option was not provided.
  * `:invalid_allowed` - `:allowed` is not a list of atoms.
  * `:invalid_value` - the input value is not a binary or an atom.
  * `:not_allowed` - the input value is valid, but does not match any allowed atom.

## Telemetry events

`SafeAtom` emits the following [Telemetry](https://hexdocs.pm/telemetry/) events:

### `[:safe_atom, :cast, :rejected]`

Emitted whenever `cast/2` returns `{:error, reason}`.

* **measurements**:
  * `:system_time` - `System.system_time/0` at the moment of rejection
* **metadata**:
  * `:reason` - one of `:missing_allowed`, `:invalid_allowed`, `:invalid_value`, or `:not_allowed`
  * `:value` - the input value passed to `cast/2`
  * `:allowed` - the `:allowed` option value, or `nil` when the option is missing

# `reason`

```elixir
@type reason() :: :missing_allowed | :invalid_allowed | :invalid_value | :not_allowed
```

# `cast`

```elixir
@spec cast(
  term(),
  keyword()
) :: {:ok, atom()} | {:error, reason()}
@spec cast(term(), term()) :: {:error, :missing_allowed}
```

Casts a binary or atom to one of the explicitly allowed atoms.

The `:allowed` option is required and must be a list of atoms.

For binary input, `SafeAtom` compares the input with `Atom.to_string/1` for
each allowed atom. The returned atom is always taken from the `:allowed` list.

Returns `{:error, :missing_allowed}` when called with options that do not
contain `:allowed`.

## Examples

    iex> SafeAtom.cast("user", allowed: [:user, :guest])
    {:ok, :user}

    iex> SafeAtom.cast(:guest, allowed: [:user, :guest])
    {:ok, :guest}

    iex> SafeAtom.cast("admin", allowed: [:user, :guest])
    {:error, :not_allowed}

    iex> SafeAtom.cast(:admin, allowed: [:user, :guest])
    {:error, :not_allowed}

    iex> SafeAtom.cast("user", allowed: [])
    {:error, :not_allowed}

    iex> SafeAtom.cast("user", [])
    {:error, :missing_allowed}

    iex> SafeAtom.cast("user", allowed: :user)
    {:error, :invalid_allowed}

    iex> SafeAtom.cast("user", allowed: [:user, "guest"])
    {:error, :invalid_allowed}

    iex> SafeAtom.cast(123, allowed: [:user])
    {:error, :invalid_value}

    iex> SafeAtom.cast(nil, allowed: [nil])
    {:ok, nil}

# `cast!`

```elixir
@spec cast!(
  term(),
  keyword()
) :: atom()
```

---

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