@formwright/schema 0.2.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/dist/index.cjs +21 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,7 @@ 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" | "date" | "time" | "datetime" | "daterange" | "color" | "range" | "file" | "group" | "collection" | "heading" | "separator" | "paragraph" | (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
52
|
/**
|
|
53
53
|
* Map a field to your own UI — a custom element, native tag, or a widget you
|
|
54
54
|
* registered by name. The serializable bits (tag/component/valueProp/event/attrs)
|
|
@@ -176,8 +176,19 @@ interface FieldSchema {
|
|
|
176
176
|
* Container layout:
|
|
177
177
|
* - `group`: `"fieldset"` (default) or `"accordion"` (collapsible section).
|
|
178
178
|
* - `collection`: `"list"` (default), `"cards"`, or `"accordion"` (each row collapsible).
|
|
179
|
+
* - `steps`: `"bar"` (default), `"tabs"`, or `"numbers"` progress indicator.
|
|
179
180
|
*/
|
|
180
|
-
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>;
|
|
181
192
|
/** `collection` only: label for each row, e.g. "Contact". */
|
|
182
193
|
readonly itemLabel?: Resolvable<string>;
|
|
183
194
|
/** `collection` only: text for the add-row button (default: "Add"). */
|
package/dist/index.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ 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" | "date" | "time" | "datetime" | "daterange" | "color" | "range" | "file" | "group" | "collection" | "heading" | "separator" | "paragraph" | (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
52
|
/**
|
|
53
53
|
* Map a field to your own UI — a custom element, native tag, or a widget you
|
|
54
54
|
* registered by name. The serializable bits (tag/component/valueProp/event/attrs)
|
|
@@ -176,8 +176,19 @@ interface FieldSchema {
|
|
|
176
176
|
* Container layout:
|
|
177
177
|
* - `group`: `"fieldset"` (default) or `"accordion"` (collapsible section).
|
|
178
178
|
* - `collection`: `"list"` (default), `"cards"`, or `"accordion"` (each row collapsible).
|
|
179
|
+
* - `steps`: `"bar"` (default), `"tabs"`, or `"numbers"` progress indicator.
|
|
179
180
|
*/
|
|
180
|
-
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>;
|
|
181
192
|
/** `collection` only: label for each row, e.g. "Contact". */
|
|
182
193
|
readonly itemLabel?: Resolvable<string>;
|
|
183
194
|
/** `collection` only: text for the add-row button (default: "Add"). */
|
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":";AAoBA,IAAM,aAAA,uBAAyC,GAAA,CAAe;AAAA,EAC5D,MAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA,UAAA;AAAA,EACA,UAAA;AAAA,EACA,QAAA;AAAA,EACA,UAAA;AAAA,EACA,OAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAC,CAAA;AAED,IAAM,kCAAuC,IAAI,GAAA,CAAe,CAAC,OAAA,EAAS,YAAY,CAAC,CAAA;AAEvF,SAAS,SAAS,CAAA,EAA0C;AAC1D,EAAA,OAAO,OAAO,MAAM,QAAA,IAAY,CAAA,KAAM,QAAQ,CAAC,KAAA,CAAM,QAAQ,CAAC,CAAA;AAChE;AAEA,IAAM,YAAN,MAAgB;AAAA,EACL,SAA4B,EAAC;AAAA,EACtC,GAAA,CAAI,MAAc,OAAA,EAAuB;AACvC,IAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,EAAE,IAAA,EAAM,SAAS,CAAA;AAAA,EACpC;AACF,CAAA;AAEA,SAAS,aAAA,CAAc,KAAA,EAAgB,IAAA,EAAc,OAAA,EAAsB,CAAA,EAAoB;AAC7F,EAAA,IAAI,CAAC,QAAA,CAAS,KAAK,CAAA,EAAG;AACpB,IAAA,CAAA,CAAE,GAAA,CAAI,MAAM,yBAAyB,CAAA;AACrC,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,EAAA,GAAK,MAAM,IAAI,CAAA;AACrB,EAAA,IAAI,OAAO,EAAA,KAAO,QAAA,IAAY,EAAA,CAAG,WAAW,CAAA,EAAG;AAC7C,IAAA,CAAA,CAAE,GAAA,CAAI,CAAA,EAAG,IAAI,CAAA,GAAA,CAAA,EAAO,qCAAqC,CAAA;AAAA,EAC3D,CAAA,MAAA,IAAW,OAAA,CAAQ,GAAA,CAAI,EAAE,CAAA,EAAG;AAC1B,IAAA,CAAA,CAAE,IAAI,CAAA,EAAG,IAAI,CAAA,GAAA,CAAA,EAAO,CAAA,oBAAA,EAAuB,EAAE,CAAA,CAAA,CAAG,CAAA;AAAA,EAClD,CAAA,MAAO;AACL,IAAA,OAAA,CAAQ,IAAI,EAAE,CAAA;AAAA,EAChB;AAEA,EAAA,MAAM,IAAA,GAAO,MAAM,MAAM,CAAA;AACzB,EAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,IAAA,CAAK,WAAW,CAAA,EAAG;AACjD,IAAA,CAAA,CAAE,GAAA,CAAI,CAAA,EAAG,IAAI,CAAA,KAAA,CAAA,EAAS,uCAAuC,CAAA;AAAA,EAC/D,CAAA,MAAA,IAAW,CAAC,aAAA,CAAc,GAAA,CAAI,IAAI,CAAA,EAAG;AAKrC,EAAA,IAAA,CAAK,SAAS,QAAA,IAAY,IAAA,KAAS,YAAY,KAAA,CAAM,SAAS,MAAM,MAAA,EAAW;AAC7E,IAAA,CAAA,CAAE,IAAI,CAAA,EAAG,IAAI,CAAA,QAAA,CAAA,EAAY,CAAA,eAAA,EAAkB,IAAI,CAAA,wBAAA,CAA0B,CAAA;AAAA,EAC3E;AAIA,EAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,eAAA,CAAgB,GAAA,CAAI,IAAI,CAAA,EAAG;AACzD,IAAA,MAAM,MAAA,GAAS,MAAM,QAAQ,CAAA;AAC7B,IAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,IAAK,MAAA,CAAO,WAAW,CAAA,EAAG;AACjD,MAAA,CAAA,CAAE,IAAI,CAAA,EAAG,IAAI,CAAA,OAAA,CAAA,EAAW,CAAA,eAAA,EAAkB,IAAI,CAAA,yCAAA,CAA2C,CAAA;AAAA,IAC3F,CAAA,MAAO;AAEL,MAAA,MAAM,QAAA,uBAAe,GAAA,EAAY;AACjC,MAAA,MAAA,CAAO,OAAA,CAAQ,CAAC,CAAA,EAAG,CAAA,KAAM,aAAA,CAAc,CAAA,EAAG,CAAA,EAAG,IAAI,CAAA,QAAA,EAAW,CAAC,CAAA,CAAA,CAAA,EAAK,QAAA,EAAU,CAAC,CAAC,CAAA;AAAA,IAChF;AAAA,EACF;AACF;AAGO,SAAS,eAAe,KAAA,EAAkC;AAC/D,EAAA,MAAM,CAAA,GAAI,IAAI,SAAA,EAAU;AAExB,EAAA,IAAI,CAAC,QAAA,CAAS,KAAK,CAAA,EAAG;AACpB,IAAA,CAAA,CAAE,GAAA,CAAI,KAAK,0BAA0B,CAAA;AACrC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,EAAE,MAAA,EAAO;AAAA,EACvC;AAEA,EAAA,IAAI,OAAO,MAAM,IAAI,CAAA,KAAM,YAAa,KAAA,CAAM,IAAI,CAAA,CAAa,MAAA,KAAW,CAAA,EAAG;AAC3E,IAAA,CAAA,CAAE,GAAA,CAAI,MAAM,sCAAsC,CAAA;AAAA,EACpD;AACA,EAAA,IAAI,OAAO,MAAM,SAAS,CAAA,KAAM,YAAa,KAAA,CAAM,SAAS,CAAA,CAAa,MAAA,KAAW,CAAA,EAAG;AACrF,IAAA,CAAA,CAAE,GAAA,CAAI,WAAW,2CAA2C,CAAA;AAAA,EAC9D;AAEA,EAAA,MAAM,MAAA,GAAS,MAAM,QAAQ,CAAA;AAC7B,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,EAAG;AAC1B,IAAA,CAAA,CAAE,GAAA,CAAI,UAAU,gCAAgC,CAAA;AAAA,EAClD,CAAA,MAAA,IAAW,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG;AAC9B,IAAA,CAAA,CAAE,GAAA,CAAI,UAAU,+CAA+C,CAAA;AAAA,EACjE,CAAA,MAAO;AACL,IAAA,MAAM,OAAA,uBAAc,GAAA,EAAY;AAChC,IAAA,MAAA,CAAO,OAAA,CAAQ,CAAC,CAAA,EAAG,CAAA,KAAM,aAAA,CAAc,CAAA,EAAG,CAAA,OAAA,EAAU,CAAC,CAAA,CAAA,CAAA,EAAK,OAAA,EAAS,CAAC,CAAC,CAAA;AAAA,EACvE;AAEA,EAAA,MAAM,SAAA,GAAY,MAAM,WAAW,CAAA;AACnC,EAAA,IAAI,SAAA,KAAc,MAAA,IAAa,CAAC,QAAA,CAAS,SAAS,CAAA,EAAG;AACnD,IAAA,CAAA,CAAE,GAAA,CAAI,aAAa,iDAAiD,CAAA;AAAA,EACtE;AAEA,EAAA,MAAM,MAAA,GAAS,MAAM,QAAQ,CAAA;AAC7B,EAAA,IAAI,MAAA,KAAW,MAAA,IAAa,CAAC,QAAA,CAAS,MAAM,CAAA,EAAG;AAC7C,IAAA,CAAA,CAAE,GAAA,CAAI,UAAU,8CAA8C,CAAA;AAAA,EAChE;AAEA,EAAA,IAAI,CAAA,CAAE,MAAA,CAAO,MAAA,GAAS,CAAA,EAAG;AACvB,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,EAAE,MAAA,EAAO;AAAA,EACvC;AACA,EAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,KAAA,EAAO,KAAA,EAA+B;AAC3D;AAGO,SAAS,YAAY,KAAA,EAA4B;AACtD,EAAA,MAAM,MAAA,GAAS,eAAe,KAAK,CAAA;AACnC,EAAA,IAAI,CAAC,OAAO,EAAA,EAAI;AACd,IAAA,MAAM,MAAA,GAAS,MAAA,CAAO,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,IAAA,EAAO,CAAA,CAAE,IAAI,KAAK,CAAA,CAAE,OAAO,CAAA,CAAE,CAAA,CAAE,KAAK,IAAI,CAAA;AAChF,IAAA,MAAM,IAAI,qBAAA,CAAsB,CAAA;AAAA,EAA+B,MAAM,CAAA,CAAA,EAAI,MAAA,CAAO,MAAM,CAAA;AAAA,EACxF;AACA,EAAA,OAAO,MAAA,CAAO,KAAA;AAChB;AAEO,IAAM,qBAAA,GAAN,cAAoC,KAAA,CAAM;AAAA,EACtC,MAAA;AAAA,EACT,WAAA,CAAY,SAAiB,MAAA,EAAoC;AAC/D,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,uBAAA;AACZ,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AACF;AAGO,SAAS,aAAa,KAAA,EAAqC;AAChE,EAAA,OAAO,cAAA,CAAe,KAAK,CAAA,CAAE,EAAA;AAC/B;AAGO,SAAS,SAAS,MAAA,EAA8B;AACrD,EAAA,OAAO,OAAO,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,KAAmB,EAAE,EAAE,CAAA;AACnD","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"]}
|