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/3and comes back throughAsh.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 (aselectread) are listed under"unloaded". - The other fields of a record (
__meta__,__metadata__, loaded relationships, calculations, aggregates) are kept only when they differ from a freshstruct(resource), as plain terms. - Everything else (primary keys, action input, actors, metadata, options) is a plain term: strings, numbers,
booleans and
nilare 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:$aatom,$ttuple,$mmap as pairs,$sstruct,$bbase64 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:storageand 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)
Summary
Functions
The branch for the stored columns (id, domain and status as stored, name) and the state map.
The JSON-safe state of branch: overlay, base, log, metadata and inserted_at.
Functions
@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
@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, [], []}