# `Ramal.Storage.Codec`
[🔗](https://github.com/agustinrojas1/ramal/blob/v0.2.0/lib/ramal/storage/codec.ex#L1)

Turns a branch into a JSON-safe map and back, for storage backends that keep text (`Ramal.Storage.Postgres` keeps it
in a `jsonb` column).

`encode_state/1` covers what the table has no column for: overlay, base (fingerprints and snapshots), log, metadata
and the proposal. `decode_state/2` takes that map and the columns and gives the branch back, equal to the one encoded.

How values are written, confirmed against Ash 3.37 (`lib/ash/type/type.ex`):

  * An attribute value goes through `Ash.Type.dump_to_embedded/3` and comes back through
    `Ash.Type.cast_from_embedded/3`, so types with a custom stored form (atoms, embedded resources, `Ash.Type.Term`)
    round-trip as their type defines. Unloaded attributes (a `select` read) are listed under `"unloaded"`.
  * The other fields of a record (`__meta__`, `__metadata__`, loaded relationships, calculations, aggregates) are
    kept only when they differ from a fresh `struct(resource)`, as plain terms.
  * Everything else (primary keys, action input, actors, metadata, options) is a plain term: strings, numbers,
    booleans and `nil` are themselves; a list is a list; a map with only string keys that do not start with `$` is an
    object; and everything else is a one-key object that says what it is: `$a` atom, `$t` tuple, `$m` map as pairs,
    `$s` struct, `$b` base64 of a binary that is not valid UTF-8.
  * Atoms are read back with `String.to_existing_atom/1`, never created. Module names are found through the code
    path when the atom does not exist yet (a fresh VM has not loaded the host's modules), so a module that is not on
    the path is an error, not a new atom. Modules are loaded as they are named, before the values that follow them
    are read, which brings in the atoms they mention. An atom that no loaded module mentions (a metadata key that
    only a host controller uses, say, in a VM that has not loaded that controller) fails the load with
    `:storage` and names the atom; string keys never have this problem.

Pids, references, ports and functions cannot be stored: encoding returns `{:error, %Ramal.Error{reason: :storage}}`
naming the path of the offending value. The actor is stored as the term it is, with one exception: when it is an Ash
resource record, the attributes marked `sensitive?` (password hashes, tokens) are stored as `nil`, so a stored branch
never holds them (`docs/adr/0010-stored-actors.md`). The actor read back from storage therefore has those fields
empty; pass `actor:` to `Ramal.merge/2` to replace the whole actor. Fields that are not attributes, such as a loaded
relationship, are stored as they are.

## Example

    iex> branch = Ramal.Branch.new(Ramal.Test.Farm, name: "move R7")
    iex> {:ok, state} = Ramal.Storage.Codec.encode_state(branch)
    iex> columns = %{id: branch.id, domain: "Elixir.Ramal.Test.Farm", name: "move R7", status: "open"}
    iex> {:ok, ^branch} = Ramal.Storage.Codec.decode_state(columns, state)

# `decode_state`

```elixir
@spec decode_state(map(), map()) ::
  {:ok, Ramal.Branch.t()} | {:error, Ramal.Error.t()}
```

The branch for the stored `columns` (`id`, `domain` and `status` as stored, `name`) and the `state` map.

Returns `{:error, %Ramal.Error{reason: :storage}}` when the state was written by a newer version, names a module or
atom that is not available, or no longer matches a resource's attributes.

## Example

    iex> columns = %{id: "b1", domain: "Elixir.Ramal.Test.Farm", name: nil, status: "open"}
    iex> {:error, error} = Ramal.Storage.Codec.decode_state(columns, %{"version" => 99})
    iex> error.reason
    :storage

# `encode_state`

```elixir
@spec encode_state(Ramal.Branch.t()) :: {:ok, map()} | {:error, Ramal.Error.t()}
```

The JSON-safe state of `branch`: overlay, base, log, metadata and `inserted_at`.

## Example

    iex> {:ok, state} = Ramal.Storage.Codec.encode_state(Ramal.Branch.new(Ramal.Test.Farm))
    iex> {state["version"], state["overlay"], state["log"]}
    {1, [], []}

---

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