A file's own metadata (Storehouse.File's :metadata): a map the app
keeps with the file, in its record's column and in the JSON it sends,
of what a person entered (a caption, a credit, a description for
readers who cannot see it) and what the app decided.
It holds what JSON holds, so that it goes into a jsonb column and a
response as it is: string keys; strings (UTF-8, with no NUL byte, which
PostgreSQL's jsonb cannot keep), numbers, booleans and nil; lists
and maps of them. normalize/1 turns atoms (keys and values) into
strings, and dates and times into ISO 8601 strings; anything else is refused, with where it
is.
What analysis reads from the file itself is not metadata: it is the
file's :analysis (Storehouse.Analysis), so the two never overwrite
each other.
Summary
Functions
into with from merged in: maps merged key by key, all the way down;
any other value, nil too, replaced.
Metadata as Storehouse.File keeps it, or {:error, {:invalid_metadata, path}} with where the first value it refuses is. At most 8
levels deep, and 65536 bytes of JSON.
Types
@type path() :: [String.t() | non_neg_integer()]
Where a value that is refused is: the keys and indexes down to it.
Functions
into with from merged in: maps merged key by key, all the way down;
any other value, nil too, replaced.
Examples
iex> Storehouse.Metadata.merge(%{"credit" => %{"name" => "Ana", "year" => 2025}}, %{"credit" => %{"year" => 2026}})
%{"credit" => %{"name" => "Ana", "year" => 2026}}
Metadata as Storehouse.File keeps it, or {:error, {:invalid_metadata, path}} with where the first value it refuses is. At most 8
levels deep, and 65536 bytes of JSON.
Examples
iex> Storehouse.Metadata.normalize(%{caption: "The quay", rating: 4, taken: ~D[2026-10-01]})
{:ok, %{"caption" => "The quay", "rating" => 4, "taken" => "2026-10-01"}}
iex> Storehouse.Metadata.normalize(%{"gear" => %{"lens" => {35, :mm}}})
{:error, {:invalid_metadata, ["gear", "lens"]}}
iex> Storehouse.Metadata.normalize(%{"title" => "a\0b"})
{:error, {:invalid_metadata, ["title"]}}