@thomasfosterau/effect-forms 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1768 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import * as Schema from "effect/Schema";
3
+ import * as SchemaAST from "effect/SchemaAST";
4
+ import * as Effect from "effect/Effect";
5
+ import * as SchemaParser from "effect/SchemaParser";
6
+ import "effect/SchemaIssue";
7
+ import * as Duration from "effect/Duration";
8
+ //#region src/Coerce.ts
9
+ var Coerce_exports = /* @__PURE__ */ __exportAll({
10
+ booleanFromCheckbox: () => booleanFromCheckbox,
11
+ emptyToNull: () => emptyToNull,
12
+ emptyToUndefined: () => emptyToUndefined,
13
+ numberFromField: () => numberFromField,
14
+ trimToNull: () => trimToNull,
15
+ trimToUndefined: () => trimToUndefined
16
+ });
17
+ /**
18
+ * @copyright Thomas Foster
19
+ *
20
+ * Tri-state coercers for the HTML-form boundary.
21
+ *
22
+ * An HTML form submits every value as a string, and an absent optional field
23
+ * arrives as `undefined` (or an empty string). Mapping that onto a
24
+ * value / `null` / `undefined` update contract — where a value **sets**, `null`
25
+ * **clears** a nullable field, and `undefined` **skips** (carry-forward) — is
26
+ * the same boundary discipline the host app applies, lifted here so any
27
+ * consumer of the core gets it without a framework dependency.
28
+ *
29
+ * These are plain, total functions: they never throw and never reach for a
30
+ * framework's `error()` helper (validation failures belong on the schema, not
31
+ * in a coercer).
32
+ *
33
+ * @module effect-forms/Coerce
34
+ */
35
+ /** Empty string / `undefined` → `undefined`; otherwise the value (skip semantics). */
36
+ function emptyToUndefined(value) {
37
+ return value === "" || value === void 0 ? void 0 : value;
38
+ }
39
+ /**
40
+ * `undefined` stays `undefined` (skip); empty string / `null` → `null`
41
+ * (clear a nullable field); otherwise the value (set). The three-state coercer
42
+ * for nullable update fields.
43
+ */
44
+ function emptyToNull(value) {
45
+ if (value === void 0) return void 0;
46
+ if (value === "" || value === null) return null;
47
+ return value;
48
+ }
49
+ /** Trim, then {@link emptyToUndefined}: whitespace-only collapses to `undefined` (skip). */
50
+ function trimToUndefined(value) {
51
+ return emptyToUndefined(value?.trim());
52
+ }
53
+ /** Trim, then {@link emptyToNull}: whitespace-only / blank clears (`null`). */
54
+ function trimToNull(value) {
55
+ return emptyToNull(value?.trim());
56
+ }
57
+ function numberFromField(raw, { nullable }) {
58
+ const coerced = nullable ? emptyToNull(raw) : emptyToUndefined(raw);
59
+ return typeof coerced === "string" ? Number(coerced) : coerced;
60
+ }
61
+ /**
62
+ * Interpret a checkbox field. An HTML checkbox submits its `value` (default
63
+ * `"on"`) only when checked and omits the field entirely when unchecked, so a
64
+ * present field is `true` and an absent one is `false`. The truthy strings
65
+ * `"on"` / `"true"` / `"1"` / `"yes"` all read as `true`; an explicit
66
+ * `"false"` / `"0"` / `"off"` / `"no"` reads as `false`.
67
+ */
68
+ function booleanFromCheckbox(raw) {
69
+ if (raw === void 0) return false;
70
+ const normalised = raw.trim().toLowerCase();
71
+ return !(normalised === "" || normalised === "false" || normalised === "0" || normalised === "off" || normalised === "no");
72
+ }
73
+ //#endregion
74
+ //#region src/Command.ts
75
+ /**
76
+ * @copyright Thomas Foster
77
+ *
78
+ * Command-struct introspection and form derivation.
79
+ *
80
+ * A host app's write surface is a pair of flat command `Schema.Struct`s per
81
+ * entity — a create struct (required fields bare, the rest `optional`) and an
82
+ * update struct (keyed by a required `id`, every field `optional`, clearable
83
+ * fields `optional(NullOr(...))` carrying the value / `null` / `undefined`
84
+ * tri-state). This module reads that pair into a plain-data {@link CommandPlan}
85
+ * and derives the three per-entity form artefacts that were previously
86
+ * hand-written per entity: the all-string Form-Input schema fields, the blank
87
+ * defaults, and the tri-state adapters that map a validated multipart input
88
+ * onto the encoded command payload.
89
+ *
90
+ * Classification leads with behavioural decode probes — date codecs are
91
+ * transformations and branded ids erase to `String`, so "what does this
92
+ * schema accept" is the only robust signal for those (the same approach the
93
+ * host app's model walkers use). Two kinds fall back to AST matching instead,
94
+ * because no decode probe can name what they are: `enum`'s literal option set
95
+ * (needed anyway for the renderer, not just the classification) and
96
+ * `structured`'s "this is a nested object, not a scalar" shape. Neither
97
+ * shadows a probe-classified kind — see {@link classify}.
98
+ *
99
+ * The derived adapters emit the **encoded** command payload (dates stay
100
+ * `YYYY-MM-DD` strings, ids stay plain strings) — the command's canonical
101
+ * decode downstream owns branding and validation. Genuinely irregular fields
102
+ * (a create-time default, an asymmetric coercion) stay expressible via the
103
+ * per-field {@link AdaptOverrides} escape hatch instead of forcing ejection.
104
+ *
105
+ * @module effect-forms/Command
106
+ */
107
+ var Command_exports = /* @__PURE__ */ __exportAll({
108
+ blankDefaults: () => blankDefaults,
109
+ createAdapter: () => createAdapter,
110
+ formInputFields: () => formInputFields,
111
+ jsonRowsFromField: () => jsonRowsFromField,
112
+ plan: () => plan,
113
+ updateAdapter: () => updateAdapter
114
+ });
115
+ /**
116
+ * Run one decode probe: whether the schema accepts the input, plus the decoded
117
+ * value for instance checks (the date probe needs to see a `Date` come out,
118
+ * not just a successful decode).
119
+ */
120
+ const probe = (schema, input) => {
121
+ try {
122
+ return {
123
+ ok: true,
124
+ value: Schema.decodeUnknownSync(schema)(input)
125
+ };
126
+ } catch {
127
+ return { ok: false };
128
+ }
129
+ };
130
+ /** True when a schema decodes the given input without throwing. */
131
+ const decodes = (schema, input) => probe(schema, input).ok;
132
+ /** A syntactically valid UUID no other field kind accepts as non-text. */
133
+ const PROBE_UUID = "9f8c1c2e-4b3a-4c5d-8e6f-0a1b2c3d4e5f";
134
+ /**
135
+ * Peel exactly the `optional` / `NullOr` wrapper layers off a property's type
136
+ * AST, stopping at the first union that isn't itself a pure wrapper. A wrapper
137
+ * union carries exactly one non-`null`/`undefined` member (the wrapped type);
138
+ * a union with zero or several such members is a real value-level union (an
139
+ * `enum` field's own literal set, say) rather than another layer to peel, so
140
+ * the loop stops there. Unlike {@link module:effect-forms/Constraints}'s
141
+ * `unwrapOptional` (which only ever needs the *first* real member), this
142
+ * walks every wrapper layer and preserves a multi-member union intact —
143
+ * `enumOptions` below needs the full literal set, not just one branch of it.
144
+ */
145
+ function unwrapWrapperUnion(ast) {
146
+ let current = ast;
147
+ while (SchemaAST.isUnion(current)) {
148
+ const real = current.types.filter((t) => !SchemaAST.isUndefined(t) && !SchemaAST.isNull(t));
149
+ if (real.length !== 1 || real.length === current.types.length) break;
150
+ current = real[0];
151
+ }
152
+ return current;
153
+ }
154
+ /**
155
+ * The literal option values of a field shaped like `Schema.Literal(...)` or
156
+ * `Schema.Literals([...])` (optionally `optional` / `NullOr`-wrapped) —
157
+ * `undefined` when the field isn't literal-shaped. Stringified: a `<select>`'s
158
+ * option values are strings at the form boundary regardless of the literal's
159
+ * own type.
160
+ */
161
+ function enumOptions(ast) {
162
+ const inner = unwrapWrapperUnion(ast);
163
+ if (SchemaAST.isLiteral(inner)) return [String(inner.literal)];
164
+ if (SchemaAST.isUnion(inner)) {
165
+ const literals = inner.types.filter(SchemaAST.isLiteral);
166
+ if (literals.length === inner.types.length && literals.length > 0) return literals.map((literal) => String(literal.literal));
167
+ }
168
+ }
169
+ /**
170
+ * Whether a field is itself an object/struct schema (optionally `optional` /
171
+ * `NullOr`-wrapped) — the `structured` kind's shape (inline / block ProseKit
172
+ * JSON: `{ type, content }`). Mirrors {@link module:effect-forms/Field}'s and
173
+ * {@link module:effect-forms/Constraints}'s own `SchemaAST.isObjects` struct
174
+ * check. A `Schema.declare`d type (`UploadedDocument`, `Image`) has no
175
+ * `Objects` AST node, so it is never caught here — the default-throw the
176
+ * derivation relies on stays intact.
177
+ */
178
+ function isStructured(ast) {
179
+ return SchemaAST.isObjects(unwrapWrapperUnion(ast));
180
+ }
181
+ /**
182
+ * The scalar kinds classified purely by what a schema decodes, with no AST
183
+ * inspection: array → boolean → number → date → id → text. `undefined` when
184
+ * the schema accepts none of them, so {@link classify} can fall through to
185
+ * its AST-based checks (`enum`, `structured`).
186
+ */
187
+ const probeKind = (schema) => {
188
+ if (decodes(schema, [])) return "jsonArray";
189
+ if (decodes(schema, true)) return "boolean";
190
+ if (decodes(schema, 42)) return "number";
191
+ const date = probe(schema, "2024-01-02T03:04:05.000Z");
192
+ if (date.ok && date.value instanceof Date) return "date";
193
+ if (decodes(schema, PROBE_UUID) && !decodes(schema, "not-a-uuid")) return "id";
194
+ if (decodes(schema, "plain text")) return "text";
195
+ };
196
+ /**
197
+ * Classify one field schema: by its AST shape first, then by what it accepts.
198
+ * `enum` is checked before {@link probeKind} rather than after, since a
199
+ * decode probe can't tell "the literal set an enum accepts" from any other
200
+ * shape (worse, a numeric-literal enum whose set happens to include the `42`
201
+ * probe value would otherwise misclassify as `number`) — reading the literal
202
+ * set off the AST is unambiguous, and it's needed anyway for the renderer.
203
+ * `structured` is checked last, after every probe has failed: an object
204
+ * schema never decodes any of `probeKind`'s scalar probes, so there is no
205
+ * real ordering hazard, but putting the broadest, AST-only check last keeps
206
+ * every more specific kind first in line. A field that matches nothing
207
+ * (`Schema.declare`d — `UploadedDocument`, `Image`) throws at module load so
208
+ * the gap surfaces when the plan is declared, not on a live submission.
209
+ */
210
+ const classify = (key, schema) => {
211
+ const options = enumOptions(schema.ast);
212
+ if (options !== void 0) return {
213
+ kind: "enum",
214
+ options
215
+ };
216
+ const probed = probeKind(schema);
217
+ if (probed !== void 0) return { kind: probed };
218
+ if (isStructured(schema.ast)) return { kind: "structured" };
219
+ throw new Error(`Command.plan: field "${key}" matches no known kind (text/id/number/date/jsonArray/boolean/enum/structured) — a Schema.declare'd type needs an explicit form-level treatment before it can derive`);
220
+ };
221
+ /**
222
+ * Read a create/update command struct pair into a {@link CommandPlan}.
223
+ * Runs once at module load — probe cost is a handful of tiny decodes per
224
+ * field, never per render or per request.
225
+ */
226
+ const plan = (create, update) => {
227
+ const createFields = create.fields;
228
+ const updateFields = update.fields;
229
+ const keys = [];
230
+ for (const key in updateFields) if (key !== "id") keys.push(key);
231
+ for (const key in createFields) if (key !== "id" && !(key in updateFields)) keys.push(key);
232
+ return {
233
+ fields: keys.map((key) => {
234
+ const createField = createFields[key];
235
+ const updateField = updateFields[key];
236
+ const { kind, options } = classify(key, createField ?? updateField);
237
+ return {
238
+ key,
239
+ kind,
240
+ inCreate: createField !== void 0,
241
+ createRequired: createField !== void 0 && !decodes(createField, void 0),
242
+ inUpdate: updateField !== void 0,
243
+ updateClearable: updateField !== void 0 && decodes(updateField, null),
244
+ ...options !== void 0 ? { options } : {}
245
+ };
246
+ }),
247
+ clientId: "id" in createFields
248
+ };
249
+ };
250
+ /**
251
+ * Derive the all-string Form-Input schema fields for one mode — every value is
252
+ * a string at the multipart boundary. Create-required fields get a min-length
253
+ * check so blankness surfaces as an inline field error; the update `id` gets
254
+ * the UUID check. Spread into `Schema.Struct({ ...Command.formInputFields(plan,
255
+ * "create") })`, with `overrides` spread last for irregular fields.
256
+ */
257
+ const formInputFields = (commandPlan, mode, overrides = {}) => {
258
+ const fields = {};
259
+ if (mode === "create" && commandPlan.clientId) fields.id = Schema.optional(Schema.String);
260
+ if (mode === "update") fields.id = Schema.String.check(Schema.isUUID());
261
+ for (const field of commandPlan.fields) {
262
+ if (mode === "create" ? !field.inCreate : !field.inUpdate) continue;
263
+ fields[field.key] = mode === "create" && field.createRequired ? Schema.String.check(Schema.isMinLength(1)) : Schema.optional(Schema.String);
264
+ }
265
+ return {
266
+ ...fields,
267
+ ...overrides
268
+ };
269
+ };
270
+ /**
271
+ * Blank string defaults for one mode's Form-Input shape. Client-id entities
272
+ * override `id` at the call site (`{ ...blankDefaults(plan, "create"), id:
273
+ * crypto.randomUUID() }`) so retries stay idempotent.
274
+ */
275
+ const blankDefaults = (commandPlan, mode) => {
276
+ const defaults = {};
277
+ if (mode === "update" || commandPlan.clientId) defaults.id = "";
278
+ for (const field of commandPlan.fields) {
279
+ if (mode === "create" ? !field.inCreate : !field.inUpdate) continue;
280
+ defaults[field.key] = "";
281
+ }
282
+ return defaults;
283
+ };
284
+ /**
285
+ * Parse a JSON-encoded row list (a contributor list) from a multipart form
286
+ * field. A blank or absent field yields `undefined` (skip — leave the existing
287
+ * list untouched); an empty JSON array (`"[]"`) clears the list. Rows keep
288
+ * `id` / `creditedName` when present; `orderingPosition` falls back to the
289
+ * row's array index. Throws a plain `Error` on a malformed payload — mapping
290
+ * that onto a framework response is the caller's concern.
291
+ */
292
+ const jsonRowsFromField = (raw, label) => {
293
+ if (raw === void 0 || raw.trim() === "") return void 0;
294
+ let parsed;
295
+ try {
296
+ parsed = JSON.parse(raw);
297
+ } catch {
298
+ throw new Error(`Invalid ${label} payload — expected JSON array`);
299
+ }
300
+ if (!Array.isArray(parsed)) throw new Error(`Invalid ${label} payload — expected array`);
301
+ return parsed.map((row, index) => {
302
+ if (typeof row !== "object" || row === null) throw new Error(`Invalid ${label} entry at index ${index}`);
303
+ const r = row;
304
+ if (typeof r.agentId !== "string") throw new Error(`${label} entry at index ${index} missing agentId`);
305
+ return {
306
+ id: typeof r.id === "string" ? r.id : void 0,
307
+ agentId: r.agentId,
308
+ creditedName: typeof r.creditedName === "string" ? r.creditedName : void 0,
309
+ orderingPosition: typeof r.orderingPosition === "number" ? r.orderingPosition : index
310
+ };
311
+ });
312
+ };
313
+ /** Text fields: trim always; required create fields never skip. */
314
+ const coerceText = (ctx, raw) => ctx.required ? (raw ?? "").trim() : ctx.clearable ? trimToNull(raw) : trimToUndefined(raw);
315
+ /** Id fields: no trimming (pickers emit `""` or a UUID); required create FKs pass through raw. */
316
+ const coerceId = (ctx, raw) => ctx.required ? raw : ctx.clearable ? emptyToNull(raw) : emptyToUndefined(raw);
317
+ /** Numeric text fields: blank clears on the clearable path, skips otherwise. */
318
+ const coerceNumber = (ctx, raw) => ctx.clearable ? numberFromField(raw, { nullable: true }) : numberFromField(raw, { nullable: false });
319
+ /** Date fields stay encoded (`YYYY-MM-DD` strings); create trims, update passes through. */
320
+ const coerceDate = (ctx, raw) => {
321
+ const value = ctx.trimCreate ? raw?.trim() : raw;
322
+ return ctx.clearable ? emptyToNull(value) : emptyToUndefined(value);
323
+ };
324
+ /**
325
+ * Boolean fields: a `<select>`'s value set, not a checkbox — the only truthy
326
+ * raw value is the literal string `"true"`, everything else (`"false"`,
327
+ * blank, absent) reads as `false`. Create defaults an absent/blank field to
328
+ * `false` (never skips); update skips absent/blank unless clearable, in which
329
+ * case it clears (`null`) instead — the create-defaults / update-skips
330
+ * asymmetry the Content family already hand-writes.
331
+ */
332
+ const coerceBoolean = (ctx, raw) => {
333
+ const trimmed = raw?.trim();
334
+ if (trimmed === void 0 || trimmed === "") return ctx.required ? false : ctx.clearable ? null : void 0;
335
+ return trimmed === "true";
336
+ };
337
+ /**
338
+ * Parse a JSON-encoded structured document (inline / block ProseKit JSON)
339
+ * from a multipart field. Blank or absent skips (`undefined`) or, on the
340
+ * clearable path, clears (`null`); a present value is `JSON.parse`d and
341
+ * passed through as-is — shape validation is the command's own decode's job,
342
+ * exactly as {@link jsonRowsFromField} leaves row-shape validation to itself
343
+ * rather than duplicating it here. `JSON.parse` throws a `SyntaxError` on
344
+ * malformed JSON, same as {@link jsonRowsFromField} does on a malformed row
345
+ * list — mapping that onto a framework response is the caller's concern.
346
+ */
347
+ const structuredFromField = (raw, clearable) => {
348
+ const trimmed = raw?.trim();
349
+ if (trimmed === void 0 || trimmed === "") return clearable ? null : void 0;
350
+ return JSON.parse(trimmed);
351
+ };
352
+ /** Build one field's {@link CoerceContext} for one mode. */
353
+ const coerceContextFor = (field, mode) => ({
354
+ required: mode === "create" && field.createRequired,
355
+ clearable: mode === "update" && field.updateClearable,
356
+ trimCreate: mode === "create"
357
+ });
358
+ /** The default coercion for one field in one mode. */
359
+ const coerce = (field, mode, raw) => {
360
+ const ctx = coerceContextFor(field, mode);
361
+ switch (field.kind) {
362
+ case "text":
363
+ case "enum": return coerceText(ctx, raw);
364
+ case "id": return coerceId(ctx, raw);
365
+ case "number": return coerceNumber(ctx, raw);
366
+ case "date": return coerceDate(ctx, raw);
367
+ case "jsonArray": return jsonRowsFromField(raw, field.key);
368
+ case "boolean": return coerceBoolean(ctx, raw);
369
+ case "structured": return structuredFromField(raw, ctx.clearable);
370
+ }
371
+ };
372
+ /**
373
+ * Derive the create adapter: validated all-string input → encoded create
374
+ * command payload. `overrides` replace the default per-field coercion for
375
+ * genuinely irregular fields (e.g. a create-time fallback default).
376
+ */
377
+ const createAdapter = (commandPlan, overrides = {}) => (input) => {
378
+ const out = {};
379
+ if (commandPlan.clientId) out.id = trimToUndefined(input.id);
380
+ for (const field of commandPlan.fields) {
381
+ if (!field.inCreate) continue;
382
+ const override = overrides[field.key];
383
+ out[field.key] = override ? override(input[field.key]) : coerce(field, "create", input[field.key]);
384
+ }
385
+ return out;
386
+ };
387
+ /**
388
+ * Derive the update adapter: validated all-string input → encoded update
389
+ * command payload (value sets, `null` clears a clearable field, `undefined`
390
+ * skips). The required `id` passes through as-is — the command's canonical
391
+ * decode brands it.
392
+ */
393
+ const updateAdapter = (commandPlan, overrides = {}) => (input) => {
394
+ const out = { id: input.id };
395
+ for (const field of commandPlan.fields) {
396
+ if (!field.inUpdate) continue;
397
+ const override = overrides[field.key];
398
+ out[field.key] = override ? override(input[field.key]) : coerce(field, "update", input[field.key]);
399
+ }
400
+ return out;
401
+ };
402
+ //#endregion
403
+ //#region src/Constraints.ts
404
+ var Constraints_exports = /* @__PURE__ */ __exportAll({
405
+ constraintAt: () => constraintAt,
406
+ constraintsFromSchema: () => constraintsFromSchema,
407
+ isLeafConstraint: () => isLeafConstraint
408
+ });
409
+ /** Whether a node in an {@link InputConstraints} tree is a leaf ({@link InputConstraint}) rather than a branch. */
410
+ const isLeafConstraint = (value) => "required" in value || "min" in value || "max" in value || "minlength" in value || "maxlength" in value || "pattern" in value || "step" in value;
411
+ /** Whether a property's type AST is optional (so the field is not required). */
412
+ const isOptional = (type) => type.context?.isOptional === true;
413
+ /**
414
+ * Unwrap an `optional` / `NullOr` union the way {@link module:effect-forms/Field}'s
415
+ * `fromAst` does: recurse into the first member that isn't a bare `undefined`
416
+ * / `null` literal, so a wrapped struct / array / refined leaf still yields
417
+ * its own shape instead of falling through to an empty constraint.
418
+ */
419
+ function unwrapOptional(type) {
420
+ if (!SchemaAST.isUnion(type)) return type;
421
+ for (const member of type.types) {
422
+ if (SchemaAST.isUndefined(member) || SchemaAST.isNull(member)) continue;
423
+ return unwrapOptional(member);
424
+ }
425
+ return type;
426
+ }
427
+ /** Flatten a `Checks` array (recursing into `FilterGroup`s) into individual filters. */
428
+ function flattenChecks(checks, out) {
429
+ if (!checks) return;
430
+ for (const check of checks) if (check._tag === "FilterGroup") flattenChecks(check.checks, out);
431
+ else out.push(check);
432
+ }
433
+ /**
434
+ * One constraint patch per recognised representation id — the built-in
435
+ * checks that map onto a native HTML attribute. An id with no entry here
436
+ * (an unannotated custom filter, or a built-in check with no HTML
437
+ * equivalent) derives nothing. `isInt` isn't a plain patch: it only sets
438
+ * `step` as a fallback when nothing more specific already did, so it's
439
+ * handled separately in {@link constraintFromChecks}.
440
+ */
441
+ const payloadPatches = {
442
+ "effect/schema/isMinLength": (p) => ({ minlength: p.minLength }),
443
+ "effect/schema/isMaxLength": (p) => ({ maxlength: p.maxLength }),
444
+ "effect/schema/isLengthBetween": (p) => ({
445
+ minlength: p.minimum,
446
+ maxlength: p.maximum
447
+ }),
448
+ "effect/schema/isPattern": (p) => ({ pattern: p.source }),
449
+ "effect/schema/isGreaterThan": (p) => ({ min: p.exclusiveMinimum }),
450
+ "effect/schema/isGreaterThanOrEqualTo": (p) => ({ min: p.minimum }),
451
+ "effect/schema/isLessThan": (p) => ({ max: p.exclusiveMaximum }),
452
+ "effect/schema/isLessThanOrEqualTo": (p) => ({ max: p.maximum }),
453
+ "effect/schema/isBetween": (p) => ({
454
+ min: p.minimum,
455
+ max: p.maximum
456
+ }),
457
+ "effect/schema/isMultipleOf": (p) => ({ step: p.divisor })
458
+ };
459
+ /**
460
+ * Look up and apply `payloadPatches[representation.id]`. The cast is sound:
461
+ * each payload is authored by the same built-in check constructor its id
462
+ * names, so an entry is never called with a mismatched payload shape.
463
+ */
464
+ function applyPayloadPatch(representation) {
465
+ const patch = payloadPatches[representation.id];
466
+ return patch?.(representation.payload);
467
+ }
468
+ /**
469
+ * Derive an {@link InputConstraint}'s refinement attributes from the checks
470
+ * attached to `outer` (the property's own type, e.g. the union wrapping an
471
+ * optional field) and `inner` (the unwrapped leaf) — `inner`'s checks win on
472
+ * conflict since they're the more specific, unwrapped node. A check whose
473
+ * `representation` annotation has no recognised id (an unannotated custom
474
+ * filter) is skipped: there is no attribute to derive from it.
475
+ */
476
+ function constraintFromChecks(outer, inner) {
477
+ const filters = [];
478
+ flattenChecks(outer.checks, filters);
479
+ if (inner !== outer) flattenChecks(inner.checks, filters);
480
+ let out = {};
481
+ let hasIntCheck = false;
482
+ for (const filter of filters) {
483
+ const representation = filter.annotations?.representation;
484
+ if (!representation) continue;
485
+ if (representation.id === "effect/schema/isInt") {
486
+ hasIntCheck = true;
487
+ continue;
488
+ }
489
+ out = {
490
+ ...out,
491
+ ...applyPayloadPatch(representation)
492
+ };
493
+ }
494
+ if (hasIntCheck && out.step === void 0) out.step = 1;
495
+ return out;
496
+ }
497
+ /**
498
+ * Whether `ast` is a homogeneous array/list — `Schema.Array(item)` or
499
+ * `Schema.NonEmptyArray(item)` — as opposed to a fixed-shape tuple with
500
+ * differently-typed positional elements. Only a homogeneous array has a
501
+ * single item schema to descend into; a tuple's elements each need their own
502
+ * constraint, which this leaf-per-key tree can't represent, so it's left to
503
+ * `overrides`.
504
+ */
505
+ function homogeneousItem(ast) {
506
+ if (ast.rest.length !== 1) return void 0;
507
+ const [item] = ast.rest;
508
+ if (ast.elements.length === 0) return item;
509
+ if (ast.elements.length === 1 && ast.elements[0] === item) return item;
510
+ }
511
+ /**
512
+ * Derive the constraint (or nested tree) for one property/item type: a
513
+ * struct descends into its properties, a homogeneous array descends into its
514
+ * item type (the array's own length checks have no native equivalent for a
515
+ * repeated field, so they're not derived) while folding the array's own
516
+ * optionality into the item's `required`, and anything else is a leaf whose
517
+ * `required` comes from optionality and whose refinement attributes come
518
+ * from {@link constraintFromChecks}.
519
+ */
520
+ function constraintsForType(type) {
521
+ const required = !isOptional(type);
522
+ const inner = unwrapOptional(type);
523
+ if (SchemaAST.isObjects(inner)) return constraintsForObjects(inner);
524
+ if (SchemaAST.isArrays(inner)) {
525
+ const item = homogeneousItem(inner);
526
+ if (!item) return { required };
527
+ const itemConstraints = constraintsForType(item);
528
+ return isLeafConstraint(itemConstraints) ? {
529
+ ...itemConstraints,
530
+ required: required && itemConstraints.required !== false
531
+ } : itemConstraints;
532
+ }
533
+ return {
534
+ required,
535
+ ...constraintFromChecks(type, inner)
536
+ };
537
+ }
538
+ /** Build the constraint tree for one object AST node. */
539
+ function constraintsForObjects(ast) {
540
+ const out = {};
541
+ for (const ps of ast.propertySignatures) out[String(ps.name)] = constraintsForType(ps.type);
542
+ return out;
543
+ }
544
+ /** Deep-merge constraint overrides over a derived tree. */
545
+ function mergeConstraints(base, overrides) {
546
+ const out = { ...base };
547
+ for (const key of Object.keys(overrides)) {
548
+ const override = overrides[key];
549
+ if (override === void 0) continue;
550
+ const existing = out[key];
551
+ if (existing !== void 0 && !isLeafConstraint(existing) && !isLeafConstraint(override)) out[key] = mergeConstraints(existing, override);
552
+ else if (existing !== void 0 && isLeafConstraint(existing) && isLeafConstraint(override)) out[key] = {
553
+ ...existing,
554
+ ...override
555
+ };
556
+ else out[key] = override;
557
+ }
558
+ return out;
559
+ }
560
+ /**
561
+ * Derive the {@link InputConstraints} for a struct schema. Non-struct schemas
562
+ * yield an empty tree. `overrides` (deep-merged over the derived tree, and
563
+ * winning on conflict) is the seam for anything the AST walk does not infer.
564
+ */
565
+ function constraintsFromSchema(schema, overrides = {}) {
566
+ const ast = schema.ast;
567
+ return mergeConstraints(SchemaAST.isObjects(ast) ? constraintsForObjects(ast) : {}, overrides);
568
+ }
569
+ /**
570
+ * Read the {@link InputConstraint} leaf at `path` in an {@link InputConstraints}
571
+ * tree, defaulting to `{}` when `path` has no derived constraints. Array
572
+ * indices in `path` are skipped rather than descended into — the tree has no
573
+ * per-item nodes to index into, since {@link constraintsForType} derives one
574
+ * constraint (or branch) per *field*, shared by every item at that field. A
575
+ * bare item path (`tags[0]`) resolves to that shared leaf; a path that
576
+ * continues past the index into a struct item's own field (`items[0].label`)
577
+ * skips the index and resolves straight to `label`'s derived constraint.
578
+ */
579
+ function constraintAt(constraints, path) {
580
+ let node = constraints;
581
+ for (const segment of path) {
582
+ if (typeof segment === "number") continue;
583
+ if (node === void 0 || isLeafConstraint(node)) return {};
584
+ node = node[segment];
585
+ }
586
+ return node !== void 0 && isLeafConstraint(node) ? node : {};
587
+ }
588
+ //#endregion
589
+ //#region src/FormData.ts
590
+ var FormData_exports = /* @__PURE__ */ __exportAll({
591
+ formDataToObject: () => formDataToObject,
592
+ parseFieldPath: () => parseFieldPath,
593
+ requestToObject: () => requestToObject
594
+ });
595
+ /**
596
+ * Split a flat field name into its path tokens. Supports dot notation
597
+ * (`a.b`), bracket notation (`a[b]`, `a[0]`), and the trailing append form
598
+ * (`a[]`, represented by an empty-string token). `"a[b][0]"` → `["a","b","0"]`;
599
+ * `"a.b[]"` → `["a","b",""]`.
600
+ */
601
+ function parseFieldPath(name) {
602
+ const tokens = [];
603
+ let buffer = "";
604
+ let inBracket = false;
605
+ for (const char of name) if (char === "[") {
606
+ if (buffer !== "") tokens.push(buffer);
607
+ buffer = "";
608
+ inBracket = true;
609
+ } else if (char === "]") {
610
+ tokens.push(buffer);
611
+ buffer = "";
612
+ inBracket = false;
613
+ } else if (char === "." && !inBracket) {
614
+ if (buffer !== "") tokens.push(buffer);
615
+ buffer = "";
616
+ } else buffer += char;
617
+ if (buffer !== "") tokens.push(buffer);
618
+ return tokens.length > 1 && tokens[0] === "" ? tokens.slice(1) : tokens;
619
+ }
620
+ const isIndexToken = (token) => token === "" || /^\d+$/.test(token);
621
+ /** Create the container a token should live in: array for index/append tokens. */
622
+ const makeContainer = (nextToken) => isIndexToken(nextToken) ? [] : {};
623
+ const setOnContainer = (container, token, value) => {
624
+ if (Array.isArray(container)) if (token === "") container.push(value);
625
+ else container[Number(token)] = value;
626
+ else container[token] = value;
627
+ };
628
+ const getFromContainer = (container, token) => Array.isArray(container) ? container[Number(token)] : container[token];
629
+ /** Assign `value` deep into `root` along `tokens`, creating containers as needed. */
630
+ function assign(root, tokens, value) {
631
+ let node = root;
632
+ for (let i = 0; i < tokens.length - 1; i++) {
633
+ const token = tokens[i];
634
+ const nextToken = tokens[i + 1];
635
+ let child = token === "" ? void 0 : getFromContainer(node, token);
636
+ if (child === void 0 || typeof child !== "object" || child === null) {
637
+ child = makeContainer(nextToken);
638
+ setOnContainer(node, token, child);
639
+ }
640
+ node = child;
641
+ }
642
+ const last = tokens[tokens.length - 1];
643
+ if (last !== "" && !Array.isArray(node)) {
644
+ const existing = getFromContainer(node, last);
645
+ if (existing !== void 0) {
646
+ const arr = Array.isArray(existing) ? existing : [existing];
647
+ arr.push(value);
648
+ setOnContainer(node, last, arr);
649
+ return;
650
+ }
651
+ }
652
+ setOnContainer(node, last, value);
653
+ }
654
+ /** Normalise a source into `[name, value]` entries. */
655
+ function* entriesOf(source) {
656
+ if (typeof source[Symbol.iterator] === "function" && !(source instanceof FormData) && !(source instanceof URLSearchParams)) {
657
+ yield* source;
658
+ return;
659
+ }
660
+ for (const entry of source.entries()) yield entry;
661
+ }
662
+ /**
663
+ * Parse a form body into a nested object / array tree. Field names use dot or
664
+ * bracket path notation (see {@link parseFieldPath}); repeated bare keys
665
+ * become arrays; `File` values pass through. The result is ready to hand to
666
+ * `Schema.decodeUnknown` with a schema whose leaves coerce the string inputs.
667
+ */
668
+ function formDataToObject(source) {
669
+ const root = {};
670
+ for (const [name, value] of entriesOf(source)) {
671
+ const tokens = parseFieldPath(name);
672
+ if (tokens.length === 0) continue;
673
+ assign(root, tokens, value);
674
+ }
675
+ return root;
676
+ }
677
+ /** Read and parse a `Request`'s form body. Errors (e.g. wrong content-type) reject. */
678
+ async function requestToObject(request) {
679
+ return formDataToObject(await request.formData());
680
+ }
681
+ //#endregion
682
+ //#region src/Path.ts
683
+ /**
684
+ * @copyright Thomas Foster
685
+ *
686
+ * Nested get / set along a {@link FormPath}.
687
+ *
688
+ * A form's data is a tree, and the reactive bindings address it by path:
689
+ * `["author", "name"]`, `["tags", 0]`. This module reads and writes a value
690
+ * at such a path — {@link getAt} descends, {@link setAt} returns a new tree
691
+ * with the value replaced (structural sharing, never a mutation) so a rune /
692
+ * atom store can diff cheaply. It is the framework-neutral half of the
693
+ * reactive field bindings (the Svelte store leans on it for
694
+ * `setValue` and the array operations).
695
+ *
696
+ * Paths are {@link FormPath}s — the same array-of-segments the {@link
697
+ * module:effect-forms/Errors} tree and {@link module:effect-forms/Issues}
698
+ * walker already speak — so string keys address object properties and number
699
+ * segments address array indices. {@link toString} / {@link fromString}
700
+ * convert to and from the flat `author.name` / `tags[0]` notation the HTML
701
+ * body parser ({@link module:effect-forms/FormData}) uses.
702
+ *
703
+ * @module effect-forms/Path
704
+ */
705
+ var Path_exports = /* @__PURE__ */ __exportAll({
706
+ clearAll: () => clearAll,
707
+ fromString: () => fromString,
708
+ getAt: () => getAt,
709
+ insertAt: () => insertAt,
710
+ isUnder: () => isUnder,
711
+ moveAt: () => moveAt,
712
+ remapIndex: () => remapIndex,
713
+ remapKeyedRecord: () => remapKeyedRecord,
714
+ removeAt: () => removeAt,
715
+ replaceAt: () => replaceAt$1,
716
+ setAt: () => setAt,
717
+ swapAt: () => swapAt,
718
+ toString: () => toString
719
+ });
720
+ /**
721
+ * Read the value at `path`. Returns `undefined` if any segment is missing or
722
+ * traverses a non-object. The empty path returns the object itself.
723
+ */
724
+ function getAt(obj, path) {
725
+ let current = obj;
726
+ for (const segment of path) {
727
+ if (current === null || typeof current !== "object") return void 0;
728
+ current = current[segment];
729
+ }
730
+ return current;
731
+ }
732
+ /**
733
+ * Return a new tree with the value at `path` replaced by `value`. Objects and
734
+ * arrays along the path are cloned (structural sharing elsewhere); missing
735
+ * containers are created as arrays for numeric segments and objects otherwise.
736
+ * The empty path returns `value`. Pure — `obj` is never mutated.
737
+ */
738
+ function setAt(obj, path, value) {
739
+ if (path.length === 0) return value;
740
+ const [head, ...rest] = path;
741
+ const clone = Array.isArray(obj) ? [...obj] : { ...obj };
742
+ const child = clone[head];
743
+ clone[head] = rest.length === 0 ? value : setAt(child === null || typeof child !== "object" ? typeof rest[0] === "number" ? [] : {} : child, rest, value);
744
+ return clone;
745
+ }
746
+ /** Whether `path` is `root` or nested beneath it. */
747
+ function isUnder(path, root) {
748
+ if (path.length < root.length) return false;
749
+ for (let i = 0; i < root.length; i++) if (path[i] !== root[i]) return false;
750
+ return true;
751
+ }
752
+ /**
753
+ * Render a {@link FormPath} as flat field-name notation: string segments join
754
+ * with `.` and number segments become `[n]` (`["tags", 0]` → `"tags[0]"`).
755
+ * The inverse of {@link fromString}.
756
+ */
757
+ function toString(path) {
758
+ let out = "";
759
+ for (const segment of path) if (typeof segment === "number") out += `[${segment}]`;
760
+ else out += out === "" ? segment : `.${segment}`;
761
+ return out;
762
+ }
763
+ /**
764
+ * Parse flat field-name notation into a {@link FormPath}: integer tokens become
765
+ * number segments (array indices), everything else stays a string. The inverse
766
+ * of {@link toString}.
767
+ */
768
+ function fromString(name) {
769
+ return parseFieldPath(name).map((token) => /^\d+$/.test(token) ? Number(token) : token);
770
+ }
771
+ /** The {@link IndexMap} for removing the item at `index`: shifts later indices down by one. */
772
+ function removeAt(index) {
773
+ return (i) => i === index ? null : i > index ? i - 1 : i;
774
+ }
775
+ /** The {@link IndexMap} for inserting an item at `index`: shifts `index` and later up by one. */
776
+ function insertAt(index) {
777
+ return (i) => i >= index ? i + 1 : i;
778
+ }
779
+ /** The {@link IndexMap} for swapping the items at `a` and `b`. */
780
+ function swapAt(a, b) {
781
+ return (i) => i === a ? b : i === b ? a : i;
782
+ }
783
+ /** The {@link IndexMap} for moving the item at `from` to `to`, shifting the items between. */
784
+ function moveAt(from, to) {
785
+ return (i) => {
786
+ if (i === from) return to;
787
+ if (from < to) return i > from && i <= to ? i - 1 : i;
788
+ return i >= to && i < from ? i + 1 : i;
789
+ };
790
+ }
791
+ /** The {@link IndexMap} for replacing the item at `index` in place: drops only that index's state. */
792
+ function replaceAt$1(index) {
793
+ return (i) => i === index ? null : i;
794
+ }
795
+ /** The {@link IndexMap} for clearing every item in the array: drops all per-index state. */
796
+ const clearAll = () => null;
797
+ /**
798
+ * Remap `path`'s index segment immediately under `arrayPath` through `map`.
799
+ * Paths outside `arrayPath`, and `arrayPath` itself (a whole-array message),
800
+ * pass through unchanged; a path whose index segment maps to `null` is
801
+ * dropped (returns `null`) — its item no longer exists. Pure.
802
+ */
803
+ function remapIndex(path, arrayPath, map) {
804
+ if (!isUnder(path, arrayPath) || path.length === arrayPath.length) return path;
805
+ const segment = path[arrayPath.length];
806
+ if (typeof segment !== "number") return path;
807
+ const mapped = map(segment);
808
+ if (mapped === null) return null;
809
+ if (mapped === segment) return path;
810
+ return [
811
+ ...path.slice(0, arrayPath.length),
812
+ mapped,
813
+ ...path.slice(arrayPath.length + 1)
814
+ ];
815
+ }
816
+ /**
817
+ * Remap every entry of a flat, path-keyed record (a {@link toString} key per
818
+ * entry — e.g. touched flags) through {@link remapIndex}. Entries whose path
819
+ * maps to `null` are dropped. Pure.
820
+ */
821
+ function remapKeyedRecord(record, arrayPath, map) {
822
+ const out = {};
823
+ for (const key of Object.keys(record)) {
824
+ const remapped = remapIndex(fromString(key), arrayPath, map);
825
+ if (remapped !== null) out[toString(remapped)] = record[key];
826
+ }
827
+ return out;
828
+ }
829
+ //#endregion
830
+ //#region src/Errors.ts
831
+ var Errors_exports = /* @__PURE__ */ __exportAll({
832
+ ROOT_ERROR_KEY: () => ROOT_ERROR_KEY,
833
+ clearSourceAt: () => clearSourceAt,
834
+ emptySourced: () => emptySourced,
835
+ flattenErrors: () => flattenErrors,
836
+ getError: () => getError,
837
+ hasErrors: () => hasErrors,
838
+ issuesToErrors: () => issuesToErrors,
839
+ mergeErrors: () => mergeErrors,
840
+ mergeSourced: () => mergeSourced,
841
+ noErrors: () => noErrors,
842
+ remapIndices: () => remapIndices,
843
+ replaceAt: () => replaceAt,
844
+ setError: () => setError,
845
+ setSourceAt: () => setSourceAt
846
+ });
847
+ /** The root key under which a branch's own (non-field) messages are stored. */
848
+ const ROOT_ERROR_KEY = "_errors";
849
+ const isMessageList = (value) => Array.isArray(value) && value.every((v) => typeof v === "string");
850
+ const ownErrors = (node) => {
851
+ const existing = node[ROOT_ERROR_KEY];
852
+ if (isMessageList(existing)) return existing;
853
+ const created = [];
854
+ node[ROOT_ERROR_KEY] = created;
855
+ return created;
856
+ };
857
+ /**
858
+ * If `node[key]` is a leaf's message list, convert it in place into a branch
859
+ * carrying those same messages as its own {@link ROOT_ERROR_KEY} (a path used
860
+ * as a leaf is now needed as a branch too) — a no-op when `node[key]` is
861
+ * already a branch or absent. Shared by {@link insert} and {@link setLeaf}'s
862
+ * path-descent, the one piece of their branch-walking that's identical.
863
+ */
864
+ const promoteLeafToBranch = (node, key) => {
865
+ const existing = node[key];
866
+ if (isMessageList(existing)) node[key] = { [ROOT_ERROR_KEY]: existing };
867
+ };
868
+ /**
869
+ * Insert one message at `path` into a mutable error tree. A leaf becomes a
870
+ * `string[]`; if the same path is later used as a branch the existing list is
871
+ * preserved under {@link ROOT_ERROR_KEY}; if a branch later receives its own
872
+ * message it lands under {@link ROOT_ERROR_KEY}. The empty path targets the
873
+ * tree root's {@link ROOT_ERROR_KEY}.
874
+ */
875
+ const insert = (root, path, message) => {
876
+ if (path.length === 0) {
877
+ ownErrors(root).push(message);
878
+ return;
879
+ }
880
+ let node = root;
881
+ for (let i = 0; i < path.length - 1; i++) {
882
+ const key = String(path[i]);
883
+ if (node[key] === void 0) node[key] = {};
884
+ else promoteLeafToBranch(node, key);
885
+ node = node[key];
886
+ }
887
+ const last = String(path[path.length - 1]);
888
+ const existing = node[last];
889
+ if (existing === void 0) node[last] = [message];
890
+ else if (isMessageList(existing)) existing.push(message);
891
+ else ownErrors(existing).push(message);
892
+ };
893
+ /** Build a nested {@link FormErrors} tree from a flat list of {@link FormIssue}s. */
894
+ function issuesToErrors(issues) {
895
+ const root = {};
896
+ for (const issue of issues) insert(root, issue.path, issue.message);
897
+ return root;
898
+ }
899
+ /**
900
+ * Flatten a {@link FormErrors} tree back into a list of {@link FormIssue}s, in
901
+ * depth-first order. The inverse of {@link issuesToErrors} (modulo message
902
+ * ordering within a single field).
903
+ */
904
+ function flattenErrors(errors) {
905
+ const out = [];
906
+ const walk = (node, path) => {
907
+ for (const key of Object.keys(node)) {
908
+ const value = node[key];
909
+ if (value === void 0) continue;
910
+ if (key === "_errors") {
911
+ for (const message of value) out.push({
912
+ path,
913
+ message
914
+ });
915
+ continue;
916
+ }
917
+ const segment = /^\d+$/.test(key) ? Number(key) : key;
918
+ if (isMessageList(value)) for (const message of value) out.push({
919
+ path: [...path, segment],
920
+ message
921
+ });
922
+ else walk(value, [...path, segment]);
923
+ }
924
+ };
925
+ walk(errors, []);
926
+ return out;
927
+ }
928
+ /** Whether the tree carries any message anywhere. */
929
+ function hasErrors(errors) {
930
+ return flattenErrors(errors).length > 0;
931
+ }
932
+ /**
933
+ * Read the messages attached directly to `path` (a leaf's list, or a branch's
934
+ * own {@link ROOT_ERROR_KEY}). Returns `undefined` when the path has no direct
935
+ * messages.
936
+ */
937
+ function getError(errors, path) {
938
+ let node = errors;
939
+ for (const segment of path) {
940
+ if (node === void 0 || isMessageList(node)) return void 0;
941
+ node = node[String(segment)];
942
+ }
943
+ if (node === void 0) return void 0;
944
+ if (isMessageList(node)) return node;
945
+ return node[ROOT_ERROR_KEY];
946
+ }
947
+ /** Return a new tree with `message` added at `path`. Pure. */
948
+ function setError(errors, path, message) {
949
+ const next = structuredClone(errors);
950
+ insert(next, path, message);
951
+ return next;
952
+ }
953
+ /** Combine two error trees into one (left then right). Pure. */
954
+ function mergeErrors(a, b) {
955
+ return issuesToErrors([...flattenErrors(a), ...flattenErrors(b)]);
956
+ }
957
+ /** The empty error tree. */
958
+ const noErrors = {};
959
+ /**
960
+ * Remap the array-index segments of every message under `arrayPath` through
961
+ * `map` (see {@link module:effect-forms/Path.IndexMap}), so an error recorded
962
+ * against a row follows that row through a remove / insert / swap / move.
963
+ * Messages whose index maps to `null` are dropped. Pure.
964
+ */
965
+ function remapIndices(errors, arrayPath, map) {
966
+ const remapped = [];
967
+ for (const issue of flattenErrors(errors)) {
968
+ const path = remapIndex(issue.path, arrayPath, map);
969
+ if (path !== null) remapped.push({
970
+ path,
971
+ message: issue.message
972
+ });
973
+ }
974
+ return issuesToErrors(remapped);
975
+ }
976
+ /**
977
+ * Return a new tree with the messages *directly at* `path` replaced by
978
+ * `messages` (removing the key when `messages` is empty), leaving every other
979
+ * path — including children of a branch `path` — untouched. Unlike
980
+ * {@link setError} (which appends one message), this is the "this field's
981
+ * validator just ran, here is its complete result" operation a per-field
982
+ * validator needs: it can clear a field back to valid without disturbing
983
+ * sibling or nested messages. Pure.
984
+ */
985
+ function replaceAt(errors, path, messages) {
986
+ const next = structuredClone(errors);
987
+ setLeaf(next, path, messages);
988
+ return next;
989
+ }
990
+ const setLeaf = (root, path, messages) => {
991
+ if (path.length === 0) {
992
+ setOwnMessages(root, messages);
993
+ return;
994
+ }
995
+ const parent = descendClearing(root, path, messages);
996
+ if (parent === void 0) return;
997
+ setLeafMessages(parent, String(path[path.length - 1]), messages);
998
+ };
999
+ /** Set or clear a node's own {@link ROOT_ERROR_KEY} messages. */
1000
+ const setOwnMessages = (node, messages) => {
1001
+ if (messages.length === 0) delete node[ROOT_ERROR_KEY];
1002
+ else node[ROOT_ERROR_KEY] = [...messages];
1003
+ };
1004
+ /**
1005
+ * Walk to `path`'s parent branch, creating branches as needed. When
1006
+ * `messages` is empty (a clear, not a set) a missing branch means there is
1007
+ * nothing to clear — descent stops early and returns `undefined` rather than
1008
+ * creating empty branches just to immediately no-op inside them.
1009
+ */
1010
+ const descendClearing = (root, path, messages) => {
1011
+ let node = root;
1012
+ for (let i = 0; i < path.length - 1; i++) {
1013
+ const key = String(path[i]);
1014
+ if (node[key] === void 0) {
1015
+ if (messages.length === 0) return void 0;
1016
+ node[key] = {};
1017
+ } else promoteLeafToBranch(node, key);
1018
+ node = node[key];
1019
+ }
1020
+ return node;
1021
+ };
1022
+ /** Set or clear `node[key]`'s messages — a leaf's list, or (if it's already a branch) its own {@link ROOT_ERROR_KEY}. */
1023
+ const setLeafMessages = (node, key, messages) => {
1024
+ const existing = node[key];
1025
+ if (existing === void 0) {
1026
+ if (messages.length > 0) node[key] = [...messages];
1027
+ return;
1028
+ }
1029
+ if (isMessageList(existing)) {
1030
+ if (messages.length === 0) delete node[key];
1031
+ else node[key] = [...messages];
1032
+ return;
1033
+ }
1034
+ if (messages.length === 0) delete existing[ROOT_ERROR_KEY];
1035
+ else existing[ROOT_ERROR_KEY] = [...messages];
1036
+ };
1037
+ /** Both source trees empty. */
1038
+ const emptySourced = {
1039
+ client: noErrors,
1040
+ server: noErrors
1041
+ };
1042
+ /** Combine both source trees into the single tree a UI reads (server first, then client). */
1043
+ function mergeSourced(sourced) {
1044
+ return mergeErrors(sourced.server, sourced.client);
1045
+ }
1046
+ /**
1047
+ * Return new {@link SourcedErrors} with `source`'s tree updated at `path` via
1048
+ * {@link replaceAt} (the other source's tree is untouched). Pure.
1049
+ */
1050
+ function setSourceAt(sourced, source, path, messages) {
1051
+ return {
1052
+ ...sourced,
1053
+ [source]: replaceAt(sourced[source], path, messages)
1054
+ };
1055
+ }
1056
+ /** Clear `source`'s messages at `path` (and nowhere else). Shorthand for {@link setSourceAt} with `[]`. */
1057
+ function clearSourceAt(sourced, source, path) {
1058
+ return setSourceAt(sourced, source, path, []);
1059
+ }
1060
+ //#endregion
1061
+ //#region src/Field.ts
1062
+ var Field_exports = /* @__PURE__ */ __exportAll({
1063
+ defaultEncodedValues: () => defaultEncodedValues,
1064
+ defaultItemAt: () => defaultItemAt,
1065
+ defaultValue: () => defaultValue,
1066
+ isArrayFieldDef: () => isArrayFieldDef,
1067
+ isFieldDef: () => isFieldDef,
1068
+ makeArrayField: () => makeArrayField,
1069
+ makeField: () => makeField,
1070
+ schemaAt: () => schemaAt
1071
+ });
1072
+ /** Whether `def` is an {@link ArrayFieldDef}. */
1073
+ const isArrayFieldDef = (def) => def._tag === "array";
1074
+ /** Whether `def` is a scalar {@link FieldDef}. */
1075
+ const isFieldDef = (def) => def._tag === "field";
1076
+ /** Construct a scalar {@link FieldDef}. */
1077
+ const makeField = (key, schema) => ({
1078
+ _tag: "field",
1079
+ key,
1080
+ schema
1081
+ });
1082
+ /** Construct an {@link ArrayFieldDef}. */
1083
+ const makeArrayField = (key, itemSchema) => ({
1084
+ _tag: "array",
1085
+ key,
1086
+ itemSchema
1087
+ });
1088
+ /**
1089
+ * Derive a blank *encoded* default from a schema AST: a struct recurses into
1090
+ * its properties, an array seeds to `[]`, and any scalar seeds to `""` (an
1091
+ * HTML input's empty value). Conservative by design — a value that will not
1092
+ * decode cleanly is still a valid *starting* value the user edits.
1093
+ */
1094
+ function defaultValue(schema) {
1095
+ return fromAst(schema.ast);
1096
+ }
1097
+ /**
1098
+ * Unwrap a union to its first structured (struct / array) member — an
1099
+ * `optional` / `NullOr` wrapper (`Struct | undefined` / `Struct | null`)
1100
+ * still resolves to the wrapped struct or array's AST rather than a scalar
1101
+ * fallback. Non-unions, and unions with no structured member, pass through.
1102
+ */
1103
+ function unwrapToStructured(ast) {
1104
+ if (!SchemaAST.isUnion(ast)) return ast;
1105
+ for (const member of ast.types) {
1106
+ const inner = unwrapToStructured(member);
1107
+ if (SchemaAST.isObjects(inner) || SchemaAST.isArrays(inner)) return inner;
1108
+ }
1109
+ return ast;
1110
+ }
1111
+ function fromAst(ast) {
1112
+ const unwrapped = unwrapToStructured(ast);
1113
+ if (SchemaAST.isObjects(unwrapped)) {
1114
+ const out = {};
1115
+ for (const ps of unwrapped.propertySignatures) out[String(ps.name)] = fromAst(ps.type);
1116
+ return out;
1117
+ }
1118
+ if (SchemaAST.isArrays(unwrapped)) return [];
1119
+ return "";
1120
+ }
1121
+ /** The item AST of an {@link SchemaAST.Arrays} node: the rest (spread) type, or the first tuple element. */
1122
+ function arrayElementAst(ast) {
1123
+ return ast.rest[0] ?? ast.elements[0];
1124
+ }
1125
+ /** Descend one array-index path segment from an (already-unwrapped) array AST. */
1126
+ function stepIntoArray(ast) {
1127
+ if (!SchemaAST.isArrays(ast)) return void 0;
1128
+ const item = arrayElementAst(ast);
1129
+ return item === void 0 ? void 0 : unwrapToStructured(item);
1130
+ }
1131
+ /** Descend one struct-property path segment from an (already-unwrapped) struct AST. */
1132
+ function stepIntoStruct(ast, key) {
1133
+ if (!SchemaAST.isObjects(ast)) return void 0;
1134
+ const ps = ast.propertySignatures.find((p) => String(p.name) === key);
1135
+ return ps === void 0 ? void 0 : unwrapToStructured(ps.type);
1136
+ }
1137
+ /**
1138
+ * Walk `schema`'s AST along `path` — through struct properties and array
1139
+ * elements alike — and return the AST found there, or `undefined` if `path`
1140
+ * doesn't resolve (a missing property, or a non-array indexed). Unlike a
1141
+ * {@link FieldsRecord} lookup, this works for any nesting depth and for
1142
+ * schemas built directly with `Form.make` (no builder involved).
1143
+ */
1144
+ function schemaAt(schema, path) {
1145
+ let ast = unwrapToStructured(schema.ast);
1146
+ for (const segment of path) {
1147
+ if (ast === void 0) return void 0;
1148
+ ast = typeof segment === "number" ? stepIntoArray(ast) : stepIntoStruct(ast, segment);
1149
+ }
1150
+ return ast;
1151
+ }
1152
+ /**
1153
+ * Derive a blank encoded item for the array field at `path` (any nesting
1154
+ * depth) by walking `schema`'s AST there — the nested-array counterpart to
1155
+ * {@link defaultValue}. Returns `""` when `path` doesn't resolve to an array.
1156
+ */
1157
+ function defaultItemAt(schema, path) {
1158
+ const ast = schemaAt(schema, path);
1159
+ if (ast === void 0 || !SchemaAST.isArrays(ast)) return "";
1160
+ const item = arrayElementAst(ast);
1161
+ return item === void 0 ? "" : fromAst(item);
1162
+ }
1163
+ /** Seed a blank encoded value for every field: `[]` for arrays, `""` otherwise. */
1164
+ function defaultEncodedValues(fields) {
1165
+ const out = {};
1166
+ for (const [key, def] of Object.entries(fields)) out[key] = isArrayFieldDef(def) ? [] : defaultValue(def.schema);
1167
+ return out;
1168
+ }
1169
+ //#endregion
1170
+ //#region src/Issues.ts
1171
+ var Issues_exports = /* @__PURE__ */ __exportAll({
1172
+ defaultLeafFormatter: () => defaultLeafFormatter,
1173
+ filterOutputToFormIssues: () => filterOutputToFormIssues,
1174
+ schemaIssuesToFormIssues: () => schemaIssuesToFormIssues
1175
+ });
1176
+ /** The default leaf formatter — Effect Schema's own rendering of the leaf. */
1177
+ const defaultLeafFormatter = (issue) => String(issue);
1178
+ /**
1179
+ * Normalise a `Pointer.path` segment into a {@link FormPath} segment: integer
1180
+ * keys become numbers (array indices), everything else becomes a string.
1181
+ */
1182
+ const normaliseSegment = (segment) => {
1183
+ if (typeof segment === "number") return segment;
1184
+ if (typeof segment === "symbol") return segment.toString();
1185
+ return /^\d+$/.test(segment) ? Number(segment) : segment;
1186
+ };
1187
+ /**
1188
+ * Walk an Effect Schema issue tree, emitting one {@link FormIssue} per leaf
1189
+ * failure. Path segments accumulate through `Pointer` nodes; `Composite` and
1190
+ * `AnyOf` fan out to siblings; `Filter` / `Encoding` are transparent
1191
+ * wrappers. An `AnyOf` with no recorded member failures (a bare union
1192
+ * mismatch) is itself a leaf.
1193
+ */
1194
+ function schemaIssuesToFormIssues(issue, format = defaultLeafFormatter) {
1195
+ const out = [];
1196
+ const walk = (current, path) => {
1197
+ switch (current._tag) {
1198
+ case "Pointer":
1199
+ walk(current.issue, [...path, ...current.path.map(normaliseSegment)]);
1200
+ return;
1201
+ case "Composite":
1202
+ for (const child of current.issues) walk(child, path);
1203
+ return;
1204
+ case "AnyOf":
1205
+ if (current.issues.length === 0) {
1206
+ out.push({
1207
+ path,
1208
+ message: format(current)
1209
+ });
1210
+ return;
1211
+ }
1212
+ for (const child of current.issues) walk(child, path);
1213
+ return;
1214
+ case "Filter":
1215
+ case "Encoding":
1216
+ walk(current.issue, path);
1217
+ return;
1218
+ default:
1219
+ out.push({
1220
+ path,
1221
+ message: format(current)
1222
+ });
1223
+ return;
1224
+ }
1225
+ };
1226
+ walk(issue, []);
1227
+ return out;
1228
+ }
1229
+ /**
1230
+ * Normalise a filter predicate's {@link Schema.FilterOutput} into a flat list of
1231
+ * {@link FormIssue}s (empty on success). Shared by whole-form refinements
1232
+ * ({@link module:effect-forms/FormBuilder}) and per-field validators
1233
+ * ({@link module:effect-forms/Validate}) so both fold into the same nested
1234
+ * error tree as schema-decode failures.
1235
+ */
1236
+ function filterOutputToFormIssues(output) {
1237
+ if (output === void 0 || output === true) return [];
1238
+ if (output === false) return [{
1239
+ path: [],
1240
+ message: "Invalid value"
1241
+ }];
1242
+ if (Array.isArray(output)) return output.flatMap(filterIssueToFormIssues);
1243
+ return filterIssueToFormIssues(output);
1244
+ }
1245
+ function filterIssueToFormIssues(issue) {
1246
+ if (typeof issue === "string") return [{
1247
+ path: [],
1248
+ message: issue
1249
+ }];
1250
+ if ("path" in issue && "issue" in issue) {
1251
+ const prefix = issue.path.map((s) => typeof s === "symbol" ? s.toString() : s);
1252
+ return (typeof issue.issue === "string" ? [{
1253
+ path: [],
1254
+ message: issue.issue
1255
+ }] : schemaIssuesToFormIssues(issue.issue)).map((i) => ({
1256
+ path: [...prefix, ...i.path],
1257
+ message: i.message
1258
+ }));
1259
+ }
1260
+ return schemaIssuesToFormIssues(issue);
1261
+ }
1262
+ //#endregion
1263
+ //#region src/FormBuilder.ts
1264
+ /**
1265
+ * @copyright Thomas Foster
1266
+ *
1267
+ * Compose a form schema one field at a time, with cross-field refinements.
1268
+ *
1269
+ * {@link make} (aliased {@link empty}) starts an empty builder;
1270
+ * {@link FormBuilder.addField} adds a scalar or array field, {@link
1271
+ * FormBuilder.merge} folds another builder in, and {@link FormBuilder.refine} /
1272
+ * {@link FormBuilder.refineEffect} attach cross-field validations. The point is
1273
+ * incremental composition — assemble a large form from small, reusable
1274
+ * field-group builders — plus a home for whole-form rules (`password ===
1275
+ * confirmPassword`) that a single field schema can't express.
1276
+ *
1277
+ * {@link buildSchema} collapses the accumulated fields into an ordinary
1278
+ * `Schema.Struct` (array fields become `Schema.Array(item)`) and applies every
1279
+ * *synchronous* refinement as a struct-level `check`, so the result decodes,
1280
+ * derives constraints, and validates exactly like a hand-written schema — hand
1281
+ * it straight to {@link module:effect-forms/Form}'s `make`. *Asynchronous*
1282
+ * refinements can't be schema checks (Effect Schema has no effectful filter),
1283
+ * so they're carried on the builder and run by the Effect-returning validate
1284
+ * path — {@link asyncRefinements} exposes them and {@link runAsyncRefinements}
1285
+ * runs them into a {@link FormIssue} list.
1286
+ *
1287
+ * @module effect-forms/FormBuilder
1288
+ */
1289
+ var FormBuilder_exports = /* @__PURE__ */ __exportAll({
1290
+ TypeId: () => TypeId,
1291
+ asyncRefinements: () => asyncRefinements,
1292
+ buildSchema: () => buildSchema,
1293
+ empty: () => empty,
1294
+ filterOutputToFormIssues: () => filterOutputToFormIssues,
1295
+ isFormBuilder: () => isFormBuilder,
1296
+ make: () => make$2,
1297
+ runAsyncRefinements: () => runAsyncRefinements
1298
+ });
1299
+ /** Brand for a {@link FormBuilder}. */
1300
+ const TypeId = Symbol.for("@thomasfosterau/effect-forms/FormBuilder");
1301
+ const proto = {
1302
+ [TypeId]: TypeId,
1303
+ addField(keyOrField, schema) {
1304
+ const field = typeof keyOrField === "string" ? makeField(keyOrField, schema) : keyOrField;
1305
+ return build({
1306
+ ...this.fields,
1307
+ [field.key]: field
1308
+ }, this.refinements);
1309
+ },
1310
+ merge(other) {
1311
+ return build({
1312
+ ...this.fields,
1313
+ ...other.fields
1314
+ }, [...this.refinements, ...other.refinements]);
1315
+ },
1316
+ refine(predicate) {
1317
+ return build(this.fields, [...this.refinements, {
1318
+ _tag: "sync",
1319
+ fn: predicate
1320
+ }]);
1321
+ },
1322
+ refineEffect(predicate) {
1323
+ return build(this.fields, [...this.refinements, {
1324
+ _tag: "async",
1325
+ fn: predicate
1326
+ }]);
1327
+ }
1328
+ };
1329
+ function build(fields, refinements) {
1330
+ const self = Object.create(proto);
1331
+ self.fields = fields;
1332
+ self.refinements = refinements;
1333
+ return self;
1334
+ }
1335
+ /** Whether `u` is a {@link FormBuilder}. */
1336
+ const isFormBuilder = (u) => typeof u === "object" && u !== null && TypeId in u;
1337
+ /** The empty builder — the starting point for {@link FormBuilder.addField}. */
1338
+ const make$2 = build({}, []);
1339
+ /** Alias of {@link make} — the empty builder. */
1340
+ const empty = make$2;
1341
+ /**
1342
+ * Collapse a builder into an Effect Schema: a `Schema.Struct` of the fields
1343
+ * (array fields as `Schema.Array(item)`) with every synchronous refinement
1344
+ * applied as a struct-level check. Ready for {@link module:effect-forms/Form}.
1345
+ */
1346
+ function buildSchema(self) {
1347
+ const structFields = {};
1348
+ for (const [key, def] of Object.entries(self.fields)) if (isArrayFieldDef(def)) structFields[key] = Schema.Array(def.itemSchema);
1349
+ else if (isFieldDef(def)) structFields[key] = def.schema;
1350
+ let schema = Schema.Struct(structFields);
1351
+ for (const refinement of self.refinements) if (refinement._tag === "sync") schema = schema.check(Schema.makeFilter(refinement.fn));
1352
+ return schema;
1353
+ }
1354
+ /** The asynchronous refinements carried on a builder, as bare predicates. */
1355
+ function asyncRefinements(self) {
1356
+ return self.refinements.filter((r) => r._tag === "async").map((r) => r.fn);
1357
+ }
1358
+ /**
1359
+ * Run every asynchronous refinement on `self` against `values` and collect
1360
+ * their failures as {@link FormIssue}s (empty when all pass). Runs them
1361
+ * concurrently; requirements propagate on the `R` channel.
1362
+ */
1363
+ function runAsyncRefinements(self, values) {
1364
+ const refinements = asyncRefinements(self);
1365
+ if (refinements.length === 0) return Effect.succeed([]);
1366
+ return Effect.forEach(refinements, (fn) => fn(values), { concurrency: "unbounded" }).pipe(Effect.map((outputs) => outputs.flatMap(filterOutputToFormIssues)));
1367
+ }
1368
+ //#endregion
1369
+ //#region src/State.ts
1370
+ /**
1371
+ * @copyright Thomas Foster
1372
+ *
1373
+ * {@link FormState} — the serialisable snapshot of a form.
1374
+ *
1375
+ * This is the single object a
1376
+ * server produces (validate a submission, or seed an empty form) and hands to
1377
+ * the client to hydrate from. It is plain JSON so it survives the SSR → client
1378
+ * boundary, and {@link FormStateSchema} gives it an Effect Schema so it can be
1379
+ * decoded back on arrival.
1380
+ *
1381
+ * @module effect-forms/State
1382
+ */
1383
+ var State_exports = /* @__PURE__ */ __exportAll({
1384
+ FormStateSchema: () => FormStateSchema,
1385
+ isValid: () => isValid,
1386
+ make: () => make$1,
1387
+ withError: () => withError,
1388
+ withMessage: () => withMessage
1389
+ });
1390
+ /** Build a {@link FormState} from data plus overrides. `valid` defaults to "no errors". */
1391
+ function make$1(data, options = {}) {
1392
+ const errors = options.errors ?? noErrors;
1393
+ return {
1394
+ id: options.id ?? "default",
1395
+ valid: options.valid ?? !hasErrors(errors),
1396
+ posted: options.posted ?? false,
1397
+ data,
1398
+ errors,
1399
+ ...options.message !== void 0 ? { message: options.message } : {},
1400
+ ...options.constraints !== void 0 ? { constraints: options.constraints } : {}
1401
+ };
1402
+ }
1403
+ /** Return a new state carrying `message` (and not otherwise changed). */
1404
+ function withMessage(state, message) {
1405
+ return {
1406
+ ...state,
1407
+ message
1408
+ };
1409
+ }
1410
+ /**
1411
+ * Return a new state with `message` added at `path` and `valid` forced to
1412
+ * `false` — for attaching a
1413
+ * server-side validation failure to a specific field after the schema passed.
1414
+ */
1415
+ function withError(state, path, message) {
1416
+ return {
1417
+ ...state,
1418
+ valid: false,
1419
+ errors: setError(state.errors, path, message)
1420
+ };
1421
+ }
1422
+ /** Whether the state is valid. */
1423
+ const isValid = (state) => state.valid;
1424
+ /** Effect Schema describing the JSON shape of {@link FormState} for transport. */
1425
+ function FormStateSchema(data) {
1426
+ return Schema.Struct({
1427
+ id: Schema.String,
1428
+ valid: Schema.Boolean,
1429
+ posted: Schema.Boolean,
1430
+ data,
1431
+ errors: Schema.Record(Schema.String, Schema.Unknown),
1432
+ message: Schema.optional(Schema.Unknown),
1433
+ constraints: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
1434
+ });
1435
+ }
1436
+ //#endregion
1437
+ //#region src/Form.ts
1438
+ /**
1439
+ * @copyright Thomas Foster
1440
+ *
1441
+ * {@link make} — the core entry point: bind an Effect Schema to a form and
1442
+ * get back a validator and an empty-state factory.
1443
+ *
1444
+ * This is the framework-neutral validate-and-seed entry point,
1445
+ * expressed as a small object bound to one schema:
1446
+ *
1447
+ * - `validate(input)` parses an HTML form body (or a plain object) and decodes
1448
+ * it through the schema, returning a {@link FormState} — valid with the
1449
+ * decoded data, or invalid with the nested {@link FormErrors} and the posted
1450
+ * values preserved so the form re-renders what the user typed. It is an
1451
+ * `Effect`, so a schema requiring decoding services composes naturally and
1452
+ * the caller stays on the typed error channel — there is none; validation
1453
+ * failures live in the returned state, not the error channel.
1454
+ * - `empty()` seeds an unposted, invalid-by-default form from the supplied
1455
+ * defaults — what a `load` returns to render a blank create form.
1456
+ *
1457
+ * Coercion (strings → numbers / booleans / dates) is the schema's job: build
1458
+ * the schema from coercing leaves (`Schema.FiniteFromString`, the
1459
+ * {@link module:effect-forms/Coerce} helpers, …) so a multipart body decodes
1460
+ * cleanly.
1461
+ *
1462
+ * @module effect-forms/Form
1463
+ */
1464
+ var Form_exports = /* @__PURE__ */ __exportAll({
1465
+ fromBuilder: () => fromBuilder,
1466
+ make: () => make
1467
+ });
1468
+ const toStructured = (input) => {
1469
+ if (input === null || input === void 0) return Effect.succeed(null);
1470
+ if (input instanceof Request) return Effect.promise(() => requestToObject(input));
1471
+ if (input instanceof FormData || input instanceof URLSearchParams) return Effect.succeed(formDataToObject(input));
1472
+ return Effect.succeed(input);
1473
+ };
1474
+ /**
1475
+ * Bind `schema` to a form. See the module docs for the contract. The returned
1476
+ * {@link Form} is stateless and reusable across requests.
1477
+ */
1478
+ function make(schema, options) {
1479
+ const id = options.id ?? "default";
1480
+ const { defaults, leafFormatter } = options;
1481
+ const constraints = options.deriveConstraints === false ? options.constraints ?? {} : constraintsFromSchema(schema, options.constraints ?? {});
1482
+ const decode = SchemaParser.decodeUnknownEffect(schema, { errors: "all" });
1483
+ const empty = (overrides = {}) => make$1(defaults, {
1484
+ id,
1485
+ valid: false,
1486
+ posted: false,
1487
+ constraints,
1488
+ ...overrides
1489
+ });
1490
+ const parse = (input) => input instanceof FormData || input instanceof URLSearchParams ? formDataToObject(input) : input;
1491
+ const validate = (input) => toStructured(input).pipe(Effect.flatMap((object) => {
1492
+ if (object === null) return Effect.succeed(empty());
1493
+ return decode(object).pipe(Effect.matchEffect({
1494
+ onSuccess: (data) => Effect.succeed(make$1(data, {
1495
+ id,
1496
+ valid: true,
1497
+ posted: true,
1498
+ constraints
1499
+ })),
1500
+ onFailure: (issue) => Effect.succeed(make$1({
1501
+ ...defaults,
1502
+ ...object
1503
+ }, {
1504
+ id,
1505
+ valid: false,
1506
+ posted: true,
1507
+ constraints,
1508
+ errors: issuesToErrors(schemaIssuesToFormIssues(issue, leafFormatter))
1509
+ }))
1510
+ }));
1511
+ }));
1512
+ return {
1513
+ schema,
1514
+ id,
1515
+ constraints,
1516
+ empty,
1517
+ parse,
1518
+ validate
1519
+ };
1520
+ }
1521
+ /**
1522
+ * Bind a {@link FormBuilder} to a {@link Form}. Collapses the builder to a
1523
+ * schema (see {@link buildSchema}), then behaves like {@link make} — except its
1524
+ * `validate` also runs the builder's *asynchronous* refinements after a
1525
+ * successful decode, folding any failures into the same nested error tree. When
1526
+ * `defaults` is omitted it is derived from the fields (blank encoded values).
1527
+ */
1528
+ function fromBuilder(builder, options = {}) {
1529
+ const schema = buildSchema(builder);
1530
+ const defaults = options.defaults ?? defaultEncodedValues(builder.fields);
1531
+ const base = make(schema, {
1532
+ ...options,
1533
+ defaults
1534
+ });
1535
+ const decode = SchemaParser.decodeUnknownEffect(schema, { errors: "all" });
1536
+ const validate = (input) => toStructured(input).pipe(Effect.flatMap((object) => {
1537
+ if (object === null) return Effect.succeed(base.empty());
1538
+ const posted = {
1539
+ ...defaults,
1540
+ ...object
1541
+ };
1542
+ return decode(object).pipe(Effect.matchEffect({
1543
+ onSuccess: (data) => runAsyncRefinements(builder, data).pipe(Effect.map((issues) => issues.length === 0 ? make$1(data, {
1544
+ id: base.id,
1545
+ valid: true,
1546
+ posted: true,
1547
+ constraints: base.constraints
1548
+ }) : make$1(posted, {
1549
+ id: base.id,
1550
+ valid: false,
1551
+ posted: true,
1552
+ constraints: base.constraints,
1553
+ errors: issuesToErrors(issues)
1554
+ }))),
1555
+ onFailure: (issue) => Effect.succeed(make$1(posted, {
1556
+ id: base.id,
1557
+ valid: false,
1558
+ posted: true,
1559
+ constraints: base.constraints,
1560
+ errors: issuesToErrors(schemaIssuesToFormIssues(issue, options.leafFormatter))
1561
+ }))
1562
+ }));
1563
+ }));
1564
+ return {
1565
+ ...base,
1566
+ validate
1567
+ };
1568
+ }
1569
+ //#endregion
1570
+ //#region src/Mode.ts
1571
+ /**
1572
+ * @copyright Thomas Foster
1573
+ *
1574
+ * When a form validates (and, optionally, auto-submits).
1575
+ *
1576
+ * A reactive form has to decide *when* to run validation as the user types.
1577
+ * The three modes mirror the common form-library vocabulary:
1578
+ *
1579
+ * - **`onSubmit`** (the default) — validate only when the form is submitted (or
1580
+ * validation is triggered explicitly). The quietest mode.
1581
+ * - **`onBlur`** — validate a field when it loses focus.
1582
+ * - **`onChange`** — validate on every change, optionally `debounce`d.
1583
+ *
1584
+ * `onBlur` / `onChange` may also `autoSubmit`, turning the form into a
1585
+ * live-saving surface (a settings panel, a filter bar). This module is the
1586
+ * framework-neutral half: {@link parse} normalises the loose {@link FormMode}
1587
+ * a caller supplies into a fully-resolved {@link ParsedMode} the reactive
1588
+ * store drives from. The store owns the actual scheduling.
1589
+ *
1590
+ * A separate `revalidate` dimension governs what happens *after a submit*: an
1591
+ * `onSubmit`-mode form is otherwise silent until the next submit, which
1592
+ * leaves a failed post's errors frozen even after the user fixes the
1593
+ * offending field. `revalidate` says which trigger (`"onChange"` /
1594
+ * `"onBlur"` — independent of the primary `validation` trigger) re-validates
1595
+ * once the form has been posted at least once; `"never"` keeps the classic
1596
+ * frozen-until-resubmit behaviour.
1597
+ *
1598
+ * @module effect-forms/Mode
1599
+ */
1600
+ var Mode_exports = /* @__PURE__ */ __exportAll({ parse: () => parse });
1601
+ /**
1602
+ * Normalise a {@link FormMode} into a {@link ParsedMode}. An absent mode is
1603
+ * `onSubmit`. `debounce` is resolved to milliseconds and only applies in
1604
+ * `onChange`; `autoSubmit` is only honoured for `onBlur` / `onChange`;
1605
+ * `revalidate` defaults to `"onChange"` (TanStack's / RHF's default
1606
+ * post-submit behaviour) regardless of the primary `validation` mode.
1607
+ */
1608
+ function parse(mode) {
1609
+ const validation = mode?.validation ?? "onSubmit";
1610
+ const revalidate = mode?.revalidate ?? "onChange";
1611
+ if (validation === "onBlur") return {
1612
+ validation: "onBlur",
1613
+ debounce: null,
1614
+ autoSubmit: mode?.autoSubmit === true,
1615
+ revalidate
1616
+ };
1617
+ if (validation === "onChange") return {
1618
+ validation: "onChange",
1619
+ debounce: mode?.debounce === void 0 ? null : Duration.toMillis(mode.debounce),
1620
+ autoSubmit: mode?.autoSubmit === true,
1621
+ revalidate
1622
+ };
1623
+ return {
1624
+ validation: "onSubmit",
1625
+ debounce: null,
1626
+ autoSubmit: false,
1627
+ revalidate
1628
+ };
1629
+ }
1630
+ //#endregion
1631
+ //#region src/Result.ts
1632
+ var Result_exports = /* @__PURE__ */ __exportAll({
1633
+ errorResult: () => errorResult,
1634
+ failure: () => failure,
1635
+ redirect: () => redirect,
1636
+ success: () => success
1637
+ });
1638
+ /** Construct a {@link FormSuccess} (default status 200). */
1639
+ const success = (state, status = 200) => ({
1640
+ _tag: "Success",
1641
+ status,
1642
+ state
1643
+ });
1644
+ /** Construct a {@link FormFailure} (default status 400). */
1645
+ const failure = (state, status = 400) => ({
1646
+ _tag: "Failure",
1647
+ status,
1648
+ state
1649
+ });
1650
+ /** Construct a {@link FormRedirect} (default status 303). */
1651
+ const redirect = (location, status = 303) => ({
1652
+ _tag: "Redirect",
1653
+ status,
1654
+ location
1655
+ });
1656
+ /** Construct a {@link FormErrorResult} (default status 500). */
1657
+ const errorResult = (message, status = 500) => ({
1658
+ _tag: "Error",
1659
+ status,
1660
+ message
1661
+ });
1662
+ //#endregion
1663
+ //#region src/Validate.ts
1664
+ /**
1665
+ * @copyright Thomas Foster
1666
+ *
1667
+ * Low-level validation helpers — decode a value through a schema and get back
1668
+ * the {@link FormErrors} tree (or the list of {@link FormIssue}s) instead of a
1669
+ * raised failure. `Form.make` is the high-level entry; these are for ad
1670
+ * hoc field- or value-level validation (e.g. a Svelte client validating a
1671
+ * single field on blur).
1672
+ *
1673
+ * The second half of the module is per-field validators: {@link
1674
+ * SyncFieldValidator} / {@link AsyncFieldValidator} (bundled as {@link
1675
+ * FieldValidators}) are the framework-neutral shape a reactive store attaches
1676
+ * to one field, mirroring TanStack Form's `validators.onChange(Async)` /
1677
+ * `onBlur(Async)`. {@link runSyncFieldValidator} runs one synchronously;
1678
+ * {@link runAsyncFieldValidator} builds the debounced Effect an async one
1679
+ * runs as — the store is responsible for forking it into a fiber per field
1680
+ * and interrupting the previous fiber on every new run (see that function's
1681
+ * docs for why that alone gives debounce + "latest wins" cancellation).
1682
+ *
1683
+ * @module effect-forms/Validate
1684
+ */
1685
+ var Validate_exports = /* @__PURE__ */ __exportAll({
1686
+ resolveFieldTrigger: () => resolveFieldTrigger,
1687
+ runAsyncFieldValidator: () => runAsyncFieldValidator,
1688
+ runSyncFieldValidator: () => runSyncFieldValidator,
1689
+ validateValue: () => validateValue,
1690
+ validateValueSync: () => validateValueSync
1691
+ });
1692
+ /**
1693
+ * Decode `input` through `schema`, returning a {@link ValidationResult} on the
1694
+ * success channel (decode failures become `valid: false`, not an Effect
1695
+ * failure). Decoding-service requirements propagate on the `R` channel.
1696
+ */
1697
+ function validateValue(schema, input, format) {
1698
+ return SchemaParser.decodeUnknownEffect(schema, { errors: "all" })(input).pipe(Effect.matchEffect({
1699
+ onSuccess: (data) => Effect.succeed({
1700
+ valid: true,
1701
+ data
1702
+ }),
1703
+ onFailure: (issue) => {
1704
+ const issues = schemaIssuesToFormIssues(issue, format);
1705
+ return Effect.succeed({
1706
+ valid: false,
1707
+ errors: issuesToErrors(issues),
1708
+ issues
1709
+ });
1710
+ }
1711
+ }));
1712
+ }
1713
+ /**
1714
+ * Synchronous {@link validateValue} for schemas with no decoding-service
1715
+ * requirements (`R = never`) and no async decoding — the common case for plain
1716
+ * form schemas. {@link validateValue} never fails on the error channel, so
1717
+ * running it with {@link Effect.runSync} is total.
1718
+ */
1719
+ function validateValueSync(schema, input, format) {
1720
+ return Effect.runSync(validateValue(schema, input, format));
1721
+ }
1722
+ /**
1723
+ * Run a {@link SyncFieldValidator}, folding its {@link Schema.FilterOutput}
1724
+ * into {@link FormIssue}s (empty on success). Total — a sync field validator
1725
+ * cannot fail the Effect error channel.
1726
+ */
1727
+ function runSyncFieldValidator(validator, value) {
1728
+ return filterOutputToFormIssues(validator(value));
1729
+ }
1730
+ /**
1731
+ * Build the Effect an {@link AsyncFieldValidator} run is: sleep `debounceMs`
1732
+ * (a no-op at `0`, the default), then run `validator` and fold its output
1733
+ * into {@link FormIssue}s.
1734
+ *
1735
+ * The debounce is a plain {@link Effect.sleep}, so it rides Effect's `Clock`
1736
+ * — deterministic and instantly-advanceable under `TestClock` in tests, real
1737
+ * time otherwise (no `setTimeout` bookkeeping for a caller to manage).
1738
+ * "Latest wins" falls out of ordinary fiber interruption: a caller re-running
1739
+ * this on every keystroke forks a fresh fiber per call and interrupts the
1740
+ * previous one before forking the next — interrupting mid-sleep skips the
1741
+ * validator entirely (a true debounce, not just a discarded stale result),
1742
+ * and interrupting mid-validator aborts whatever the validator was doing
1743
+ * (e.g. an in-flight `fetch` wrapped as an Effect).
1744
+ */
1745
+ function runAsyncFieldValidator(validator, value, debounceMs = 0) {
1746
+ return Effect.gen(function* () {
1747
+ if (debounceMs > 0) yield* Effect.sleep(Duration.millis(debounceMs));
1748
+ return filterOutputToFormIssues(yield* validator(value));
1749
+ });
1750
+ }
1751
+ /**
1752
+ * Pick `validators`' sync/async pair for `trigger` (TanStack's `onChange` /
1753
+ * `onBlur` vs `onChangeAsync` / `onBlurAsync`). A reactive store calls this
1754
+ * once per trigger instead of repeating the trigger ternary at each call
1755
+ * site — the pure decision half of what {@link module:effect-forms-svelte/FormStore}'s
1756
+ * field-trigger dispatch runs.
1757
+ */
1758
+ function resolveFieldTrigger(validators, trigger) {
1759
+ return {
1760
+ sync: trigger === "change" ? validators?.onChange : validators?.onBlur,
1761
+ async: trigger === "change" ? validators?.onChangeAsync : validators?.onBlurAsync,
1762
+ asyncDebounceMs: validators?.asyncDebounceMs ?? 0
1763
+ };
1764
+ }
1765
+ //#endregion
1766
+ export { Coerce_exports as Coerce, Command_exports as Command, Constraints_exports as Constraints, Errors_exports as Errors, Field_exports as Field, Form_exports as Form, FormBuilder_exports as FormBuilder, FormData_exports as FormData, Issues_exports as Issues, Mode_exports as Mode, Path_exports as Path, Result_exports as Result, State_exports as State, Validate_exports as Validate };
1767
+
1768
+ //# sourceMappingURL=index.js.map