# `Plushie.Type`
[🔗](https://github.com/plushie-ui/plushie-elixir/blob/v0.7.2/lib/plushie/type.ex#L1)

Types for widget fields and events.

Widget field declarations like `field :name, :string` or
`field :color, Plushie.Type.Color` each name a type. The type
determines what values a field accepts, how they appear in
typespecs, and how they encode for the renderer.

The SDK includes types for common values; when your application
needs something more specific, you can define your own by
implementing the `Plushie.Type` behaviour.

## Primitive types

Types for basic values like numbers, strings, and booleans,
represented as atoms in field declarations:

  - `:integer` - `Plushie.Type.Integer`
  - `:float` - `Plushie.Type.Float`
  - `:string` - `Plushie.Type.String`
  - `:boolean` - `Plushie.Type.Boolean`
  - `:atom` - `Plushie.Type.Atom`
  - `:any` - `Plushie.Type.Any`
  - `:map` - `Plushie.Type.Map`

Example in a field declaration:

    field :label, :string
    field :count, :integer, default: 0

## Domain types

The SDK includes additional types beyond the primitives, referenced
by module name. Some examples:

  - `Plushie.Type.Color` - CSS colors (named atoms, hex strings, RGBA maps)
  - `Plushie.Type.Padding` - uniform number, `{v, h}` tuple, or per-side map
  - `Plushie.Type.Font` - font family, weight, style, and stretch
  - `Plushie.Type.Border` - color, width, and radius
  - `Plushie.Type.A11y` - accessibility annotations

Example in a field declaration:

    field :color, Plushie.Type.Color
    field :padding, Plushie.Type.Padding, default: 8

## Composite types

When you need a list of values, a set of specific atoms, or a
choice between types, you can express that directly in the field
declaration:

  - `{:enum, [:a, :b, :c]}` - one of a fixed set of atoms
  - `{:list, :string}` - list where every element matches the inner type
  - `{:map, {:string, :integer}}` - map with typed keys and values
  - `{:map, [name: :string, age: :integer]}` - map with specific named fields
  - `{:tuple, [:float, :float]}` - fixed-size tuple with typed positions
  - `{:union, [Plushie.Type.Color, Plushie.Type.StyleMap]}` - tries each type in order, first successful cast wins

Example in a field declaration:

    field :tags, {:list, :string}
    field :mode, {:enum, [:read, :write]}
    field :scores, {:map, {:string, :integer}}

## Building your own type

If the built-in types and composite types don't quite fit,
you can define your own. All custom types start with
`use Plushie.Type`. From there, you can either compose existing
types using the DSL macros, or implement callbacks directly for
more control.

### Composing with the DSL

The DSL macros let you build types from existing ones. All
callbacks are generated automatically.

**Enum** for a fixed set of atom values:

    defmodule MyApp.Type.Priority do
      use Plushie.Type
      enum [:low, :medium, :high, :critical]
    end

**Struct** for grouping related fields into a single value:

    defmodule MyApp.Type.Dimensions do
      use Plushie.Type

      struct do
        field :width, :float
        field :height, :float
      end
    end

**Union** when a field accepts multiple forms (first match wins):

    defmodule MyApp.Type.Size do
      use Plushie.Type

      union do
        enum [:small, :medium, :large]
        type MyApp.Type.Dimensions
      end
    end

Use them in widget field declarations by module name:

    field :priority, MyApp.Type.Priority, default: :medium
    field :size, MyApp.Type.Size

### Custom callbacks

When you need custom validation, coercion from multiple input
forms, or encoding logic that the DSL can't express, you can
implement the callbacks directly. You still start with
`use Plushie.Type`.

A type needs at least `cast/1` and `typespec/0`. Adding `guard/1`
enables pattern matching in generated setters, and `encode/1`
handles cases where the wire representation differs from the
Elixir value:

    defmodule MyApp.Type.Percentage do
      use Plushie.Type

      # Accept integers 0..100 or floats 0.0..1.0, normalize to float
      @impl Plushie.Type
      def cast(v) when is_integer(v) and v >= 0 and v <= 100, do: {:ok, v / 100}
      def cast(v) when is_float(v) and v >= 0.0 and v <= 1.0, do: {:ok, v}
      def cast(_), do: :error

      @impl Plushie.Type
      def typespec, do: quote(do: float())

      @impl Plushie.Type
      def guard(var), do: quote(do: is_float(unquote(var)))

      # encode/1 not needed: floats are already wire-safe
    end

You can also delegate to other types within your callbacks.
This is useful when your type wraps or combines existing types
with custom logic:

    def cast(%{fg: fg, bg: bg}) do
      with {:ok, fg} <- Plushie.Type.Color.cast(fg),
           {:ok, bg} <- Plushie.Type.Color.cast(bg) do
        {:ok, %{fg: fg, bg: bg}}
      else
        _ -> :error
      end
    end

`Plushie.Type.Integer` serves as a minimal reference.
`Plushie.Type.Color` shows how to handle many input forms.

## Callbacks

Required:

  - `cast/1` - validates input, returns `{:ok, normalized}` or
    `:error`. Defines what values are accepted and what canonical
    form they take.
  - `typespec/0` - returns a quoted typespec
    (e.g. `quote(do: float())`). Used in generated `@type` and
    `@spec` attributes.

Optional:

  - `castable/0` - returns a quoted typespec for the values
    `cast/1` accepts. Default: same as `typespec/0`. Implement
    when the input forms are broader than the canonical type
    (e.g., Color accepts atoms, strings, and maps but stores a
    hex string).
  - `decode/1` - decodes a wire-format value (JSON-decoded
    strings, numbers, maps with string keys) into the canonical
    type. Default: delegates to `cast/1`. Implement when wire
    and Elixir representations differ (e.g., enums receive
    strings but store atoms).
  - `encode/1` - converts to wire-safe form (atoms to strings,
    structs to maps). Only needed when the Elixir value isn't
    directly serializable.
  - `guard/1` - returns a quoted guard expression. Enables pattern
    matching in generated setter functions.
  - `fields/0` - for struct types, returns `[{name, type}, ...]`.
  - `field_options/0` - declares constraint keys (e.g.
    `[:min, :max]`). Validated at compile time.
  - `constrain_guard/2` - generates guards from field constraints
    (e.g. range checks from `:min`/`:max` options).
  - `merge/2` - controls how a field default combines with a user
    override. Default: full replacement. Implement for struct types
    where partial updates should preserve unset fields.
  - `resolve/2` - derives the final value from sibling widget props
    at render time. Default: identity. Implement when your type
    needs to read other props to compute its value.

# `cast`

```elixir
@callback cast(term()) :: {:ok, term()} | :error
```

# `castable`
*optional* 

```elixir
@callback castable() :: Macro.t()
```

Returns the quoted typespec for values accepted by `cast/1`.

For types that normalize input (e.g., Color accepts atoms, strings,
and maps but stores a hex string), this describes the broader input
surface. Widget setter `@spec` annotations use this so dialyzer
and documentation reflect what users can actually pass.

Defaults to `typespec/0` when not implemented, which is correct
for types where the input and canonical forms are the same.

# `constrain_guard`
*optional* 

```elixir
@callback constrain_guard(
  Macro.t(),
  keyword()
) :: [Macro.t()]
```

# `decode`
*optional* 

```elixir
@callback decode(term()) :: {:ok, term()} | :error
```

Decodes a wire-format value into the canonical type.

Wire data arrives as JSON-decoded values (strings, numbers,
booleans, lists, maps with string keys). This callback handles
coercion from those wire representations to the canonical Elixir
type.

Defaults to `cast/1` when not implemented, which is correct for
types where wire and Elixir representations are the same
(integers, floats, strings, booleans).

Types where wire and Elixir differ should implement this. For
example, enums receive strings from the wire but store atoms.

# `encode`
*optional* 

```elixir
@callback encode(term()) :: term()
```

# `field_options`
*optional* 

```elixir
@callback field_options() :: [atom()]
```

# `fields`
*optional* 

```elixir
@callback fields() :: [{atom(), term()}] | nil
```

# `guard`
*optional* 

```elixir
@callback guard(Macro.t()) :: Macro.t() | nil
```

# `merge`
*optional* 

```elixir
@callback merge(default :: term(), override :: term()) :: term()
```

Merges a default value with a user-provided override.

When a widget field has a default value and the user also provides a
value, this callback controls how the two combine. The default
implementation replaces the default entirely with the override, which
is correct for most types.

Struct types with many optional fields can implement field-level
merge instead. Consider a `%Config{theme: nil, locale: nil}` type.
A widget declares `default: %Config{theme: :dark}` and the user
sets `config: %Config{locale: :en}`. A field-level merge produces
`%Config{theme: :dark, locale: :en}` rather than discarding the
default theme.

Implement this callback when your type is a struct and partial
overrides should preserve unset defaults. See `Plushie.Type.A11y`
for a real-world example.

# `resolve`
*optional* 

```elixir
@callback resolve(value :: term(), props :: map()) :: term()
```

Resolves derived values after all widget props are set.

Called during tree normalization with the current field value and the
full props map of the widget. This lets a type compute its final
value based on sibling props that aren't known at declaration time.

For example, imagine a type with a `derive_from` field that names
another prop. A widget declares `field :tooltip, MyType, default:
%MyType{derive_from: :label}`. When `resolve/2` runs, it looks up
the `:label` prop from the props map and uses it as the tooltip
value. The `derive_from` directive is cleared since it only exists
to guide resolution, not to appear on the wire.

Most types don't need this. Implement it only when your type must
read other widget props to compute its final value. See
`Plushie.Type.A11y` for a real-world example.

# `typespec`

```elixir
@callback typespec() :: Macro.t()
```

# `cast_composite`

```elixir
@spec cast_composite(
  {atom(), term()},
  term()
) :: {:ok, term()} | :error
```

Casts a value according to a composite type constructor.

Dispatches to the appropriate `Plushie.Type.Composite` module
based on the composite kind.

# `cast_field`

```elixir
@spec cast_field(type :: term(), value :: term()) :: {:ok, term()} | :error
```

Casts a value according to a type identifier.

Resolves the type via `resolve/1` and delegates to the type module's
`cast/1`. The `:string` type accepts `nil` (wire-format absent fields).

Returns `{:ok, value}` on success, `:error` on failure.

# `cast_named_fields`

```elixir
@spec cast_named_fields([{atom(), term()}], map() | keyword()) ::
  {:ok, map()} | :error
```

Casts a map or keyword list against a list of `{name, type}` field specs.

Looks up each field by atom key first, then by string key as a
fallback. Casts through the field's type and returns `{:ok, map}`
or `:error`. Missing fields become nil.

# `cast_optional_field`

```elixir
@spec cast_optional_field(term(), term()) :: {:ok, term()} | :error
```

Casts a named field value, treating nil as a valid absent value.

Used by map record composites and struct type cast.

# `cast_value`

```elixir
@spec cast_value(term(), term()) :: {:ok, term()} | :error
```

Universal cast entry point that handles both module types and composite types.

Resolves the type via `resolve/1` and delegates to either
`cast_composite/2` or the type module's `cast/1`.

# `composite_kind?`

```elixir
@spec composite_kind?(atom()) :: boolean()
```

Returns true if the given atom is a known composite kind.

# `composite_module`

```elixir
@spec composite_module(atom()) :: module()
```

Returns the composite module for the given kind atom.

# `decode_field`

```elixir
@spec decode_field(type :: term(), value :: term()) :: {:ok, term()} | :error
```

Decodes a wire-format event field value.

Like `cast_field/2` but uses the type's `decode/1` callback,
which handles wire representations (strings for enums, lists for
tuples, etc.).

# `decode_named_fields`

```elixir
@spec decode_named_fields([{atom(), term()}], map()) :: {:ok, map()} | :error
```

Decodes a map against a list of `{name, type}` field specs using
the decode path. Like `cast_named_fields/2` but uses `decode_value`
for inner types (handling wire representations).

# `decode_optional_field`

```elixir
@spec decode_optional_field(term(), term()) :: {:ok, term()} | :error
```

Decodes a named field value from wire format, treating nil as absent.

# `decode_value`

```elixir
@spec decode_value(term(), term()) :: {:ok, term()} | :error
```

Decodes a wire-format value through the type's decode path.

Like `cast_value/2` but uses `decode/1` for module types and
`decode/2` for composite types.

# `encode_value`

```elixir
@spec encode_value(term()) :: term()
```

Encodes a value to its wire-safe representation.

Handles primitives (atoms become strings, tuples become lists) and
structs (delegates to `module.encode/1` when available, otherwise
strips `__struct__` and recursively encodes the map).

# `merge_value`

```elixir
@spec merge_value(module(), term(), term()) :: term()
```

Merges a default value with a user override using the type's merge semantics.

Falls back to simple replacement if the type module doesn't implement merge/2.

# `primitive_shortcuts`

```elixir
@spec primitive_shortcuts() :: %{required(atom()) =&gt; module()}
```

Returns the map of primitive atom shortcuts to their type modules.

# `resolve`

```elixir
@spec resolve(term()) :: module() | {:composite, term()}
```

Resolves a type reference to a module or composite descriptor.

Accepts primitive atom shortcuts (`:integer`, `:string`, etc.),
module names, and composite tuple forms (`{:enum, [...]}`,
`{:list, type}`, `{:map, spec}`, `{:tuple, [types]}`,
`{:union, [types]}`).

# `resolve_value`

```elixir
@spec resolve_value(module(), term(), map()) :: term()
```

Resolves derived values using the type's resolve semantics.

Falls back to identity if the type module doesn't implement resolve/2.

# `shortcut?`

```elixir
@spec shortcut?(atom()) :: boolean()
```

Returns true if the given atom is a known primitive type shortcut.

# `type_display_string`

```elixir
@spec type_display_string(term()) :: String.t()
```

Returns a human-readable type string for documentation.

# `valid_event_type?`

```elixir
@spec valid_event_type?(type :: term()) :: boolean()
```

Returns true if the given type identifier is valid for event fields.

Valid types are primitive shortcuts, modules implementing Plushie.Type
(with `cast/1`), or composite type tuples.

---

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