# `PhoenixKitCatalogue.Metadata`
[🔗](https://github.com/BeamLabEU/phoenix_kit_catalogue/blob/0.1.14/lib/phoenix_kit_catalogue/metadata.ex#L1)

Global, code-defined list of metadata fields that catalogue resources
(items and catalogues) can opt into.

Resources store chosen values in `resource.data["meta"]` as a flat map
keyed by the definition's `:key` — e.g. `%{"color" => "red"}`. Only
fields the user has explicitly added appear in that map.

The multilang layer owns the top-level `data` map (keys like `_name`,
`_primary_language`, per-language entries) — metadata lives strictly
under the `"meta"` sub-key so the two don't collide.

All fields are text inputs for now — typed inputs (decimal / enum /
etc.) can be reintroduced later by adding a `:type` field to the
definition shape and dispatching at render + cast time.

Edit `definitions/1` to add/remove fields. Removing a field from the
list does **not** wipe stored values — resources that already hold a
value for the removed key will surface it as "Legacy" in the form so
the data isn't lost; the user can clear it manually.

## Form helpers

Callers (LiveViews) drive the three-phase metadata flow through this
module's pure helpers:

- `build_state/2` turns a `resource.data` blob into `%{attached, values}`
  on mount (known keys first in definition order, legacy keys sorted).
- `absorb_params/2` folds the user's latest inputs from the form's
  `"meta"` submap into `state.values` on validate/save.
- `inject_into_data/3` casts values to storage shape and wedges them
  into `params["data"]["meta"]` right before hitting the context.

See `PhoenixKitCatalogue.Web.ItemFormLive` / `CatalogueFormLive` for
the reference wiring — one `meta_state` assign and three calls.

# `definition`

```elixir
@type definition() :: %{key: String.t(), label: String.t()}
```

# `resource_type`

```elixir
@type resource_type() :: :item | :catalogue
```

# `state`

```elixir
@type state() :: %{
  attached: [String.t()],
  values: %{required(String.t()) =&gt; String.t()}
}
```

# `absorb_params`

```elixir
@spec absorb_params(state(), map()) :: state()
```

Merges whatever the user currently has typed into the metadata inputs
(delivered as `params["meta"]`) into the state's `:values`. Unattached
keys are ignored so the render doesn't resurrect a row the user just
removed.

Returns the updated state; leaves the state untouched when the params
don't contain a `"meta"` submap.

# `build_state`

```elixir
@spec build_state(resource_type(), map() | struct() | nil) :: state()
```

Builds the initial meta state from a resource's `data` map.

Accepts either the raw `resource.data` (a map or `nil`) or a struct
with a `:data` field — both shapes surface in practice because new
structs default to `%{}` but a caller may pass `nil` if the field is
unset. Known keys (those in `definitions(resource_type)`) land first
in their declared order; legacy keys (stored but no longer defined)
come after, alphabetized, so the UI can flag them and offer a
remove-only action without dropping stored data.

Returns `%{attached: [key], values: %{key => stringified_value}}`.

## Examples

    iex> Metadata.build_state(:catalogue, %{"meta" => %{"brand" => "Acme"}})
    %{attached: ["brand"], values: %{"brand" => "Acme"}}

    iex> Metadata.build_state(:item, nil)
    %{attached: [], values: %{}}

# `cast_value`

```elixir
@spec cast_value(definition(), term()) :: String.t() | nil
```

Normalizes a raw form value for storage: trims whitespace, collapses
blanks to `nil` so callers can drop empty entries from the JSONB map.

# `definition`

```elixir
@spec definition(resource_type(), String.t()) :: definition() | nil
```

Fetches a single definition by key. Returns `nil` if the key isn't in `definitions/1`.

# `definitions`

```elixir
@spec definitions(resource_type()) :: [definition()]
```

The global list of metadata definitions for a given resource type.
Order here is the order used for the "Add metadata" dropdown.

The `:label` values are translated at call time via
`PhoenixKitWeb.Gettext` — do not cache the result across locale
changes. The `:key` is stable (it's the JSONB key) and never
translated.

# `inject_into_data`

```elixir
@spec inject_into_data(map(), state(), resource_type()) :: map()
```

Casts the state into its storage shape and wedges it into
`params["data"]["meta"]`. Known-key values go through `cast_value/2`
(blanks become `nil` → the key drops). Legacy keys (no current
definition) pass through untouched so their data isn't silently
nuked by a save — the user clears them explicitly via the × button.

---

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