# `Bourse.Unified.FieldMaps`
[🔗](https://github.com/ZenHive/bourse/blob/main/lib/bourse/unified/field_maps.ex#L1)

Derives unified struct field sets from authored `normalization.field_maps`.

The source of truth is the union of canonical unified keys authored per parse
type across the supported venues. The
hand-authored structs (`Bourse.Ticker`, `Bourse.Order`, ...) keep their curated
docs, typespecs, and helper functions, but their *field set* is governed by
the spec — `Bourse.Unified.FieldMapsTest` fails the build when a struct drifts
from the derived canonical set.

## Why governance, not full code generation

Most `field_maps` belong to the core `parse*` types; many derivative structs
have no authored field map to derive
from). A few Tier 2/3 types do carry one and are governed alongside the core
set (e.g. `Bourse.OpenInterest`), so `@struct_for` is the authoritative governed
list rather than a fixed count. And the core structs carry curated helpers
(`Bourse.Order` status predicates,
`Bourse.Balance.get/2`, `Bourse.OHLCV.from_list/1`) that no generator can emit.
So the spec governs the *field set* (drift-guarded by a test), not the full
module source.

## Honesty Rule

A canonical key appears in the derived set even when no exchange statically
resolved a coercion for it (`field_map[key] == null`) — it is part of Bourse's
unified shape and surfaces as a `nil`-valued struct field. Slots carrying a
non-nil `_unresolved_reason` (e.g. `multi_payload_branching:<N>`) still
contribute whatever keys they expose.

## Naming divergences

A small set of canonical keys map to a different struct field name by design
(mirrored in `Bourse.ResponseParser`). These are listed in `divergences/1` and
applied when computing the canonical field set for a struct.

Field names are surfaced as snake_case strings (not atoms): the derived set is
compared against struct keys at governance time, so deriving atoms from raw
spec strings would grow the atom table for no consumer benefit.

    iex> "ask" in Bourse.Unified.FieldMaps.canonical_fields("ticker")
    true

    iex> Bourse.Unified.FieldMaps.coercion_type("safeNumber")
    :number

# `canonical_field_sets`

```elixir
@spec canonical_field_sets() :: %{required(String.t()) =&gt; [String.t()]}
```

Derives the full canonical field set per parse type as `%{parse_type => [str]}`.

# `canonical_fields`

```elixir
@spec canonical_fields(String.t()) :: [String.t()]
```

Derives the canonical field set for a parse type as a sorted list of
snake_case strings.

The set is the union of `field_map` keys across all in-scope exchange specs,
converted to snake_case, with the parse type's `divergences/1` renames
applied. OHLCV carries no field map (array shape) and yields `[]`.

    iex> fields = Bourse.Unified.FieldMaps.canonical_fields("trade")
    iex> "order_id" in fields and "order" not in fields
    true

# `coercion_type`

```elixir
@spec coercion_type(String.t() | nil) ::
  :string | :integer | :number | :boolean | :unknown
```

Maps a coercion vocabulary token to its JSONSpec scalar type.

Returns `:unknown` for a `nil` coercion (an unresolved canonical key) or an
unrecognised token.

# `divergences`

```elixir
@spec divergences(String.t()) :: %{required(String.t()) =&gt; String.t()}
```

Returns the by-design canonical-key -> struct-field renames for a parse type.

# `divergences_for_struct`

```elixir
@spec divergences_for_struct(module() | nil) :: %{required(String.t()) =&gt; String.t()}
```

Returns the by-design canonical-key -> struct-field renames for a unified struct
module, or an empty map. Lets a verifier reconcile our struct field names
(`order_id`) against Bourse's canonical keys (`order`) from the same source of
truth as the runtime parser.

# `parse_types`

```elixir
@spec parse_types() :: [String.t()]
```

Returns the parse types (field_maps slots) that govern a unified struct.

# `struct_for`

```elixir
@spec struct_for(String.t()) :: module() | nil
```

Returns the unified struct module governed by a parse type, or `nil`.

---

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