# `DpExchange.Core.Types.Validate`
[🔗](https://github.com/DistortionPoint/dp-exchange-core/blob/v0.3.120/lib/dp_exchange/core/types/validate.ex#L1)

The validating constructor every `Types.*` struct builds `new/1` on.

## `@enforce_keys` guards presence, not `nil`

`@enforce_keys [:open, :high, :low, :close, ...]` makes `struct!(Candle, attrs)` raise
when a key is **absent** from `attrs`. It does nothing when the key is **present and
`nil`**: `struct!(Candle, open: nil, high: ..., ...)` builds without complaint, even
though `Candle`'s own typespec declares `open: Decimal.t()` — never `Decimal.t() | nil`.

That gap is not academic. A `nil` in a field the typespec calls non-nilable is exactly
what a JSON decode bug produces — `Map.get(json, "open")` on a key the venue renamed
returns `nil`, silently, and every value downstream stays plausible until the struct
reaches `Decimal.compare/2` or similar, several calls away from where the bad data
actually entered. The error message at that point names a `Decimal` function, not the
venue response that caused it.

## What `new!/3` adds

`struct!/2`, plus a check that every field named in `required_non_nil` is present AND
non-nil, raising `ArgumentError` naming the offending field when it is not. A caller gets
a message pointing at the exact field and the venue's own module, at the moment the
struct was about to be built — not a `FunctionClauseError` inside `Decimal` three frames
later with no indication which field was the problem.

## What this deliberately does not do

**No coercion, no defaulting, no guessing.** A `nil` where the type forbids one is an
error, full stop — the same rule this whole family applies everywhere else. `new!/3`
fails closed; it does not repair the input.

**Two further values are refused in any field**, however deeply nested in a list, tuple
or plain map: a `Decimal` that is `NaN` or infinite, which is not a number any field of
these types can carry, and a `NaiveDateTime`, because every time in this family is a
`DateTime` and a time with no zone cannot be placed.

**Closed vocabularies are checked per type, not here.** `new!/3` knows nothing about which
field is a `:buy | :sell`. A type whose typespec states one pipes its result through
`one_of!/3` (and `positive_integer!/2` for a count), so `Fill`, `Trade`, `Position` and
`OrderLeg` refuse a side outside what they state. The `Decimal` fields are type-checked
through `decimals!/2` (and `decimal_map_values!/2` for maps of Decimals): a type pipes its
result through them for every field its typespec states as `Decimal.t()`, and a string, a
map or any other non-number is an `ArgumentError` naming module, field and value (an
integer is accepted, and a venue builds these with integers in its own tests). A **float**
is refused at the top level of any field and inside a list or
tuple, because no field of any type is a float and a float has already lost digits. Free-form
maps (`details`, `line_items`, `metrics`) may still hold one: they are the venue's own.

**Value rules a type's own doc states are held per type too**, by `positive!/2` (a traded
price, a resting bid or ask, a position or withdrawal size, a strike), `non_negative!/2`,
`text!/2` (an address, network, asset or id is a non-blank string), `maps!/2`, `dates!/2`,
`datetimes!/2` (a time field `new!/3` cannot recognise by name), `symbol_lists!/2` (a
container of the right shape or `nil`), `integers!/2` (a sequence number) and
`timeframe!/2` (a candle width in `Timeframe.nameable/0`). Each names module, field and
value, accepts `nil` (presence is `new!/3`'s job; `timeframe!/2` is the one exception, and
its field is enforced non-nil) and never repairs the input.

## Struct literals still work

Nothing here removes `defstruct` or `@enforce_keys` from any `Types.*` module, and
`%Candle{...}` remains a valid, unchecked way to build one — internal code and tests that
already construct known-good values by hand are unaffected. `new/1` (built on this
module, one per type) is the path a venue package's *decoder* should prefer, because it
is the one that turns a decode bug into an error at the boundary instead of a crash three
calls downstream.

# `dates!`

```elixir
@spec dates!(struct(), [atom()]) :: struct()
```

