@formwright/schema 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Formwright contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @formwright/schema
2
+
3
+ Schema types and a dependency-free runtime validator for [Formwright](../../README.md).
4
+
5
+ The schema is Formwright's public contract: plain, serializable data that both the
6
+ runtime renderer and the codegen compiler consume. This package gives you the
7
+ TypeScript types plus a validator that produces precise, path-addressed issues —
8
+ ideal for repairing LLM-emitted schemas before they reach the runtime.
9
+
10
+ ```ts
11
+ import { validateSchema, parseSchema } from "@formwright/schema";
12
+
13
+ const result = validateSchema(unknownInput);
14
+ if (!result.ok) {
15
+ for (const issue of result.issues) console.warn(issue.path, issue.message);
16
+ }
17
+
18
+ // or throw on failure:
19
+ const schema = parseSchema(unknownInput);
20
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ // src/validate.ts
4
+ var BUILTIN_TYPES = /* @__PURE__ */ new Set([
5
+ "text",
6
+ "number",
7
+ "email",
8
+ "password",
9
+ "textarea",
10
+ "select",
11
+ "checkbox",
12
+ "radio",
13
+ "group",
14
+ "collection"
15
+ ]);
16
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set(["group", "collection"]);
17
+ function isRecord(v) {
18
+ return typeof v === "object" && v !== null && !Array.isArray(v);
19
+ }
20
+ var Collector = class {
21
+ issues = [];
22
+ add(path, message) {
23
+ this.issues.push({ path, message });
24
+ }
25
+ };
26
+ function validateField(field, path, seenIds, c) {
27
+ if (!isRecord(field)) {
28
+ c.add(path, "field must be an object");
29
+ return;
30
+ }
31
+ const id = field["id"];
32
+ if (typeof id !== "string" || id.length === 0) {
33
+ c.add(`${path}.id`, "field.id must be a non-empty string");
34
+ } else if (seenIds.has(id)) {
35
+ c.add(`${path}.id`, `duplicate field id "${id}"`);
36
+ } else {
37
+ seenIds.add(id);
38
+ }
39
+ const type = field["type"];
40
+ if (typeof type !== "string" || type.length === 0) {
41
+ c.add(`${path}.type`, "field.type must be a non-empty string");
42
+ } else if (!BUILTIN_TYPES.has(type)) ;
43
+ if ((type === "select" || type === "radio") && field["options"] === void 0) {
44
+ c.add(`${path}.options`, `field of type "${type}" should declare options`);
45
+ }
46
+ if (typeof type === "string" && CONTAINER_TYPES.has(type)) {
47
+ const nested = field["fields"];
48
+ if (!Array.isArray(nested) || nested.length === 0) {
49
+ c.add(`${path}.fields`, `field of type "${type}" must declare a non-empty "fields" array`);
50
+ } else {
51
+ const childIds = /* @__PURE__ */ new Set();
52
+ nested.forEach((f, i) => validateField(f, `${path}.fields[${i}]`, childIds, c));
53
+ }
54
+ }
55
+ }
56
+ function validateSchema(input) {
57
+ const c = new Collector();
58
+ if (!isRecord(input)) {
59
+ c.add("$", "schema must be an object");
60
+ return { ok: false, issues: c.issues };
61
+ }
62
+ if (typeof input["id"] !== "string" || input["id"].length === 0) {
63
+ c.add("id", "schema.id must be a non-empty string");
64
+ }
65
+ if (typeof input["version"] !== "string" || input["version"].length === 0) {
66
+ c.add("version", "schema.version must be a non-empty string");
67
+ }
68
+ const fields = input["fields"];
69
+ if (!Array.isArray(fields)) {
70
+ c.add("fields", "schema.fields must be an array");
71
+ } else if (fields.length === 0) {
72
+ c.add("fields", "schema.fields must contain at least one field");
73
+ } else {
74
+ const seenIds = /* @__PURE__ */ new Set();
75
+ fields.forEach((f, i) => validateField(f, `fields[${i}]`, seenIds, c));
76
+ }
77
+ const providers = input["providers"];
78
+ if (providers !== void 0 && !isRecord(providers)) {
79
+ c.add("providers", "schema.providers must be an object when present");
80
+ }
81
+ const submit = input["submit"];
82
+ if (submit !== void 0 && !isRecord(submit)) {
83
+ c.add("submit", "schema.submit must be an object when present");
84
+ }
85
+ if (c.issues.length > 0) {
86
+ return { ok: false, issues: c.issues };
87
+ }
88
+ return { ok: true, value: input };
89
+ }
90
+ function parseSchema(input) {
91
+ const result = validateSchema(input);
92
+ if (!result.ok) {
93
+ const detail = result.issues.map((i) => ` - ${i.path}: ${i.message}`).join("\n");
94
+ throw new SchemaValidationError(`Invalid Formwright schema:
95
+ ${detail}`, result.issues);
96
+ }
97
+ return result.value;
98
+ }
99
+ var SchemaValidationError = class extends Error {
100
+ issues;
101
+ constructor(message, issues) {
102
+ super(message);
103
+ this.name = "SchemaValidationError";
104
+ this.issues = issues;
105
+ }
106
+ };
107
+ function isFormSchema(input) {
108
+ return validateSchema(input).ok;
109
+ }
110
+ function fieldIds(schema) {
111
+ return schema.fields.map((f) => f.id);
112
+ }
113
+
114
+ exports.SchemaValidationError = SchemaValidationError;
115
+ exports.fieldIds = fieldIds;
116
+ exports.isFormSchema = isFormSchema;
117
+ exports.parseSchema = parseSchema;
118
+ exports.validateSchema = validateSchema;
119
+ //# sourceMappingURL=index.cjs.map
120
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Formwright schema types — the public, serializable contract.
3
+ *
4
+ * A schema is plain data: hand-written, version-controlled, or emitted by an LLM.
5
+ * Everything the runtime and the codegen compiler need is expressed here as data,
6
+ * never as code, so a schema can round-trip through JSON.
7
+ */
8
+ /** Primitive value a form field can hold. */
9
+ type FieldValue = string | number | boolean | null | undefined | FieldValue[];
10
+ /** A reference to a value provided by a registered provider, expressed as a sigil object. */
11
+ type ProviderRef = {
12
+ readonly $t: string;
13
+ readonly args?: Record<string, FieldValue>;
14
+ } | {
15
+ readonly $query: string | readonly [string, Record<string, unknown>?];
16
+ } | {
17
+ readonly $theme: string;
18
+ };
19
+ /** A value in the schema that may be a literal or resolved through a provider at runtime. */
20
+ type Resolvable<T> = T | ProviderRef;
21
+ /**
22
+ * A condition expression evaluated against current form values.
23
+ *
24
+ * Intentionally a small, sandboxed JSONLogic-style algebra — it is *data*, never
25
+ * `eval`. `var` reads a field value; the rest compose comparisons and booleans.
26
+ */
27
+ type Condition = boolean | {
28
+ readonly var: string;
29
+ } | {
30
+ readonly "==": readonly [Condition, Condition];
31
+ } | {
32
+ readonly "!=": readonly [Condition, Condition];
33
+ } | {
34
+ readonly ">": readonly [Condition, Condition];
35
+ } | {
36
+ readonly ">=": readonly [Condition, Condition];
37
+ } | {
38
+ readonly "<": readonly [Condition, Condition];
39
+ } | {
40
+ readonly "<=": readonly [Condition, Condition];
41
+ } | {
42
+ readonly in: readonly [Condition, Condition];
43
+ } | {
44
+ readonly not: Condition;
45
+ } | {
46
+ readonly and: readonly Condition[];
47
+ } | {
48
+ readonly or: readonly Condition[];
49
+ } | FieldValue;
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 & {});
52
+ /** Validation descriptor — declarative, mapped to a Standard Schema validator at runtime. */
53
+ interface ValidationSchema {
54
+ readonly kind: "string" | "number" | "boolean" | "array";
55
+ readonly required?: boolean;
56
+ readonly min?: number;
57
+ readonly max?: number;
58
+ readonly minLength?: number;
59
+ readonly maxLength?: number;
60
+ readonly pattern?: string;
61
+ readonly format?: "email" | "url" | "uuid";
62
+ readonly message?: Resolvable<string>;
63
+ }
64
+ /** A selectable option for `select` / `radio` fields. */
65
+ interface FieldOption {
66
+ readonly label: Resolvable<string>;
67
+ readonly value: FieldValue;
68
+ }
69
+ /** A single field in the form. Resolved to a widget by `type`, keyed by `id`. */
70
+ interface FieldSchema {
71
+ readonly id: string;
72
+ readonly type: FieldType;
73
+ readonly label?: Resolvable<string>;
74
+ readonly placeholder?: Resolvable<string>;
75
+ readonly help?: Resolvable<string>;
76
+ readonly defaultValue?: FieldValue;
77
+ readonly options?: Resolvable<readonly FieldOption[]>;
78
+ readonly validation?: ValidationSchema;
79
+ /** Field is rendered only when this condition holds (default: always). */
80
+ readonly visibleWhen?: Condition;
81
+ /** Field is interactive only when this condition holds (default: always). */
82
+ readonly enabledWhen?: Condition;
83
+ /** Field is required only when this condition holds (overrides validation.required). */
84
+ readonly requiredWhen?: Condition;
85
+ /**
86
+ * Child fields — required for `group` (object) and `collection` (array-of-groups).
87
+ * A child's conditions resolve names lexically: a sibling first, then the
88
+ * enclosing scope, up to the form root (so an outer toggle can hide a field
89
+ * nested inside a group or a collection row).
90
+ */
91
+ readonly fields?: readonly FieldSchema[];
92
+ /**
93
+ * Container layout:
94
+ * - `group`: `"fieldset"` (default) or `"accordion"` (collapsible section).
95
+ * - `collection`: `"list"` (default), `"cards"`, or `"accordion"` (each row collapsible).
96
+ */
97
+ readonly layout?: "fieldset" | "accordion" | "list" | "cards";
98
+ /** `collection` only: label for each row, e.g. "Contact". */
99
+ readonly itemLabel?: Resolvable<string>;
100
+ /** `collection` only: text for the add-row button (default: "Add"). */
101
+ readonly addLabel?: Resolvable<string>;
102
+ /** `collection` only: minimum / maximum number of rows. */
103
+ readonly minItems?: number;
104
+ readonly maxItems?: number;
105
+ /** Arbitrary widget-specific config, passed through to the renderer. */
106
+ readonly props?: Record<string, unknown>;
107
+ }
108
+ /** Declares a provider the form depends on, resolved by the host at runtime. */
109
+ interface ProviderDecl {
110
+ readonly type: "i18n" | "tanstack-query" | "theme" | (string & {});
111
+ readonly [key: string]: unknown;
112
+ }
113
+ /** How the form submits: transform the payload, send it, handle success/error. */
114
+ interface SubmitSchema {
115
+ /** Name of a registered transform applied to values before sending. */
116
+ readonly transform?: string;
117
+ readonly endpoint?: {
118
+ readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
119
+ readonly url: string;
120
+ };
121
+ /** Name of a registered success handler. */
122
+ readonly onSuccess?: string;
123
+ /** Name of a registered error handler. */
124
+ readonly onError?: string;
125
+ }
126
+ /** The root form schema. */
127
+ interface FormSchema {
128
+ readonly id: string;
129
+ readonly version: string;
130
+ readonly title?: Resolvable<string>;
131
+ readonly providers?: Record<string, ProviderDecl>;
132
+ readonly fields: readonly FieldSchema[];
133
+ readonly submit?: SubmitSchema;
134
+ }
135
+
136
+ /**
137
+ * Runtime validation of an unknown value into a typed {@link FormSchema}.
138
+ *
139
+ * Dependency-free and structural — its job is to give precise, path-addressed
140
+ * errors so an LLM-emitted schema can be repaired (or rejected) before it ever
141
+ * reaches the runtime. This validates the *schema shape*, not user form input
142
+ * (that is the job of the per-field {@link ValidationSchema}).
143
+ */
144
+
145
+ interface ValidationIssue {
146
+ /** JSON-pointer-ish path to the offending node, e.g. `fields[2].type`. */
147
+ readonly path: string;
148
+ readonly message: string;
149
+ }
150
+ type ValidationResult = {
151
+ readonly ok: true;
152
+ readonly value: FormSchema;
153
+ } | {
154
+ readonly ok: false;
155
+ readonly issues: readonly ValidationIssue[];
156
+ };
157
+ /** Validate an unknown value as a {@link FormSchema}. Never throws. */
158
+ declare function validateSchema(input: unknown): ValidationResult;
159
+ /** Validate and throw a single aggregated error on failure. Convenience for trusted input. */
160
+ declare function parseSchema(input: unknown): FormSchema;
161
+ declare class SchemaValidationError extends Error {
162
+ readonly issues: readonly ValidationIssue[];
163
+ constructor(message: string, issues: readonly ValidationIssue[]);
164
+ }
165
+ /** Type guard form of {@link validateSchema}. */
166
+ declare function isFormSchema(input: unknown): input is FormSchema;
167
+ /** Convenience: list the field ids declared by a schema, in order. */
168
+ declare function fieldIds(schema: FormSchema): string[];
169
+
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 };
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Formwright schema types — the public, serializable contract.
3
+ *
4
+ * A schema is plain data: hand-written, version-controlled, or emitted by an LLM.
5
+ * Everything the runtime and the codegen compiler need is expressed here as data,
6
+ * never as code, so a schema can round-trip through JSON.
7
+ */
8
+ /** Primitive value a form field can hold. */
9
+ type FieldValue = string | number | boolean | null | undefined | FieldValue[];
10
+ /** A reference to a value provided by a registered provider, expressed as a sigil object. */
11
+ type ProviderRef = {
12
+ readonly $t: string;
13
+ readonly args?: Record<string, FieldValue>;
14
+ } | {
15
+ readonly $query: string | readonly [string, Record<string, unknown>?];
16
+ } | {
17
+ readonly $theme: string;
18
+ };
19
+ /** A value in the schema that may be a literal or resolved through a provider at runtime. */
20
+ type Resolvable<T> = T | ProviderRef;
21
+ /**
22
+ * A condition expression evaluated against current form values.
23
+ *
24
+ * Intentionally a small, sandboxed JSONLogic-style algebra — it is *data*, never
25
+ * `eval`. `var` reads a field value; the rest compose comparisons and booleans.
26
+ */
27
+ type Condition = boolean | {
28
+ readonly var: string;
29
+ } | {
30
+ readonly "==": readonly [Condition, Condition];
31
+ } | {
32
+ readonly "!=": readonly [Condition, Condition];
33
+ } | {
34
+ readonly ">": readonly [Condition, Condition];
35
+ } | {
36
+ readonly ">=": readonly [Condition, Condition];
37
+ } | {
38
+ readonly "<": readonly [Condition, Condition];
39
+ } | {
40
+ readonly "<=": readonly [Condition, Condition];
41
+ } | {
42
+ readonly in: readonly [Condition, Condition];
43
+ } | {
44
+ readonly not: Condition;
45
+ } | {
46
+ readonly and: readonly Condition[];
47
+ } | {
48
+ readonly or: readonly Condition[];
49
+ } | FieldValue;
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 & {});
52
+ /** Validation descriptor — declarative, mapped to a Standard Schema validator at runtime. */
53
+ interface ValidationSchema {
54
+ readonly kind: "string" | "number" | "boolean" | "array";
55
+ readonly required?: boolean;
56
+ readonly min?: number;
57
+ readonly max?: number;
58
+ readonly minLength?: number;
59
+ readonly maxLength?: number;
60
+ readonly pattern?: string;
61
+ readonly format?: "email" | "url" | "uuid";
62
+ readonly message?: Resolvable<string>;
63
+ }
64
+ /** A selectable option for `select` / `radio` fields. */
65
+ interface FieldOption {
66
+ readonly label: Resolvable<string>;
67
+ readonly value: FieldValue;
68
+ }
69
+ /** A single field in the form. Resolved to a widget by `type`, keyed by `id`. */
70
+ interface FieldSchema {
71
+ readonly id: string;
72
+ readonly type: FieldType;
73
+ readonly label?: Resolvable<string>;
74
+ readonly placeholder?: Resolvable<string>;
75
+ readonly help?: Resolvable<string>;
76
+ readonly defaultValue?: FieldValue;
77
+ readonly options?: Resolvable<readonly FieldOption[]>;
78
+ readonly validation?: ValidationSchema;
79
+ /** Field is rendered only when this condition holds (default: always). */
80
+ readonly visibleWhen?: Condition;
81
+ /** Field is interactive only when this condition holds (default: always). */
82
+ readonly enabledWhen?: Condition;
83
+ /** Field is required only when this condition holds (overrides validation.required). */
84
+ readonly requiredWhen?: Condition;
85
+ /**
86
+ * Child fields — required for `group` (object) and `collection` (array-of-groups).
87
+ * A child's conditions resolve names lexically: a sibling first, then the
88
+ * enclosing scope, up to the form root (so an outer toggle can hide a field
89
+ * nested inside a group or a collection row).
90
+ */
91
+ readonly fields?: readonly FieldSchema[];
92
+ /**
93
+ * Container layout:
94
+ * - `group`: `"fieldset"` (default) or `"accordion"` (collapsible section).
95
+ * - `collection`: `"list"` (default), `"cards"`, or `"accordion"` (each row collapsible).
96
+ */
97
+ readonly layout?: "fieldset" | "accordion" | "list" | "cards";
98
+ /** `collection` only: label for each row, e.g. "Contact". */
99
+ readonly itemLabel?: Resolvable<string>;
100
+ /** `collection` only: text for the add-row button (default: "Add"). */
101
+ readonly addLabel?: Resolvable<string>;
102
+ /** `collection` only: minimum / maximum number of rows. */
103
+ readonly minItems?: number;
104
+ readonly maxItems?: number;
105
+ /** Arbitrary widget-specific config, passed through to the renderer. */
106
+ readonly props?: Record<string, unknown>;
107
+ }
108
+ /** Declares a provider the form depends on, resolved by the host at runtime. */
109
+ interface ProviderDecl {
110
+ readonly type: "i18n" | "tanstack-query" | "theme" | (string & {});
111
+ readonly [key: string]: unknown;
112
+ }
113
+ /** How the form submits: transform the payload, send it, handle success/error. */
114
+ interface SubmitSchema {
115
+ /** Name of a registered transform applied to values before sending. */
116
+ readonly transform?: string;
117
+ readonly endpoint?: {
118
+ readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
119
+ readonly url: string;
120
+ };
121
+ /** Name of a registered success handler. */
122
+ readonly onSuccess?: string;
123
+ /** Name of a registered error handler. */
124
+ readonly onError?: string;
125
+ }
126
+ /** The root form schema. */
127
+ interface FormSchema {
128
+ readonly id: string;
129
+ readonly version: string;
130
+ readonly title?: Resolvable<string>;
131
+ readonly providers?: Record<string, ProviderDecl>;
132
+ readonly fields: readonly FieldSchema[];
133
+ readonly submit?: SubmitSchema;
134
+ }
135
+
136
+ /**
137
+ * Runtime validation of an unknown value into a typed {@link FormSchema}.
138
+ *
139
+ * Dependency-free and structural — its job is to give precise, path-addressed
140
+ * errors so an LLM-emitted schema can be repaired (or rejected) before it ever
141
+ * reaches the runtime. This validates the *schema shape*, not user form input
142
+ * (that is the job of the per-field {@link ValidationSchema}).
143
+ */
144
+
145
+ interface ValidationIssue {
146
+ /** JSON-pointer-ish path to the offending node, e.g. `fields[2].type`. */
147
+ readonly path: string;
148
+ readonly message: string;
149
+ }
150
+ type ValidationResult = {
151
+ readonly ok: true;
152
+ readonly value: FormSchema;
153
+ } | {
154
+ readonly ok: false;
155
+ readonly issues: readonly ValidationIssue[];
156
+ };
157
+ /** Validate an unknown value as a {@link FormSchema}. Never throws. */
158
+ declare function validateSchema(input: unknown): ValidationResult;
159
+ /** Validate and throw a single aggregated error on failure. Convenience for trusted input. */
160
+ declare function parseSchema(input: unknown): FormSchema;
161
+ declare class SchemaValidationError extends Error {
162
+ readonly issues: readonly ValidationIssue[];
163
+ constructor(message: string, issues: readonly ValidationIssue[]);
164
+ }
165
+ /** Type guard form of {@link validateSchema}. */
166
+ declare function isFormSchema(input: unknown): input is FormSchema;
167
+ /** Convenience: list the field ids declared by a schema, in order. */
168
+ declare function fieldIds(schema: FormSchema): string[];
169
+
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 };
package/dist/index.js ADDED
@@ -0,0 +1,114 @@
1
+ // src/validate.ts
2
+ var BUILTIN_TYPES = /* @__PURE__ */ new Set([
3
+ "text",
4
+ "number",
5
+ "email",
6
+ "password",
7
+ "textarea",
8
+ "select",
9
+ "checkbox",
10
+ "radio",
11
+ "group",
12
+ "collection"
13
+ ]);
14
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set(["group", "collection"]);
15
+ function isRecord(v) {
16
+ return typeof v === "object" && v !== null && !Array.isArray(v);
17
+ }
18
+ var Collector = class {
19
+ issues = [];
20
+ add(path, message) {
21
+ this.issues.push({ path, message });
22
+ }
23
+ };
24
+ function validateField(field, path, seenIds, c) {
25
+ if (!isRecord(field)) {
26
+ c.add(path, "field must be an object");
27
+ return;
28
+ }
29
+ const id = field["id"];
30
+ if (typeof id !== "string" || id.length === 0) {
31
+ c.add(`${path}.id`, "field.id must be a non-empty string");
32
+ } else if (seenIds.has(id)) {
33
+ c.add(`${path}.id`, `duplicate field id "${id}"`);
34
+ } else {
35
+ seenIds.add(id);
36
+ }
37
+ const type = field["type"];
38
+ if (typeof type !== "string" || type.length === 0) {
39
+ c.add(`${path}.type`, "field.type must be a non-empty string");
40
+ } else if (!BUILTIN_TYPES.has(type)) ;
41
+ if ((type === "select" || type === "radio") && field["options"] === void 0) {
42
+ c.add(`${path}.options`, `field of type "${type}" should declare options`);
43
+ }
44
+ if (typeof type === "string" && CONTAINER_TYPES.has(type)) {
45
+ const nested = field["fields"];
46
+ if (!Array.isArray(nested) || nested.length === 0) {
47
+ c.add(`${path}.fields`, `field of type "${type}" must declare a non-empty "fields" array`);
48
+ } else {
49
+ const childIds = /* @__PURE__ */ new Set();
50
+ nested.forEach((f, i) => validateField(f, `${path}.fields[${i}]`, childIds, c));
51
+ }
52
+ }
53
+ }
54
+ function validateSchema(input) {
55
+ const c = new Collector();
56
+ if (!isRecord(input)) {
57
+ c.add("$", "schema must be an object");
58
+ return { ok: false, issues: c.issues };
59
+ }
60
+ if (typeof input["id"] !== "string" || input["id"].length === 0) {
61
+ c.add("id", "schema.id must be a non-empty string");
62
+ }
63
+ if (typeof input["version"] !== "string" || input["version"].length === 0) {
64
+ c.add("version", "schema.version must be a non-empty string");
65
+ }
66
+ const fields = input["fields"];
67
+ if (!Array.isArray(fields)) {
68
+ c.add("fields", "schema.fields must be an array");
69
+ } else if (fields.length === 0) {
70
+ c.add("fields", "schema.fields must contain at least one field");
71
+ } else {
72
+ const seenIds = /* @__PURE__ */ new Set();
73
+ fields.forEach((f, i) => validateField(f, `fields[${i}]`, seenIds, c));
74
+ }
75
+ const providers = input["providers"];
76
+ if (providers !== void 0 && !isRecord(providers)) {
77
+ c.add("providers", "schema.providers must be an object when present");
78
+ }
79
+ const submit = input["submit"];
80
+ if (submit !== void 0 && !isRecord(submit)) {
81
+ c.add("submit", "schema.submit must be an object when present");
82
+ }
83
+ if (c.issues.length > 0) {
84
+ return { ok: false, issues: c.issues };
85
+ }
86
+ return { ok: true, value: input };
87
+ }
88
+ function parseSchema(input) {
89
+ const result = validateSchema(input);
90
+ if (!result.ok) {
91
+ const detail = result.issues.map((i) => ` - ${i.path}: ${i.message}`).join("\n");
92
+ throw new SchemaValidationError(`Invalid Formwright schema:
93
+ ${detail}`, result.issues);
94
+ }
95
+ return result.value;
96
+ }
97
+ var SchemaValidationError = class extends Error {
98
+ issues;
99
+ constructor(message, issues) {
100
+ super(message);
101
+ this.name = "SchemaValidationError";
102
+ this.issues = issues;
103
+ }
104
+ };
105
+ function isFormSchema(input) {
106
+ return validateSchema(input).ok;
107
+ }
108
+ function fieldIds(schema) {
109
+ return schema.fields.map((f) => f.id);
110
+ }
111
+
112
+ export { SchemaValidationError, fieldIds, isFormSchema, parseSchema, validateSchema };
113
+ //# sourceMappingURL=index.js.map
114
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@formwright/schema",
3
+ "version": "0.1.0",
4
+ "description": "Schema types and runtime validator for Formwright.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/aliarsalan177/formwright.git",
9
+ "directory": "packages/schema"
10
+ },
11
+ "homepage": "https://github.com/aliarsalan177/formwright#readme",
12
+ "bugs": "https://github.com/aliarsalan177/formwright/issues",
13
+ "type": "module",
14
+ "sideEffects": false,
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs"
23
+ }
24
+ },
25
+ "main": "./dist/index.cjs",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "build": "tsup",
33
+ "dev": "tsup --watch",
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "vitest run",
36
+ "clean": "rm -rf dist .turbo"
37
+ }
38
+ }