@formwright/schema 0.1.0 → 0.2.2
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/README.md +39 -6
- package/dist/index.cjs +21 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -3
- package/dist/index.d.ts +117 -3
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,53 @@
|
|
|
1
1
|
# @formwright/schema
|
|
2
2
|
|
|
3
|
-
Schema types and a dependency-free runtime validator for [Formwright](
|
|
3
|
+
> Schema types and a dependency-free runtime validator for [Formwright](https://github.com/aliarsalan177/formwright).
|
|
4
4
|
|
|
5
|
-
The schema is Formwright's public contract: plain, serializable data that both the
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
The schema is Formwright's public contract: plain, serializable data that both the runtime
|
|
6
|
+
renderer and the codegen compiler consume. This package gives you the TypeScript types plus
|
|
7
|
+
a validator that produces precise, path-addressed issues — ideal for repairing LLM-emitted
|
|
8
|
+
schemas before they reach the runtime.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm i @formwright/schema
|
|
12
|
+
```
|
|
9
13
|
|
|
10
14
|
```ts
|
|
11
15
|
import { validateSchema, parseSchema } from "@formwright/schema";
|
|
12
16
|
|
|
17
|
+
// Non-throwing: inspect issues
|
|
13
18
|
const result = validateSchema(unknownInput);
|
|
14
19
|
if (!result.ok) {
|
|
15
20
|
for (const issue of result.issues) console.warn(issue.path, issue.message);
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
//
|
|
23
|
+
// Or throw a single aggregated error on failure:
|
|
19
24
|
const schema = parseSchema(unknownInput);
|
|
20
25
|
```
|
|
26
|
+
|
|
27
|
+
## Types
|
|
28
|
+
|
|
29
|
+
`FormSchema`, `FieldSchema`, `FieldType`, `FieldOption`, `ValidationSchema`, `Condition`,
|
|
30
|
+
`SubmitSchema`, and more — the full, serializable shape of a form, including nested `group`
|
|
31
|
+
and `collection` fields and the JSONLogic-style `Condition` algebra.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import type { FormSchema } from "@formwright/schema";
|
|
35
|
+
|
|
36
|
+
const schema: FormSchema = {
|
|
37
|
+
id: "signup",
|
|
38
|
+
version: "1.0",
|
|
39
|
+
fields: [
|
|
40
|
+
{ id: "email", type: "email", validation: { kind: "string", format: "email", required: true } },
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Why a runtime validator
|
|
46
|
+
|
|
47
|
+
LLM-emitted schemas can be malformed. `validateSchema` checks the structure (ids present and
|
|
48
|
+
unique, container fields declared, etc.) and returns issues addressed by path
|
|
49
|
+
(`fields[2].type`), so you can repair or reject input before constructing a `Form`.
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -11,9 +11,16 @@ var BUILTIN_TYPES = /* @__PURE__ */ new Set([
|
|
|
11
11
|
"checkbox",
|
|
12
12
|
"radio",
|
|
13
13
|
"group",
|
|
14
|
-
"collection"
|
|
14
|
+
"collection",
|
|
15
|
+
"steps",
|
|
16
|
+
"step"
|
|
17
|
+
]);
|
|
18
|
+
var CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
19
|
+
"group",
|
|
20
|
+
"collection",
|
|
21
|
+
"steps",
|
|
22
|
+
"step"
|
|
15
23
|
]);
|
|
16
|
-
var CONTAINER_TYPES = /* @__PURE__ */ new Set(["group", "collection"]);
|
|
17
24
|
function isRecord(v) {
|
|
18
25
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
19
26
|
}
|
|
@@ -49,7 +56,18 @@ function validateField(field, path, seenIds, c) {
|
|
|
49
56
|
c.add(`${path}.fields`, `field of type "${type}" must declare a non-empty "fields" array`);
|
|
50
57
|
} else {
|
|
51
58
|
const childIds = /* @__PURE__ */ new Set();
|
|
52
|
-
nested.forEach((f, i) =>
|
|
59
|
+
nested.forEach((f, i) => {
|
|
60
|
+
if (type === "steps") {
|
|
61
|
+
const childType = isRecord(f) ? f["type"] : void 0;
|
|
62
|
+
if (childType !== "step") {
|
|
63
|
+
c.add(
|
|
64
|
+
`${path}.fields[${i}].type`,
|
|
65
|
+
`field of type "steps" must contain "step" children (got "${String(childType)}")`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
validateField(f, `${path}.fields[${i}]`, childIds, c);
|
|
70
|
+
});
|
|
53
71
|
}
|
|
54
72
|
}
|
|
55
73
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/validate.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Runtime validation of an unknown value into a typed {@link FormSchema}.\n *\n * Dependency-free and structural — its job is to give precise, path-addressed\n * errors so an LLM-emitted schema can be repaired (or rejected) before it ever\n * reaches the runtime. This validates the *schema shape*, not user form input\n * (that is the job of the per-field {@link ValidationSchema}).\n */\nimport type { FieldSchema, FieldType, FormSchema } from \"./types.js\";\n\nexport interface ValidationIssue {\n /** JSON-pointer-ish path to the offending node, e.g. `fields[2].type`. */\n readonly path: string;\n readonly message: string;\n}\n\nexport type ValidationResult =\n | { readonly ok: true; readonly value: FormSchema }\n | { readonly ok: false; readonly issues: readonly ValidationIssue[] };\n\nconst BUILTIN_TYPES: ReadonlySet<string> = new Set<FieldType>([\n \"text\",\n \"number\",\n \"email\",\n \"password\",\n \"textarea\",\n \"select\",\n \"checkbox\",\n \"radio\",\n \"group\",\n \"collection\",\n]);\n\nconst CONTAINER_TYPES: ReadonlySet<string> = new Set<FieldType>([\"group\", \"collection\"]);\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nclass Collector {\n readonly issues: ValidationIssue[] = [];\n add(path: string, message: string): void {\n this.issues.push({ path, message });\n }\n}\n\nfunction validateField(field: unknown, path: string, seenIds: Set<string>, c: Collector): void {\n if (!isRecord(field)) {\n c.add(path, \"field must be an object\");\n return;\n }\n\n const id = field[\"id\"];\n if (typeof id !== \"string\" || id.length === 0) {\n c.add(`${path}.id`, \"field.id must be a non-empty string\");\n } else if (seenIds.has(id)) {\n c.add(`${path}.id`, `duplicate field id \"${id}\"`);\n } else {\n seenIds.add(id);\n }\n\n const type = field[\"type\"];\n if (typeof type !== \"string\" || type.length === 0) {\n c.add(`${path}.type`, \"field.type must be a non-empty string\");\n } else if (!BUILTIN_TYPES.has(type)) {\n // Unknown types are allowed (custom widgets) but flagged as a hint, not an error.\n // We intentionally do not push an issue here.\n }\n\n if ((type === \"select\" || type === \"radio\") && field[\"options\"] === undefined) {\n c.add(`${path}.options`, `field of type \"${type}\" should declare options`);\n }\n\n // Containers (group/collection) must declare nested fields; recurse so nested\n // problems surface with a precise path (e.g. `fields[3].fields[0].id`).\n if (typeof type === \"string\" && CONTAINER_TYPES.has(type)) {\n const nested = field[\"fields\"];\n if (!Array.isArray(nested) || nested.length === 0) {\n c.add(`${path}.fields`, `field of type \"${type}\" must declare a non-empty \"fields\" array`);\n } else {\n // A container opens a new naming scope: child ids are unique within it, not globally.\n const childIds = new Set<string>();\n nested.forEach((f, i) => validateField(f, `${path}.fields[${i}]`, childIds, c));\n }\n }\n}\n\n/** Validate an unknown value as a {@link FormSchema}. Never throws. */\nexport function validateSchema(input: unknown): ValidationResult {\n const c = new Collector();\n\n if (!isRecord(input)) {\n c.add(\"$\", \"schema must be an object\");\n return { ok: false, issues: c.issues };\n }\n\n if (typeof input[\"id\"] !== \"string\" || (input[\"id\"] as string).length === 0) {\n c.add(\"id\", \"schema.id must be a non-empty string\");\n }\n if (typeof input[\"version\"] !== \"string\" || (input[\"version\"] as string).length === 0) {\n c.add(\"version\", \"schema.version must be a non-empty string\");\n }\n\n const fields = input[\"fields\"];\n if (!Array.isArray(fields)) {\n c.add(\"fields\", \"schema.fields must be an array\");\n } else if (fields.length === 0) {\n c.add(\"fields\", \"schema.fields must contain at least one field\");\n } else {\n const seenIds = new Set<string>();\n fields.forEach((f, i) => validateField(f, `fields[${i}]`, seenIds, c));\n }\n\n const providers = input[\"providers\"];\n if (providers !== undefined && !isRecord(providers)) {\n c.add(\"providers\", \"schema.providers must be an object when present\");\n }\n\n const submit = input[\"submit\"];\n if (submit !== undefined && !isRecord(submit)) {\n c.add(\"submit\", \"schema.submit must be an object when present\");\n }\n\n if (c.issues.length > 0) {\n return { ok: false, issues: c.issues };\n }\n return { ok: true, value: input as unknown as FormSchema };\n}\n\n/** Validate and throw a single aggregated error on failure. Convenience for trusted input. */\nexport function parseSchema(input: unknown): FormSchema {\n const result = validateSchema(input);\n if (!result.ok) {\n const detail = result.issues.map((i) => ` - ${i.path}: ${i.message}`).join(\"\\n\");\n throw new SchemaValidationError(`Invalid Formwright schema:\\n${detail}`, result.issues);\n }\n return result.value;\n}\n\nexport class SchemaValidationError extends Error {\n readonly issues: readonly ValidationIssue[];\n constructor(message: string, issues: readonly ValidationIssue[]) {\n super(message);\n this.name = \"SchemaValidationError\";\n this.issues = issues;\n }\n}\n\n/** Type guard form of {@link validateSchema}. */\nexport function isFormSchema(input: unknown): input is FormSchema {\n return validateSchema(input).ok;\n}\n\n/** Convenience: list the field ids declared by a schema, in order. */\nexport function fieldIds(schema: FormSchema): string[] {\n return schema.fields.map((f: FieldSchema) => f.id);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/validate.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Runtime validation of an unknown value into a typed {@link FormSchema}.\n *\n * Dependency-free and structural — its job is to give precise, path-addressed\n * errors so an LLM-emitted schema can be repaired (or rejected) before it ever\n * reaches the runtime. This validates the *schema shape*, not user form input\n * (that is the job of the per-field {@link ValidationSchema}).\n */\nimport type { FieldSchema, FieldType, FormSchema } from \"./types.js\";\n\nexport interface ValidationIssue {\n /** JSON-pointer-ish path to the offending node, e.g. `fields[2].type`. */\n readonly path: string;\n readonly message: string;\n}\n\nexport type ValidationResult =\n | { readonly ok: true; readonly value: FormSchema }\n | { readonly ok: false; readonly issues: readonly ValidationIssue[] };\n\nconst BUILTIN_TYPES: ReadonlySet<string> = new Set<FieldType>([\n \"text\",\n \"number\",\n \"email\",\n \"password\",\n \"textarea\",\n \"select\",\n \"checkbox\",\n \"radio\",\n \"group\",\n \"collection\",\n \"steps\",\n \"step\",\n]);\n\nconst CONTAINER_TYPES: ReadonlySet<string> = new Set<FieldType>([\n \"group\",\n \"collection\",\n \"steps\",\n \"step\",\n]);\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nclass Collector {\n readonly issues: ValidationIssue[] = [];\n add(path: string, message: string): void {\n this.issues.push({ path, message });\n }\n}\n\nfunction validateField(field: unknown, path: string, seenIds: Set<string>, c: Collector): void {\n if (!isRecord(field)) {\n c.add(path, \"field must be an object\");\n return;\n }\n\n const id = field[\"id\"];\n if (typeof id !== \"string\" || id.length === 0) {\n c.add(`${path}.id`, \"field.id must be a non-empty string\");\n } else if (seenIds.has(id)) {\n c.add(`${path}.id`, `duplicate field id \"${id}\"`);\n } else {\n seenIds.add(id);\n }\n\n const type = field[\"type\"];\n if (typeof type !== \"string\" || type.length === 0) {\n c.add(`${path}.type`, \"field.type must be a non-empty string\");\n } else if (!BUILTIN_TYPES.has(type)) {\n // Unknown types are allowed (custom widgets) but flagged as a hint, not an error.\n // We intentionally do not push an issue here.\n }\n\n if ((type === \"select\" || type === \"radio\") && field[\"options\"] === undefined) {\n c.add(`${path}.options`, `field of type \"${type}\" should declare options`);\n }\n\n // Containers must declare nested fields; recurse so nested problems surface with a\n // precise path (e.g. `fields[3].fields[0].id`).\n if (typeof type === \"string\" && CONTAINER_TYPES.has(type)) {\n const nested = field[\"fields\"];\n if (!Array.isArray(nested) || nested.length === 0) {\n c.add(`${path}.fields`, `field of type \"${type}\" must declare a non-empty \"fields\" array`);\n } else {\n const childIds = new Set<string>();\n nested.forEach((f, i) => {\n if (type === \"steps\") {\n const childType = isRecord(f) ? f[\"type\"] : undefined;\n if (childType !== \"step\") {\n c.add(\n `${path}.fields[${i}].type`,\n `field of type \"steps\" must contain \"step\" children (got \"${String(childType)}\")`,\n );\n }\n }\n validateField(f, `${path}.fields[${i}]`, childIds, c);\n });\n }\n }\n}\n\n/** Validate an unknown value as a {@link FormSchema}. Never throws. */\nexport function validateSchema(input: unknown): ValidationResult {\n const c = new Collector();\n\n if (!isRecord(input)) {\n c.add(\"$\", \"schema must be an object\");\n return { ok: false, issues: c.issues };\n }\n\n if (typeof input[\"id\"] !== \"string\" || (input[\"id\"] as string).length === 0) {\n c.add(\"id\", \"schema.id must be a non-empty string\");\n }\n if (typeof input[\"version\"] !== \"string\" || (input[\"version\"] as string).length === 0) {\n c.add(\"version\", \"schema.version must be a non-empty string\");\n }\n\n const fields = input[\"fields\"];\n if (!Array.isArray(fields)) {\n c.add(\"fields\", \"schema.fields must be an array\");\n } else if (fields.length === 0) {\n c.add(\"fields\", \"schema.fields must contain at least one field\");\n } else {\n const seenIds = new Set<string>();\n fields.forEach((f, i) => validateField(f, `fields[${i}]`, seenIds, c));\n }\n\n const providers = input[\"providers\"];\n if (providers !== undefined && !isRecord(providers)) {\n c.add(\"providers\", \"schema.providers must be an object when present\");\n }\n\n const submit = input[\"submit\"];\n if (submit !== undefined && !isRecord(submit)) {\n c.add(\"submit\", \"schema.submit must be an object when present\");\n }\n\n if (c.issues.length > 0) {\n return { ok: false, issues: c.issues };\n }\n return { ok: true, value: input as unknown as FormSchema };\n}\n\n/** Validate and throw a single aggregated error on failure. Convenience for trusted input. */\nexport function parseSchema(input: unknown): FormSchema {\n const result = validateSchema(input);\n if (!result.ok) {\n const detail = result.issues.map((i) => ` - ${i.path}: ${i.message}`).join(\"\\n\");\n throw new SchemaValidationError(`Invalid Formwright schema:\\n${detail}`, result.issues);\n }\n return result.value;\n}\n\nexport class SchemaValidationError extends Error {\n readonly issues: readonly ValidationIssue[];\n constructor(message: string, issues: readonly ValidationIssue[]) {\n super(message);\n this.name = \"SchemaValidationError\";\n this.issues = issues;\n }\n}\n\n/** Type guard form of {@link validateSchema}. */\nexport function isFormSchema(input: unknown): input is FormSchema {\n return validateSchema(input).ok;\n}\n\n/** Convenience: list the field ids declared by a schema, in order. */\nexport function fieldIds(schema: FormSchema): string[] {\n return schema.fields.map((f: FieldSchema) => f.id);\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -48,7 +48,25 @@ type Condition = boolean | {
|
|
|
48
48
|
readonly or: readonly Condition[];
|
|
49
49
|
} | FieldValue;
|
|
50
50
|
/** Built-in field widget kinds. Extensible: any string maps to a registered widget. */
|
|
51
|
-
type FieldType = "text" | "number" | "email" | "password" | "textarea" | "select" | "checkbox" | "radio" | "group" | "collection" | (string & {});
|
|
51
|
+
type FieldType = "text" | "number" | "email" | "password" | "textarea" | "select" | "checkbox" | "radio" | "date" | "time" | "datetime" | "daterange" | "color" | "range" | "file" | "group" | "collection" | "steps" | "step" | "heading" | "separator" | "paragraph" | (string & {});
|
|
52
|
+
/**
|
|
53
|
+
* Map a field to your own UI — a custom element, native tag, or a widget you
|
|
54
|
+
* registered by name. The serializable bits (tag/component/valueProp/event/attrs)
|
|
55
|
+
* live here; code-level transforms and framework `mount` functions are attached
|
|
56
|
+
* via `registerWidget` in the renderer.
|
|
57
|
+
*/
|
|
58
|
+
type WidgetRef = string | {
|
|
59
|
+
/** A registered widget name (takes precedence over `tag`). */
|
|
60
|
+
readonly component?: string;
|
|
61
|
+
/** A custom element / native tag to render, e.g. "s-select". */
|
|
62
|
+
readonly tag?: string;
|
|
63
|
+
/** Property the value is written to / read from (default "value"). */
|
|
64
|
+
readonly valueProp?: string;
|
|
65
|
+
/** DOM event that signals a change, e.g. "value-change" (default "input"). */
|
|
66
|
+
readonly event?: string;
|
|
67
|
+
/** Static attributes to set on the element. */
|
|
68
|
+
readonly attrs?: Record<string, string>;
|
|
69
|
+
};
|
|
52
70
|
/** Validation descriptor — declarative, mapped to a Standard Schema validator at runtime. */
|
|
53
71
|
interface ValidationSchema {
|
|
54
72
|
readonly kind: "string" | "number" | "boolean" | "array";
|
|
@@ -59,20 +77,85 @@ interface ValidationSchema {
|
|
|
59
77
|
readonly maxLength?: number;
|
|
60
78
|
readonly pattern?: string;
|
|
61
79
|
readonly format?: "email" | "url" | "uuid";
|
|
80
|
+
/** Catch-all override for every rule's message. */
|
|
62
81
|
readonly message?: Resolvable<string>;
|
|
82
|
+
/** Per-rule message overrides (take precedence over `message` and the defaults). */
|
|
83
|
+
readonly messages?: {
|
|
84
|
+
readonly required?: string;
|
|
85
|
+
readonly min?: string;
|
|
86
|
+
readonly max?: string;
|
|
87
|
+
readonly minLength?: string;
|
|
88
|
+
readonly maxLength?: string;
|
|
89
|
+
readonly pattern?: string;
|
|
90
|
+
readonly format?: string;
|
|
91
|
+
readonly type?: string;
|
|
92
|
+
};
|
|
63
93
|
}
|
|
64
94
|
/** A selectable option for `select` / `radio` fields. */
|
|
65
95
|
interface FieldOption {
|
|
66
96
|
readonly label: Resolvable<string>;
|
|
67
97
|
readonly value: FieldValue;
|
|
68
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Per-part class overrides — drop in your own CSS classes or Tailwind utilities
|
|
101
|
+
* to restyle any part of a field without touching the renderer.
|
|
102
|
+
*/
|
|
103
|
+
interface FieldClasses {
|
|
104
|
+
readonly field?: string;
|
|
105
|
+
readonly label?: string;
|
|
106
|
+
readonly control?: string;
|
|
107
|
+
readonly help?: string;
|
|
108
|
+
readonly description?: string;
|
|
109
|
+
readonly error?: string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A slot rendered at the start/end of an input — either decorative text/icon
|
|
113
|
+
* (a string like "$" or "🔍") or a nested field (e.g. a currency `select`) whose
|
|
114
|
+
* value is added to the payload as a sibling key.
|
|
115
|
+
*/
|
|
116
|
+
type FieldSlot = string | FieldSchema;
|
|
69
117
|
/** A single field in the form. Resolved to a widget by `type`, keyed by `id`. */
|
|
70
118
|
interface FieldSchema {
|
|
71
119
|
readonly id: string;
|
|
72
120
|
readonly type: FieldType;
|
|
73
121
|
readonly label?: Resolvable<string>;
|
|
74
122
|
readonly placeholder?: Resolvable<string>;
|
|
123
|
+
/** Native `autocomplete` token for the input (e.g. "email", "name", "off"). */
|
|
124
|
+
readonly autocomplete?: string;
|
|
75
125
|
readonly help?: Resolvable<string>;
|
|
126
|
+
/** Static body text — for `paragraph`/`heading` presentational fields. */
|
|
127
|
+
readonly content?: Resolvable<string>;
|
|
128
|
+
/** An info tooltip shown next to the field's label. */
|
|
129
|
+
readonly tooltip?: Resolvable<string>;
|
|
130
|
+
/**
|
|
131
|
+
* Capture a value per locale → payload `{ en: "...", ar: "..." }`. Requires
|
|
132
|
+
* the form's `locales`. Renders as one input with a language switcher; reshape to
|
|
133
|
+
* `translations: { en: {...} }` in submit if needed.
|
|
134
|
+
*/
|
|
135
|
+
readonly localized?: boolean;
|
|
136
|
+
/** The locale shown first for a `localized` field (default: the form's first locale). */
|
|
137
|
+
readonly defaultLocale?: string;
|
|
138
|
+
/** Longer descriptive text, positioned by {@link descriptionPosition}. */
|
|
139
|
+
readonly description?: Resolvable<string>;
|
|
140
|
+
/** Where to render `description` (default `"below-label"`). */
|
|
141
|
+
readonly descriptionPosition?: "below-label" | "below-field";
|
|
142
|
+
/** For check-like fields (checkbox/toggle): label `"start"` (with control at the end) or `"end"` (default). */
|
|
143
|
+
readonly labelPosition?: "start" | "end";
|
|
144
|
+
/** Render content (icon/text or a value-bearing field) before/after the input. */
|
|
145
|
+
readonly slots?: {
|
|
146
|
+
readonly start?: FieldSlot;
|
|
147
|
+
readonly end?: FieldSlot;
|
|
148
|
+
};
|
|
149
|
+
/** Exclude this field's value from the submitted payload (UI-only control). */
|
|
150
|
+
readonly omit?: boolean;
|
|
151
|
+
/** Render this field with your own component/element instead of the built-in for `type`. */
|
|
152
|
+
readonly widget?: WidgetRef;
|
|
153
|
+
/** Columns the field spans in the form's 12-column grid (e.g. 6 = half width, two side by side). */
|
|
154
|
+
readonly colSpan?: number;
|
|
155
|
+
/** Extra class(es) on the field wrapper (e.g. Tailwind utilities). */
|
|
156
|
+
readonly class?: string;
|
|
157
|
+
/** Per-part class overrides (wrapper, label, control, help, description, error). */
|
|
158
|
+
readonly classes?: FieldClasses;
|
|
76
159
|
readonly defaultValue?: FieldValue;
|
|
77
160
|
readonly options?: Resolvable<readonly FieldOption[]>;
|
|
78
161
|
readonly validation?: ValidationSchema;
|
|
@@ -93,8 +176,19 @@ interface FieldSchema {
|
|
|
93
176
|
* Container layout:
|
|
94
177
|
* - `group`: `"fieldset"` (default) or `"accordion"` (collapsible section).
|
|
95
178
|
* - `collection`: `"list"` (default), `"cards"`, or `"accordion"` (each row collapsible).
|
|
179
|
+
* - `steps`: `"bar"` (default), `"tabs"`, or `"numbers"` progress indicator.
|
|
96
180
|
*/
|
|
97
|
-
readonly layout?: "fieldset" | "accordion" | "list" | "cards";
|
|
181
|
+
readonly layout?: "fieldset" | "accordion" | "list" | "cards" | "bar" | "tabs" | "numbers";
|
|
182
|
+
/** `steps` only: show a progress indicator (default `true`). */
|
|
183
|
+
readonly showProgress?: boolean;
|
|
184
|
+
/** `steps` only: validate the current step before advancing (default `true`). */
|
|
185
|
+
readonly validateOnNext?: boolean;
|
|
186
|
+
/** `steps` only: label for the Next button (default `"Next"`). */
|
|
187
|
+
readonly nextLabel?: Resolvable<string>;
|
|
188
|
+
/** `steps` only: label for the Back button (default `"Back"`). */
|
|
189
|
+
readonly prevLabel?: Resolvable<string>;
|
|
190
|
+
/** `steps` only: label for Submit on the last step (default `"Submit"`). */
|
|
191
|
+
readonly submitLabel?: Resolvable<string>;
|
|
98
192
|
/** `collection` only: label for each row, e.g. "Contact". */
|
|
99
193
|
readonly itemLabel?: Resolvable<string>;
|
|
100
194
|
/** `collection` only: text for the add-row button (default: "Add"). */
|
|
@@ -110,6 +204,18 @@ interface ProviderDecl {
|
|
|
110
204
|
readonly type: "i18n" | "tanstack-query" | "theme" | (string & {});
|
|
111
205
|
readonly [key: string]: unknown;
|
|
112
206
|
}
|
|
207
|
+
/** A form-level action button (submit, reset, or a named custom action like "delete"). */
|
|
208
|
+
interface FormAction {
|
|
209
|
+
readonly name: string;
|
|
210
|
+
readonly label?: Resolvable<string>;
|
|
211
|
+
/** `"submit"` triggers submission, `"reset"` resets, `"button"` (default) emits an action event. */
|
|
212
|
+
readonly role?: "submit" | "reset" | "button";
|
|
213
|
+
readonly variant?: "primary" | "secondary" | "danger";
|
|
214
|
+
/** Render this button full-width (stretches to fill the action bar). */
|
|
215
|
+
readonly fullWidth?: boolean;
|
|
216
|
+
/** Name of a handler in `options.handlers`, called with the form on click. */
|
|
217
|
+
readonly handler?: string;
|
|
218
|
+
}
|
|
113
219
|
/** How the form submits: transform the payload, send it, handle success/error. */
|
|
114
220
|
interface SubmitSchema {
|
|
115
221
|
/** Name of a registered transform applied to values before sending. */
|
|
@@ -131,6 +237,14 @@ interface FormSchema {
|
|
|
131
237
|
readonly providers?: Record<string, ProviderDecl>;
|
|
132
238
|
readonly fields: readonly FieldSchema[];
|
|
133
239
|
readonly submit?: SubmitSchema;
|
|
240
|
+
/** Locales for `localized` fields — each captures a value per locale. */
|
|
241
|
+
readonly locales?: readonly string[];
|
|
242
|
+
/** Locales rendered right-to-left (defaults to the common RTL set: ar, he, fa, ur, …). */
|
|
243
|
+
readonly rtlLocales?: readonly string[];
|
|
244
|
+
/** Action buttons rendered at the bottom of the form (defaults to a single Submit). */
|
|
245
|
+
readonly actions?: readonly FormAction[];
|
|
246
|
+
/** How action buttons are aligned: `"start"` (default), `"end"`, or `"between"`. */
|
|
247
|
+
readonly actionsAlign?: "start" | "end" | "between";
|
|
134
248
|
}
|
|
135
249
|
|
|
136
250
|
/**
|
|
@@ -167,4 +281,4 @@ declare function isFormSchema(input: unknown): input is FormSchema;
|
|
|
167
281
|
/** Convenience: list the field ids declared by a schema, in order. */
|
|
168
282
|
declare function fieldIds(schema: FormSchema): string[];
|
|
169
283
|
|
|
170
|
-
export { type Condition, type FieldOption, type FieldSchema, type FieldType, type FieldValue, type FormSchema, type ProviderDecl, type ProviderRef, type Resolvable, SchemaValidationError, type SubmitSchema, type ValidationIssue, type ValidationResult, type ValidationSchema, fieldIds, isFormSchema, parseSchema, validateSchema };
|
|
284
|
+
export { type Condition, type FieldClasses, type FieldOption, type FieldSchema, type FieldSlot, type FieldType, type FieldValue, type FormAction, type FormSchema, type ProviderDecl, type ProviderRef, type Resolvable, SchemaValidationError, type SubmitSchema, type ValidationIssue, type ValidationResult, type ValidationSchema, type WidgetRef, fieldIds, isFormSchema, parseSchema, validateSchema };
|
package/dist/index.d.ts
CHANGED
|
@@ -48,7 +48,25 @@ type Condition = boolean | {
|
|
|
48
48
|
readonly or: readonly Condition[];
|
|
49
49
|
} | FieldValue;
|
|
50
50
|
/** Built-in field widget kinds. Extensible: any string maps to a registered widget. */
|
|
51
|
-
type FieldType = "text" | "number" | "email" | "password" | "textarea" | "select" | "checkbox" | "radio" | "group" | "collection" | (string & {});
|
|
51
|
+
type FieldType = "text" | "number" | "email" | "password" | "textarea" | "select" | "checkbox" | "radio" | "date" | "time" | "datetime" | "daterange" | "color" | "range" | "file" | "group" | "collection" | "steps" | "step" | "heading" | "separator" | "paragraph" | (string & {});
|
|
52
|
+
/**
|
|
53
|
+
* Map a field to your own UI — a custom element, native tag, or a widget you
|
|
54
|
+
* registered by name. The serializable bits (tag/component/valueProp/event/attrs)
|
|
55
|
+
* live here; code-level transforms and framework `mount` functions are attached
|
|
56
|
+
* via `registerWidget` in the renderer.
|
|
57
|
+
*/
|
|
58
|
+
type WidgetRef = string | {
|
|
59
|
+
/** A registered widget name (takes precedence over `tag`). */
|
|
60
|
+
readonly component?: string;
|
|
61
|
+
/** A custom element / native tag to render, e.g. "s-select". */
|
|
62
|
+
readonly tag?: string;
|
|
63
|
+
/** Property the value is written to / read from (default "value"). */
|
|
64
|
+
readonly valueProp?: string;
|
|
65
|
+
/** DOM event that signals a change, e.g. "value-change" (default "input"). */
|
|
66
|
+
readonly event?: string;
|
|
67
|
+
/** Static attributes to set on the element. */
|
|
68
|
+
readonly attrs?: Record<string, string>;
|
|
69
|
+
};
|
|
52
70
|
/** Validation descriptor — declarative, mapped to a Standard Schema validator at runtime. */
|
|
53
71
|
interface ValidationSchema {
|
|
54
72
|
readonly kind: "string" | "number" | "boolean" | "array";
|
|
@@ -59,20 +77,85 @@ interface ValidationSchema {
|
|
|
59
77
|
readonly maxLength?: number;
|
|
60
78
|
readonly pattern?: string;
|
|
61
79
|
readonly format?: "email" | "url" | "uuid";
|
|
80
|
+
/** Catch-all override for every rule's message. */
|
|
62
81
|
readonly message?: Resolvable<string>;
|
|
82
|
+
/** Per-rule message overrides (take precedence over `message` and the defaults). */
|
|
83
|
+
readonly messages?: {
|
|
84
|
+
readonly required?: string;
|
|
85
|
+
readonly min?: string;
|
|
86
|
+
readonly max?: string;
|
|
87
|
+
readonly minLength?: string;
|
|
88
|
+
readonly maxLength?: string;
|
|
89
|
+
readonly pattern?: string;
|
|
90
|
+
readonly format?: string;
|
|
91
|
+
readonly type?: string;
|
|
92
|
+
};
|
|
63
93
|
}
|
|
64
94
|
/** A selectable option for `select` / `radio` fields. */
|
|
65
95
|
interface FieldOption {
|
|
66
96
|
readonly label: Resolvable<string>;
|
|
67
97
|
readonly value: FieldValue;
|
|
68
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Per-part class overrides — drop in your own CSS classes or Tailwind utilities
|
|
101
|
+
* to restyle any part of a field without touching the renderer.
|
|
102
|
+
*/
|
|
103
|
+
interface FieldClasses {
|
|
104
|
+
readonly field?: string;
|
|
105
|
+
readonly label?: string;
|
|
106
|
+
readonly control?: string;
|
|
107
|
+
readonly help?: string;
|
|
108
|
+
readonly description?: string;
|
|
109
|
+
readonly error?: string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A slot rendered at the start/end of an input — either decorative text/icon
|
|
113
|
+
* (a string like "$" or "🔍") or a nested field (e.g. a currency `select`) whose
|
|
114
|
+
* value is added to the payload as a sibling key.
|
|
115
|
+
*/
|
|
116
|
+
type FieldSlot = string | FieldSchema;
|
|
69
117
|
/** A single field in the form. Resolved to a widget by `type`, keyed by `id`. */
|
|
70
118
|
interface FieldSchema {
|
|
71
119
|
readonly id: string;
|
|
72
120
|
readonly type: FieldType;
|
|
73
121
|
readonly label?: Resolvable<string>;
|
|
74
122
|
readonly placeholder?: Resolvable<string>;
|
|
123
|
+
/** Native `autocomplete` token for the input (e.g. "email", "name", "off"). */
|
|
124
|
+
readonly autocomplete?: string;
|
|
75
125
|
readonly help?: Resolvable<string>;
|
|
126
|
+
/** Static body text — for `paragraph`/`heading` presentational fields. */
|
|
127
|
+
readonly content?: Resolvable<string>;
|
|
128
|
+
/** An info tooltip shown next to the field's label. */
|
|
129
|
+
readonly tooltip?: Resolvable<string>;
|
|
130
|
+
/**
|
|
131
|
+
* Capture a value per locale → payload `{ en: "...", ar: "..." }`. Requires
|
|
132
|
+
* the form's `locales`. Renders as one input with a language switcher; reshape to
|
|
133
|
+
* `translations: { en: {...} }` in submit if needed.
|
|
134
|
+
*/
|
|
135
|
+
readonly localized?: boolean;
|
|
136
|
+
/** The locale shown first for a `localized` field (default: the form's first locale). */
|
|
137
|
+
readonly defaultLocale?: string;
|
|
138
|
+
/** Longer descriptive text, positioned by {@link descriptionPosition}. */
|
|
139
|
+
readonly description?: Resolvable<string>;
|
|
140
|
+
/** Where to render `description` (default `"below-label"`). */
|
|
141
|
+
readonly descriptionPosition?: "below-label" | "below-field";
|
|
142
|
+
/** For check-like fields (checkbox/toggle): label `"start"` (with control at the end) or `"end"` (default). */
|
|
143
|
+
readonly labelPosition?: "start" | "end";
|
|
144
|
+
/** Render content (icon/text or a value-bearing field) before/after the input. */
|
|
145
|
+
readonly slots?: {
|
|
146
|
+
readonly start?: FieldSlot;
|
|
147
|
+
readonly end?: FieldSlot;
|
|
148
|
+
};
|
|
149
|
+
/** Exclude this field's value from the submitted payload (UI-only control). */
|
|
150
|
+
readonly omit?: boolean;
|
|
151
|
+
/** Render this field with your own component/element instead of the built-in for `type`. */
|
|
152
|
+
readonly widget?: WidgetRef;
|
|
153
|
+
/** Columns the field spans in the form's 12-column grid (e.g. 6 = half width, two side by side). */
|
|
154
|
+
readonly colSpan?: number;
|
|
155
|
+
/** Extra class(es) on the field wrapper (e.g. Tailwind utilities). */
|
|
156
|
+
readonly class?: string;
|
|
157
|
+
/** Per-part class overrides (wrapper, label, control, help, description, error). */
|
|
158
|
+
readonly classes?: FieldClasses;
|
|
76
159
|
readonly defaultValue?: FieldValue;
|
|
77
160
|
readonly options?: Resolvable<readonly FieldOption[]>;
|
|
78
161
|
readonly validation?: ValidationSchema;
|
|
@@ -93,8 +176,19 @@ interface FieldSchema {
|
|
|
93
176
|
* Container layout:
|
|
94
177
|
* - `group`: `"fieldset"` (default) or `"accordion"` (collapsible section).
|
|
95
178
|
* - `collection`: `"list"` (default), `"cards"`, or `"accordion"` (each row collapsible).
|
|
179
|
+
* - `steps`: `"bar"` (default), `"tabs"`, or `"numbers"` progress indicator.
|
|
96
180
|
*/
|
|
97
|
-
readonly layout?: "fieldset" | "accordion" | "list" | "cards";
|
|
181
|
+
readonly layout?: "fieldset" | "accordion" | "list" | "cards" | "bar" | "tabs" | "numbers";
|
|
182
|
+
/** `steps` only: show a progress indicator (default `true`). */
|
|
183
|
+
readonly showProgress?: boolean;
|
|
184
|
+
/** `steps` only: validate the current step before advancing (default `true`). */
|
|
185
|
+
readonly validateOnNext?: boolean;
|
|
186
|
+
/** `steps` only: label for the Next button (default `"Next"`). */
|
|
187
|
+
readonly nextLabel?: Resolvable<string>;
|
|
188
|
+
/** `steps` only: label for the Back button (default `"Back"`). */
|
|
189
|
+
readonly prevLabel?: Resolvable<string>;
|
|
190
|
+
/** `steps` only: label for Submit on the last step (default `"Submit"`). */
|
|
191
|
+
readonly submitLabel?: Resolvable<string>;
|
|
98
192
|
/** `collection` only: label for each row, e.g. "Contact". */
|
|
99
193
|
readonly itemLabel?: Resolvable<string>;
|
|
100
194
|
/** `collection` only: text for the add-row button (default: "Add"). */
|
|
@@ -110,6 +204,18 @@ interface ProviderDecl {
|
|
|
110
204
|
readonly type: "i18n" | "tanstack-query" | "theme" | (string & {});
|
|
111
205
|
readonly [key: string]: unknown;
|
|
112
206
|
}
|
|
207
|
+
/** A form-level action button (submit, reset, or a named custom action like "delete"). */
|
|
208
|
+
interface FormAction {
|
|
209
|
+
readonly name: string;
|
|
210
|
+
readonly label?: Resolvable<string>;
|
|
211
|
+
/** `"submit"` triggers submission, `"reset"` resets, `"button"` (default) emits an action event. */
|
|
212
|
+
readonly role?: "submit" | "reset" | "button";
|
|
213
|
+
readonly variant?: "primary" | "secondary" | "danger";
|
|
214
|
+
/** Render this button full-width (stretches to fill the action bar). */
|
|
215
|
+
readonly fullWidth?: boolean;
|
|
216
|
+
/** Name of a handler in `options.handlers`, called with the form on click. */
|
|
217
|
+
readonly handler?: string;
|
|
218
|
+
}
|
|
113
219
|
/** How the form submits: transform the payload, send it, handle success/error. */
|
|
114
220
|
interface SubmitSchema {
|
|
115
221
|
/** Name of a registered transform applied to values before sending. */
|
|
@@ -131,6 +237,14 @@ interface FormSchema {
|
|
|
131
237
|
readonly providers?: Record<string, ProviderDecl>;
|
|
132
238
|
readonly fields: readonly FieldSchema[];
|
|
133
239
|
readonly submit?: SubmitSchema;
|
|
240
|
+
/** Locales for `localized` fields — each captures a value per locale. */
|
|
241
|
+
readonly locales?: readonly string[];
|
|
242
|
+
/** Locales rendered right-to-left (defaults to the common RTL set: ar, he, fa, ur, …). */
|
|
243
|
+
readonly rtlLocales?: readonly string[];
|
|
244
|
+
/** Action buttons rendered at the bottom of the form (defaults to a single Submit). */
|
|
245
|
+
readonly actions?: readonly FormAction[];
|
|
246
|
+
/** How action buttons are aligned: `"start"` (default), `"end"`, or `"between"`. */
|
|
247
|
+
readonly actionsAlign?: "start" | "end" | "between";
|
|
134
248
|
}
|
|
135
249
|
|
|
136
250
|
/**
|
|
@@ -167,4 +281,4 @@ declare function isFormSchema(input: unknown): input is FormSchema;
|
|
|
167
281
|
/** Convenience: list the field ids declared by a schema, in order. */
|
|
168
282
|
declare function fieldIds(schema: FormSchema): string[];
|
|
169
283
|
|
|
170
|
-
export { type Condition, type FieldOption, type FieldSchema, type FieldType, type FieldValue, type FormSchema, type ProviderDecl, type ProviderRef, type Resolvable, SchemaValidationError, type SubmitSchema, type ValidationIssue, type ValidationResult, type ValidationSchema, fieldIds, isFormSchema, parseSchema, validateSchema };
|
|
284
|
+
export { type Condition, type FieldClasses, type FieldOption, type FieldSchema, type FieldSlot, type FieldType, type FieldValue, type FormAction, type FormSchema, type ProviderDecl, type ProviderRef, type Resolvable, SchemaValidationError, type SubmitSchema, type ValidationIssue, type ValidationResult, type ValidationSchema, type WidgetRef, fieldIds, isFormSchema, parseSchema, validateSchema };
|
package/dist/index.js
CHANGED
|
@@ -9,9 +9,16 @@ var BUILTIN_TYPES = /* @__PURE__ */ new Set([
|
|
|
9
9
|
"checkbox",
|
|
10
10
|
"radio",
|
|
11
11
|
"group",
|
|
12
|
-
"collection"
|
|
12
|
+
"collection",
|
|
13
|
+
"steps",
|
|
14
|
+
"step"
|
|
15
|
+
]);
|
|
16
|
+
var CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
17
|
+
"group",
|
|
18
|
+
"collection",
|
|
19
|
+
"steps",
|
|
20
|
+
"step"
|
|
13
21
|
]);
|
|
14
|
-
var CONTAINER_TYPES = /* @__PURE__ */ new Set(["group", "collection"]);
|
|
15
22
|
function isRecord(v) {
|
|
16
23
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
17
24
|
}
|
|
@@ -47,7 +54,18 @@ function validateField(field, path, seenIds, c) {
|
|
|
47
54
|
c.add(`${path}.fields`, `field of type "${type}" must declare a non-empty "fields" array`);
|
|
48
55
|
} else {
|
|
49
56
|
const childIds = /* @__PURE__ */ new Set();
|
|
50
|
-
nested.forEach((f, i) =>
|
|
57
|
+
nested.forEach((f, i) => {
|
|
58
|
+
if (type === "steps") {
|
|
59
|
+
const childType = isRecord(f) ? f["type"] : void 0;
|
|
60
|
+
if (childType !== "step") {
|
|
61
|
+
c.add(
|
|
62
|
+
`${path}.fields[${i}].type`,
|
|
63
|
+
`field of type "steps" must contain "step" children (got "${String(childType)}")`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
validateField(f, `${path}.fields[${i}]`, childIds, c);
|
|
68
|
+
});
|
|
51
69
|
}
|
|
52
70
|
}
|
|
53
71
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/validate.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Runtime validation of an unknown value into a typed {@link FormSchema}.\n *\n * Dependency-free and structural — its job is to give precise, path-addressed\n * errors so an LLM-emitted schema can be repaired (or rejected) before it ever\n * reaches the runtime. This validates the *schema shape*, not user form input\n * (that is the job of the per-field {@link ValidationSchema}).\n */\nimport type { FieldSchema, FieldType, FormSchema } from \"./types.js\";\n\nexport interface ValidationIssue {\n /** JSON-pointer-ish path to the offending node, e.g. `fields[2].type`. */\n readonly path: string;\n readonly message: string;\n}\n\nexport type ValidationResult =\n | { readonly ok: true; readonly value: FormSchema }\n | { readonly ok: false; readonly issues: readonly ValidationIssue[] };\n\nconst BUILTIN_TYPES: ReadonlySet<string> = new Set<FieldType>([\n \"text\",\n \"number\",\n \"email\",\n \"password\",\n \"textarea\",\n \"select\",\n \"checkbox\",\n \"radio\",\n \"group\",\n \"collection\",\n]);\n\nconst CONTAINER_TYPES: ReadonlySet<string> = new Set<FieldType>([\"group\", \"collection\"]);\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nclass Collector {\n readonly issues: ValidationIssue[] = [];\n add(path: string, message: string): void {\n this.issues.push({ path, message });\n }\n}\n\nfunction validateField(field: unknown, path: string, seenIds: Set<string>, c: Collector): void {\n if (!isRecord(field)) {\n c.add(path, \"field must be an object\");\n return;\n }\n\n const id = field[\"id\"];\n if (typeof id !== \"string\" || id.length === 0) {\n c.add(`${path}.id`, \"field.id must be a non-empty string\");\n } else if (seenIds.has(id)) {\n c.add(`${path}.id`, `duplicate field id \"${id}\"`);\n } else {\n seenIds.add(id);\n }\n\n const type = field[\"type\"];\n if (typeof type !== \"string\" || type.length === 0) {\n c.add(`${path}.type`, \"field.type must be a non-empty string\");\n } else if (!BUILTIN_TYPES.has(type)) {\n // Unknown types are allowed (custom widgets) but flagged as a hint, not an error.\n // We intentionally do not push an issue here.\n }\n\n if ((type === \"select\" || type === \"radio\") && field[\"options\"] === undefined) {\n c.add(`${path}.options`, `field of type \"${type}\" should declare options`);\n }\n\n // Containers (group/collection) must declare nested fields; recurse so nested\n // problems surface with a precise path (e.g. `fields[3].fields[0].id`).\n if (typeof type === \"string\" && CONTAINER_TYPES.has(type)) {\n const nested = field[\"fields\"];\n if (!Array.isArray(nested) || nested.length === 0) {\n c.add(`${path}.fields`, `field of type \"${type}\" must declare a non-empty \"fields\" array`);\n } else {\n // A container opens a new naming scope: child ids are unique within it, not globally.\n const childIds = new Set<string>();\n nested.forEach((f, i) => validateField(f, `${path}.fields[${i}]`, childIds, c));\n }\n }\n}\n\n/** Validate an unknown value as a {@link FormSchema}. Never throws. */\nexport function validateSchema(input: unknown): ValidationResult {\n const c = new Collector();\n\n if (!isRecord(input)) {\n c.add(\"$\", \"schema must be an object\");\n return { ok: false, issues: c.issues };\n }\n\n if (typeof input[\"id\"] !== \"string\" || (input[\"id\"] as string).length === 0) {\n c.add(\"id\", \"schema.id must be a non-empty string\");\n }\n if (typeof input[\"version\"] !== \"string\" || (input[\"version\"] as string).length === 0) {\n c.add(\"version\", \"schema.version must be a non-empty string\");\n }\n\n const fields = input[\"fields\"];\n if (!Array.isArray(fields)) {\n c.add(\"fields\", \"schema.fields must be an array\");\n } else if (fields.length === 0) {\n c.add(\"fields\", \"schema.fields must contain at least one field\");\n } else {\n const seenIds = new Set<string>();\n fields.forEach((f, i) => validateField(f, `fields[${i}]`, seenIds, c));\n }\n\n const providers = input[\"providers\"];\n if (providers !== undefined && !isRecord(providers)) {\n c.add(\"providers\", \"schema.providers must be an object when present\");\n }\n\n const submit = input[\"submit\"];\n if (submit !== undefined && !isRecord(submit)) {\n c.add(\"submit\", \"schema.submit must be an object when present\");\n }\n\n if (c.issues.length > 0) {\n return { ok: false, issues: c.issues };\n }\n return { ok: true, value: input as unknown as FormSchema };\n}\n\n/** Validate and throw a single aggregated error on failure. Convenience for trusted input. */\nexport function parseSchema(input: unknown): FormSchema {\n const result = validateSchema(input);\n if (!result.ok) {\n const detail = result.issues.map((i) => ` - ${i.path}: ${i.message}`).join(\"\\n\");\n throw new SchemaValidationError(`Invalid Formwright schema:\\n${detail}`, result.issues);\n }\n return result.value;\n}\n\nexport class SchemaValidationError extends Error {\n readonly issues: readonly ValidationIssue[];\n constructor(message: string, issues: readonly ValidationIssue[]) {\n super(message);\n this.name = \"SchemaValidationError\";\n this.issues = issues;\n }\n}\n\n/** Type guard form of {@link validateSchema}. */\nexport function isFormSchema(input: unknown): input is FormSchema {\n return validateSchema(input).ok;\n}\n\n/** Convenience: list the field ids declared by a schema, in order. */\nexport function fieldIds(schema: FormSchema): string[] {\n return schema.fields.map((f: FieldSchema) => f.id);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/validate.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Runtime validation of an unknown value into a typed {@link FormSchema}.\n *\n * Dependency-free and structural — its job is to give precise, path-addressed\n * errors so an LLM-emitted schema can be repaired (or rejected) before it ever\n * reaches the runtime. This validates the *schema shape*, not user form input\n * (that is the job of the per-field {@link ValidationSchema}).\n */\nimport type { FieldSchema, FieldType, FormSchema } from \"./types.js\";\n\nexport interface ValidationIssue {\n /** JSON-pointer-ish path to the offending node, e.g. `fields[2].type`. */\n readonly path: string;\n readonly message: string;\n}\n\nexport type ValidationResult =\n | { readonly ok: true; readonly value: FormSchema }\n | { readonly ok: false; readonly issues: readonly ValidationIssue[] };\n\nconst BUILTIN_TYPES: ReadonlySet<string> = new Set<FieldType>([\n \"text\",\n \"number\",\n \"email\",\n \"password\",\n \"textarea\",\n \"select\",\n \"checkbox\",\n \"radio\",\n \"group\",\n \"collection\",\n \"steps\",\n \"step\",\n]);\n\nconst CONTAINER_TYPES: ReadonlySet<string> = new Set<FieldType>([\n \"group\",\n \"collection\",\n \"steps\",\n \"step\",\n]);\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nclass Collector {\n readonly issues: ValidationIssue[] = [];\n add(path: string, message: string): void {\n this.issues.push({ path, message });\n }\n}\n\nfunction validateField(field: unknown, path: string, seenIds: Set<string>, c: Collector): void {\n if (!isRecord(field)) {\n c.add(path, \"field must be an object\");\n return;\n }\n\n const id = field[\"id\"];\n if (typeof id !== \"string\" || id.length === 0) {\n c.add(`${path}.id`, \"field.id must be a non-empty string\");\n } else if (seenIds.has(id)) {\n c.add(`${path}.id`, `duplicate field id \"${id}\"`);\n } else {\n seenIds.add(id);\n }\n\n const type = field[\"type\"];\n if (typeof type !== \"string\" || type.length === 0) {\n c.add(`${path}.type`, \"field.type must be a non-empty string\");\n } else if (!BUILTIN_TYPES.has(type)) {\n // Unknown types are allowed (custom widgets) but flagged as a hint, not an error.\n // We intentionally do not push an issue here.\n }\n\n if ((type === \"select\" || type === \"radio\") && field[\"options\"] === undefined) {\n c.add(`${path}.options`, `field of type \"${type}\" should declare options`);\n }\n\n // Containers must declare nested fields; recurse so nested problems surface with a\n // precise path (e.g. `fields[3].fields[0].id`).\n if (typeof type === \"string\" && CONTAINER_TYPES.has(type)) {\n const nested = field[\"fields\"];\n if (!Array.isArray(nested) || nested.length === 0) {\n c.add(`${path}.fields`, `field of type \"${type}\" must declare a non-empty \"fields\" array`);\n } else {\n const childIds = new Set<string>();\n nested.forEach((f, i) => {\n if (type === \"steps\") {\n const childType = isRecord(f) ? f[\"type\"] : undefined;\n if (childType !== \"step\") {\n c.add(\n `${path}.fields[${i}].type`,\n `field of type \"steps\" must contain \"step\" children (got \"${String(childType)}\")`,\n );\n }\n }\n validateField(f, `${path}.fields[${i}]`, childIds, c);\n });\n }\n }\n}\n\n/** Validate an unknown value as a {@link FormSchema}. Never throws. */\nexport function validateSchema(input: unknown): ValidationResult {\n const c = new Collector();\n\n if (!isRecord(input)) {\n c.add(\"$\", \"schema must be an object\");\n return { ok: false, issues: c.issues };\n }\n\n if (typeof input[\"id\"] !== \"string\" || (input[\"id\"] as string).length === 0) {\n c.add(\"id\", \"schema.id must be a non-empty string\");\n }\n if (typeof input[\"version\"] !== \"string\" || (input[\"version\"] as string).length === 0) {\n c.add(\"version\", \"schema.version must be a non-empty string\");\n }\n\n const fields = input[\"fields\"];\n if (!Array.isArray(fields)) {\n c.add(\"fields\", \"schema.fields must be an array\");\n } else if (fields.length === 0) {\n c.add(\"fields\", \"schema.fields must contain at least one field\");\n } else {\n const seenIds = new Set<string>();\n fields.forEach((f, i) => validateField(f, `fields[${i}]`, seenIds, c));\n }\n\n const providers = input[\"providers\"];\n if (providers !== undefined && !isRecord(providers)) {\n c.add(\"providers\", \"schema.providers must be an object when present\");\n }\n\n const submit = input[\"submit\"];\n if (submit !== undefined && !isRecord(submit)) {\n c.add(\"submit\", \"schema.submit must be an object when present\");\n }\n\n if (c.issues.length > 0) {\n return { ok: false, issues: c.issues };\n }\n return { ok: true, value: input as unknown as FormSchema };\n}\n\n/** Validate and throw a single aggregated error on failure. Convenience for trusted input. */\nexport function parseSchema(input: unknown): FormSchema {\n const result = validateSchema(input);\n if (!result.ok) {\n const detail = result.issues.map((i) => ` - ${i.path}: ${i.message}`).join(\"\\n\");\n throw new SchemaValidationError(`Invalid Formwright schema:\\n${detail}`, result.issues);\n }\n return result.value;\n}\n\nexport class SchemaValidationError extends Error {\n readonly issues: readonly ValidationIssue[];\n constructor(message: string, issues: readonly ValidationIssue[]) {\n super(message);\n this.name = \"SchemaValidationError\";\n this.issues = issues;\n }\n}\n\n/** Type guard form of {@link validateSchema}. */\nexport function isFormSchema(input: unknown): input is FormSchema {\n return validateSchema(input).ok;\n}\n\n/** Convenience: list the field ids declared by a schema, in order. */\nexport function fieldIds(schema: FormSchema): string[] {\n return schema.fields.map((f: FieldSchema) => f.id);\n}\n"]}
|