Returns `built` if each of `fields` is `nil` or a `Date`, and raises `ArgumentError`
otherwise. A `DateTime` or a string there sorts and compares wrongly in the consumer.

# `datetimes!`

```elixir
@spec datetimes!(struct(), [atom()]) :: struct()
```

Returns `built` if each of `fields` is `nil` or a `DateTime`, and raises `ArgumentError`
naming module, field and value otherwise.

For a time whose field name `new!/3` cannot recognise as one. `new!/3` holds `:venue_time`,
`:timestamp`, `:as_of`, `:active_from` and every `*_at` to this rule by name; a field typed
`DateTime.t()` outside that vocabulary (`StakingReward`'s `:period_start` and `:period_end`,
which cannot join the global rule because a filing's same-named fields are `Date`s) is named
here by its type instead. A `Date`, a string or an epoch integer there built, and then
crashed `DateTime.compare/2` in the consumer. A `NaiveDateTime` is refused by `new!/3` first.

# `decimal!`

```elixir
@spec decimal!(module(), atom(), term()) :: :ok
```

Raises `ArgumentError` naming `module`, `field` and `value` unless `value` is a `Decimal`
or an integer; returns `:ok` otherwise.

The element-level check for a type that walks a nested structure itself (order book
levels, option strikes). `nil` is NOT accepted here.

# `decimal_map_values!`

```elixir
@spec decimal_map_values!(struct(), [atom()]) :: struct()
```

Returns `built` if each of `fields` is `nil` or a map whose values are all `Decimal`s or
integers, and raises `ArgumentError` naming module, field and value otherwise.

For `%{String.t() => Decimal.t()}` fields (`VolumeProfile.buy_at_price`,
`StakingBalance.by_provider`). A field holding something other than a map is refused.

# `decimals!`

```elixir
@spec decimals!(struct(), [atom()]) :: struct()
```

Returns `built` if every one of `fields` holds a `Decimal`, an integer or `nil`, and
raises `ArgumentError` naming module, field and value otherwise.

For the fields a type's typespec states as `Decimal.t()` (or `Decimal.t() | nil`). A
`"1.5"`, `"abc"` or a map in one builds fine under `new!/3` and raises inside
`Decimal.compare/2` in the CONSUMER's process, so a decoder that forgot to parse a number
is refused here, at the boundary. An integer is accepted (venues build with integers in
their own tests). `nil` passes this check because presence is `new!/3`'s job: a field
whose typespec forbids `nil` is already in `required_non_nil`, and a field whose typespec
allows it must not be refused here. Floats, NaN, infinities and absurd exponents are
refused by `new!/3` itself.

# `integers!`

```elixir
@spec integers!(struct(), [atom()]) :: struct()
```

Returns `built` if each of `fields` is `nil` or an integer, and raises `ArgumentError`
naming module, field and value otherwise.

For a venue's sequence number (`OrderBook.sequence`, `OrderBookDelta.sequence`). A `"7"`
there built, and a consumer comparing it with `>` against the integer it held last got
Erlang term ordering (a binary sorts above every integer), so a stale frame read as the
newest one and no error said why. A float is refused by `new!/3` already; a `Decimal` is
refused here.

# `maps!`

```elixir
@spec maps!(struct(), [atom()]) :: struct()
```

Returns `built` if each of `fields` is `nil` or a plain map (not a struct), and raises
`ArgumentError` otherwise. For the free-form maps (`details`, `line_items`, `metrics`) whose
contents are the venue's own but whose container is not.

# `max_decimal_exponent`

```elixir
@spec max_decimal_exponent() :: pos_integer()
```

The widest exponent a `Decimal` may carry in any field (`+/-1000`). Exposed so a venue
decoder's own `decimal/1` can apply the same bound before building a value.

# `new!`

```elixir
@spec new!(module(), [atom()], keyword() | map()) :: struct()
```

Builds `module`'s struct from `attrs`, raising `ArgumentError` naming the field if any of
`required_non_nil` is absent or explicitly `nil`.

`required_non_nil` is ordinarily the type's own `@enforce_keys` — the fields whose
typespec admits no `nil`. A type that deliberately allows `nil` on some enforced keys
(see `DpExchange.Core.Types.Order`, which enforces presence of seven keys but documents
that six of them may honestly be `nil`) passes a narrower list naming only the fields
that must never be `nil`.

# `non_negative!`

```elixir
@spec non_negative!(struct(), [atom()]) :: struct()
```

Returns `built` if each of `fields` is `nil` or a number that is not negative, and raises
`ArgumentError` naming module, field and value otherwise.

For a size, quantity or volume: a negative one is never what a venue traded or quoted, it
is a decode bug (a sign flipped, a delta read as a level), and every value around it stays
plausible. Zero is allowed here, because a zero size is a real answer (an empty side, a
flat interval); a type that cannot carry zero says so itself. Run it after `decimals!/2`,
which guarantees the fields hold a `Decimal`, an integer or `nil`.

# `one_of!`

```elixir
@spec one_of!(struct(), atom(), [term()]) :: struct()
```

Returns `built` if its `field` holds one of `allowed`, and raises `ArgumentError` naming
the field and the value otherwise.

For a closed vocabulary a type's own typespec states — `side: :buy | :sell`,
`liquidity: :maker | :taker | nil`. `new!/3` refuses a `nil` and a NaN; it cannot refuse
`side: :long` on a `Fill`, `side: "buy"` on a `Trade`, or `:buy` on a `Position`, and each
of those is a plausible value with the wrong meaning: a position built `:buy` is read by
a consumer matching `:long | :short` as no side at all, or as the wrong one. Include `nil`
in `allowed` only where the type's typespec does.

# `positive!`

```elixir
@spec positive!(struct(), [atom()]) :: struct()
```

Returns `built` if each of `fields` is `nil` or a number above zero, and raises
`ArgumentError` naming module, field and value otherwise.

For a traded price, a resting bid or ask, and a position's size: zero is not a price (a
venue's `0` means "no resting order", which its decoder turns into `nil`) and not a size
that is held. Run it after `decimals!/2`, which guarantees a `Decimal`, an integer or `nil`.

# `positive_integer!`

```elixir
@spec positive_integer!(struct(), atom()) :: struct()
```

Returns `built` if its `field` holds a positive integer, and raises `ArgumentError`
naming the field and the value otherwise.

For a count or a ratio whose typespec is `pos_integer()`. A `Decimal` `1`, a float `1.0`
and a `0` all read as "one leg" or "no legs" to whoever gets there first.

# `symbol_lists!`

```elixir
@spec symbol_lists!(struct(), [atom()]) :: struct()
```

Returns `built` if each of `fields` is `nil` or a proper list of non-blank strings, and
raises `ArgumentError` otherwise. `nil` and `[]` stay different answers ("not fetched" and
"fetched, empty"); anything else, a bare string included, is neither.

# `text!`

```elixir
@spec text!(struct(), [atom()]) :: struct()
```

Returns `built` if each of `fields` is `nil` or a non-blank string, and raises
`ArgumentError` naming module, field and value otherwise.

For the identifying text of a funds-moving or addressing field (an address, a network, an
asset, an id). A blank string is no name at all and an atom or number is not the venue's
text; both used to build, and an address with an empty network is the one the deposit is
lost to.

# `timeframe!`

```elixir
@spec timeframe!(struct(), atom()) :: struct()
```

Returns `built` if its `field` holds a label `DpExchange.Core.Timeframe.nameable?/1`
recognises, and raises `ArgumentError` naming module, field and value otherwise.

`Timeframe.aligned?/2` answers `true` for a width it has no rule for, so a bar labelled
`"1H"` or `"60m"` passed the authenticity check unexamined, and was stored under a label
nothing else reads. The label is the contract's vocabulary or it is refused. A value that
is not text at all is refused the same way (`nameable?/1` answers `false` for it).

---

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