# `GuardedStruct.AshResource`
[🔗](https://github.com/mishka-group/guarded_struct/blob/v0.1.4/lib/guarded_struct/ash_resource.ex#L1)

A Spark DSL extension that adds the GuardedStruct DSL to an Ash resource.

## Usage

    defmodule MyApp.User do
      use Ash.Resource,
        domain: MyApp.MyDomain,
        extensions: [GuardedStruct.AshResource]

      attributes do
        uuid_primary_key :id
        attribute :email, :string, allow_nil?: false, public?: true
      end

      # GuardedStruct DSL — the syntax of standalone `use GuardedStruct`, without
      # the `@derives` decorator (write `derives:` on the field).
      guardedstruct do
        field :email, :string,
          derives: "sanitize(trim, downcase) validate(string, not_empty, email_r)"

        field :nickname, :string,
          derives: "sanitize(strip_tags, trim) validate(string, max_len=20)"

        sub_field :preferences, :map do
          field :theme, :string, derives: "validate(enum=String[light::dark])"
        end
      end

      # Wire the change into Ash's changeset pipeline (Option A — manual).
      changes do
        change GuardedStruct.AshResource.Change
      end
    end

Now every `:create` and `:update` action runs the GuardedStruct pipeline
(sanitize → validate → derive → main_validator) before Ash hits the data
layer. Errors surface as standard `Ash.Changeset.add_error/2` errors.

## Two wiring modes

### Option A — manual (default)

Ship-and-forget: we provide `GuardedStruct.AshResource.Change`; you add
a one-line `changes do change ... end` block as shown above. Explicit and
inspectable — `Ash.Resource.Info.changes/1` will show the change.

### Option B — auto-wire

Set `auto_wire: true` on the section and the change is injected for you:

    guardedstruct auto_wire: true do
      field :email, :string, derives: "sanitize(trim) validate(email_r)"
    end

    # no `changes do ... end` block needed — the transformer added it

The same option can be a setter at the top of the block, the Spark form:

    guardedstruct do
      auto_wire true
      field :email, :string, derives: "sanitize(trim) validate(email_r)"
    end

`sub_field` and `conditional_field` take both forms as well:
`sub_field :profile, :map, authorized_fields: true do ... end`, or
`authorized_fields true` as the first line of its block.

Under the hood this calls `Ash.Resource.Builder.add_change/3` from a Spark
transformer that runs after our codegen. The result is identical to writing
the `changes do change ... end` block by hand — Ash's introspection sees
the change either way. `auto_wire` is `false` by default (no magic).

## What this extension does NOT do

* **It does not generate `defstruct`.** Ash already does that.
* **It does not generate `builder/2`.** Ash uses changesets.
* **It does not generate `Error` exception modules.** Ash has its own error
  classes (`Ash.Error.*`).

Instead, the extension adds a single function — `__guarded_change__/1` —
that takes a map of attrs and returns `{:ok, transformed_attrs}` or
`{:error, errors}`. The companion `GuardedStruct.AshResource.Change` module
wires it into the changeset; `GuardedStruct.AshResource.Info` provides
introspection.

## Why `__guarded_change__` (not `__guarded_validate__`)

Earlier drafts called the function `__guarded_validate__/1`. We renamed it
because the function does more than validate — sanitize ops transform
values (trim, downcase, slugify), `auto:` MFAs fill defaults, derives
cast types. "Change" matches Ash's own terminology and is honest about
the side-effect.

## Auto-map cascade

Every nested `sub_field` returns a plain map (not a struct) at every depth
when called through `__guarded_change__/1`. This is automatic and unique
to the Ash extension — standalone `use GuardedStruct` callers still get
structs from `builder/1`.

    MyResource.__guarded_change__(%{
      profile: %{address: %{geo: %{lat: 1.0, lng: 2.0}}}
    })
    # {:ok, %{profile: %{address: %{geo: %{lat: 1.0, lng: 2.0}}}}}
    #                              ^^^^ plain map, NOT a struct

This matches Ash's `:map` attribute type, so validated output drops
directly into `changeset.attributes` without conversion. Implementation
is a process-local flag — concurrency-safe (sibling processes don't see
it), re-entrancy-safe (saved+restored across nested calls), zero overhead
for standalone callers.

## Update actions — atomic-safe by default

`GuardedStruct.AshResource.Change` implements `atomic/3` and returns
`{:atomic, sanitized_map}` to Ash, so update / destroy actions stay
atomic without setting `require_atomic? false`. The pipeline
(sanitize / validate / derive / `auto:` MFAs / `Derive.Extension`)
runs in Elixir on the plain literal inputs, and the resulting UPDATE
is a single SQL statement.

The only case that falls back to imperative mode is when the caller
passes an `Ash.Expr` via `Ash.Changeset.atomic_update/3` — `atomic/3`
returns `{:not_atomic, reason}` since we can't sanitize a value we
won't see until the SQL evaluates.

## sub_field vs Ash relationships

`sub_field` inside an Ash resource creates an **embedded value type**, not
a related Ash resource. The generated submodule is a standalone
GuardedStruct (it has `defstruct`, `builder/1`, full GuardedStruct API)
but it is NOT an Ash resource (no actions, no changesets, no table). Use
`sub_field` for nested map shapes inside a single resource's attrs. For
separate tables and relationships, use Ash's own `relationships do
has_one :preferences, ... end`.

## Companion modules

* `GuardedStruct.AshResource.Change` — the `Ash.Resource.Change` module
  that bridges `__guarded_change__/1` into the changeset pipeline.
* `GuardedStruct.AshResource.Info` — runtime introspection for the
  `__guarded_*` namespace.

## Example: introspect a resource's guarded fields

    GuardedStruct.AshResource.Info.fields(MyApp.User)
    # => [:email, :nickname, :preferences]

# `guardedstruct`
*macro* 

---

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