# `DpExchange.Core.Types.Validate`
[🔗](https://github.com/DistortionPoint/dp-exchange-core/blob/v0.3.76/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 not type-checked
for being a `Decimal` (an integer is accepted, and a venue builds these with integers in its
own tests), but 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.

## 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.

# `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`.

# `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_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.

---

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