Dextrin.Schema.TypeExpr (Dextrin v0.3.2)

Copy Markdown View Source

Internal, compiled representation of a .dxns type expression — the vocabulary of DXNS.md §1 (any, never, primitive, reference, list-of, list-items, set-of, tuple-of, tuple-of-all, map-of, array-of, tagged, doc, if, ordered-map-of, sorted-set-of, enum, one-of, any-of, all-of, not, nilable, refine, struct, and shape and case, design/shapes-and-case.md) Dextrin.Schema.Compiler turns a parsed .dxns value into, and what matches?/2 below checks a decoded value against. This vocabulary is fixed, not extensible from outside an actual library change — what schema authors extend instead is composing these forms into a reusable named type (see Dextrin.Schema.Compiler's own moduledoc), which needs no new form at all.

Summary

Types

What a check knows besides the type and the value: the registry (nil when there is none -- strict and trusted are then off), and the fields of the nearest enclosing struct or shape by name, which a {:case} reads its sibling from (nil outside any).

One of a {:list-items}'s items: a type for exactly one item, or a repeat of a group of them, most nil when there is no upper bound (DXNS.md §1a).

t()

Functions

value checked against type in context: {:ok, value} with every shape's missing defaults filled in (design/shapes-and-case.md, Jan, 2026-10-20), or :error. Nothing but a shape changes a value.

The context for a check with registry, the fields siblings around it.

Whether value matches type — shared by both decode (Dextrin.Schema.Reader.read/5) and encode-time validation (Dextrin.Schema.Validator.validate_for_encode/3): the same recursive check, the same function, for both directions. It is conform/3 answered as a boolean, so there is one check, not two.

Types

context()

@type context() :: %{
  registry: Dextrin.Registry.t() | nil,
  siblings: %{required(String.t()) => term()} | nil
}

What a check knows besides the type and the value: the registry (nil when there is none -- strict and trusted are then off), and the fields of the nearest enclosing struct or shape by name, which a {:case} reads its sibling from (nil outside any).

particle()

@type particle() ::
  {:one, t()}
  | {:repeat, [particle()], non_neg_integer(), non_neg_integer() | nil}

One of a {:list-items}'s items: a type for exactly one item, or a repeat of a group of them, most nil when there is no upper bound (DXNS.md §1a).

t()

@type t() ::
  :any
  | :never
  | {:primitive, String.t()}
  | {:reference, String.t()}
  | {:list_of, t()}
  | {:set_of, t()}
  | {:list_items, [particle()]}
  | {:tuple_of, [t()]}
  | {:tuple_of_all, t()}
  | {:tagged, String.t(), t()}
  | {:doc, t(), %{optional(String.t()) => term()}}
  | {:if, t(), t(), t()}
  | {:map_of, t(), t()}
  | {:array_of, t()}
  | {:ordered_map_of, t(), t()}
  | {:sorted_set_of, t()}
  | {:enum, [term()]}
  | {:one_of, [t()]}
  | {:any_of, [t()]}
  | {:all_of, [t()]}
  | {:not, t()}
  | {:shape, String.t(), [Dextrin.Schema.Field.t()]}
  | {:case, String.t(), [{term(), t()}], t() | :none}
  | {:named, String.t()}
  | {:generic, [String.t()], t()}
  | {:apply, String.t(), [t()]}
  | {:param, String.t()}
  | {:nilable, t()}
  | {:refine, t(), %{optional(String.t()) => term()}}

Functions

conform(type, value, context)

@spec conform(t(), term(), context()) :: {:ok, term()} | :error

value checked against type in context: {:ok, value} with every shape's missing defaults filled in (design/shapes-and-case.md, Jan, 2026-10-20), or :error. Nothing but a shape changes a value.

context(registry, siblings \\ nil)

@spec context(Dextrin.Registry.t() | nil, %{required(String.t()) => term()} | nil) ::
  context()

The context for a check with registry, the fields siblings around it.

matches?(type, value, registry \\ nil)

@spec matches?(t(), term(), Dextrin.Registry.t() | nil) :: boolean()

Whether value matches type — shared by both decode (Dextrin.Schema.Reader.read/5) and encode-time validation (Dextrin.Schema.Validator.validate_for_encode/3): the same recursive check, the same function, for both directions. It is conform/3 answered as a boolean, so there is one check, not two.

{:reference, name} checks the originating schema's name, not the value's own shape — which is how it correctly rejects a materialized value that satisfies its own schema but is the wrong one for this field. Three sources carry that name: an opaque, unregistered Dextrin.Struct carries its own wire name directly; a value that went through a registered schema (decode or encode -time validation alike) carries it via Validated — Dextrin.Schema.Reader.read/5 wraps every result in one regardless of materializer shape, and encode-time validation wraps every recognized struct the same way before checking it (Dextrin.Schema.Validator's internal wrap_and_check helper), specifically so this one function can check both without needing two implementations. The third, encode-only source: registry (nil for decode, which never needs it) lets a real, unwrapped application struct's __struct__ be checked against whatever module was registered for name (Dextrin.Registry.put_struct_module/3) — needed only for a struct that turned out to have no schema of its own to be wrapped by. Nothing else matches a reference (DXN.md §4.7): a plain map, however much it looks like the struct, and an application struct registered for no schema or for another are type errors -- a value matches only its declared type.