Enact.Preview (Enact v0.1.0)

Copy Markdown View Source

The result of Enact.dry_run/3 — a distinct struct so callers are structurally unable to confuse "validated" with "executed".

  • updatesEnact.updates/2 output: the canonical, post- normalization "what will be persisted" map. What the user confirms is definitionally what executes. Patch previews carry only the provided keys; host apps render old → new diffs by comparing against subject.
  • subject — the record load_subject/2 returned, or nil. Compare updates against it for old → new diffs. Not part of the digest.
  • resolved — names of resolvers that succeeded. Never the loaded structs — those stay in ctx.assigns and never reach the caller.
  • digest — canonical hash binding the action, mode, locator params (params keys not in the input schema), and updates map. Pass it back as confirm_digest: to Enact.run/3, which recomputes post-validation and returns :conflict on mismatch.

A preview is not a promise: no reservation semantics. The confirming run/3 re-executes the full pipeline, and races surface as :invalid/:conflict normally.

Summary

Functions

Canonically digests a pending change: "sha256:..." over the action module, mode, locator params, and updates map together. Locator params are params keys not in the input schema (fields/1) — typically the URL-anchored subject id. Folding action, mode, and locators in makes "the user confirmed this exact change to this record" total — a digest minted for one action, mode, or subject never confirms another, and input-less actions (whose updates are always %{}) don't collapse across records.

Types

t()

@type t() :: %Enact.Preview{
  action: module(),
  digest: String.t(),
  mode: :create | :patch,
  resolved: [atom()],
  subject: struct() | nil,
  updates: map()
}

Functions

digest(action, mode, locators, updates)

@spec digest(module(), :create | :patch, map(), map()) :: String.t()

Canonically digests a pending change: "sha256:..." over the action module, mode, locator params, and updates map together. Locator params are params keys not in the input schema (fields/1) — typically the URL-anchored subject id. Folding action, mode, and locators in makes "the user confirmed this exact change to this record" total — a digest minted for one action, mode, or subject never confirms another, and input-less actions (whose updates are always %{}) don't collapse across records.

The encoding is hand-rolled and injective — every node is type-tagged, binaries and atom names are length-prefixed, and map entries are sorted by encoded key at every depth. Elixir map ordering alone is not sufficient (large maps enumerate in hash order), and the encoding avoids term_to_binary, whose bytes are not guaranteed stable across OTP releases — digests must survive the confirmation gap in a mixed-version rolling deploy.