@jarenjs/forms 0.9.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Joham
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,196 @@
1
+ # @jarenjs/forms
2
+
3
+ Framework-agnostic form generation for JSON Schema. Turns a schema into a renderable field tree and validates it in **three layers, one stack**:
4
+
5
+ 1. **Per field, every keystroke** — cheap, synchronous checks powered directly by [`@jarenjs/core`](../core) primitives (grapheme-aware string lengths, unicode patterns, deep equality) and the canonical format-tester registry of [`@jarenjs/formats`](../formats): everything one field can know about itself.
6
+ 2. **Cross field, every keystroke** — visibility, enablement, computed values, and preemptive assertions expressed as [Jaren JSON Query](../json/docs/QUERY-FORMAT.md) documents in an `x-form` annotation, compiled once per model by [`@jarenjs/json`](../json) and evaluated per keystroke as cheap closures.
7
+ 3. **Authoritative, on submit** — the complete compiled schema validation with [`@jarenjs/validate`](../validate), which owns `required` combinations, `dependentSchemas`, `if/then/else`, `unevaluatedProperties`, and (via the `$query` keyword) the very same cross-field rules.
8
+
9
+ No DOM, no framework: render the model with React, Vue, vanilla JS or anything else. Forms never imports the validator — apps wire the authoritative layer themselves. See it in action in the [Jaren playground](https://jklarenbeek.github.io/jarenjs/#/playground).
10
+
11
+ ## Usage
12
+
13
+ ```javascript
14
+ import {
15
+ buildFormModel,
16
+ createInitialData,
17
+ validateField,
18
+ parseFieldInput,
19
+ setValueAtPointer,
20
+ } from '@jarenjs/forms';
21
+
22
+ const schema = {
23
+ type: 'object',
24
+ title: 'Sign up',
25
+ properties: {
26
+ username: { type: 'string', minLength: 3, pattern: '^[a-z0-9_]+$' },
27
+ email: { type: 'string', format: 'email' },
28
+ age: { type: 'integer', minimum: 13 },
29
+ },
30
+ required: ['username', 'email'],
31
+ };
32
+
33
+ // 1. Build the field tree once
34
+ const model = buildFormModel(schema);
35
+ // model.children -> [{ pointer: '/username', label: 'Username', control: 'text',
36
+ // required: true, constraints: {...} }, ...]
37
+
38
+ // 2. Start with the schema's defaults (untouched fields stay absent)
39
+ let data = createInitialData(model);
40
+
41
+ // 3. On every keystroke: coerce the raw input and validate the field
42
+ const field = model.children.find((f) => f.key === 'email');
43
+ const value = parseFieldInput(field, 'not-an-email'); // '' -> undefined, numbers -> Number, ...
44
+ const errors = validateField(field, value);
45
+ // [{ keyword: 'format', message: 'Must be a valid email' }]
46
+
47
+ data = setValueAtPointer(data, field.pointer, value); // immutable update
48
+
49
+ // 4. On submit (or continuously): the authoritative validation
50
+ import { JarenValidator } from '@jarenjs/validate';
51
+ const validate = new JarenValidator({ skipErrors: false, collectErrors: true }).compile(schema);
52
+ const result = validate(data); // { valid, errors: [{ instancePath, keyword, message, ... }] }
53
+ ```
54
+
55
+ ## The form model
56
+
57
+ `buildFormModel(schema)` resolves local `$ref`s (`#/$defs/...`), merges `allOf` branches, and returns a tree of field descriptors:
58
+
59
+ | Property | Meaning |
60
+ |---|---|
61
+ | `pointer` | JSON pointer into the data (`/user/name`) |
62
+ | `label` | `title` or a humanized property name (`firstName` → "First Name") |
63
+ | `kind` | `string` `number` `integer` `boolean` `enum` `const` `object` `array` |
64
+ | `control` | Rendering hint: `text` `email` `url` `password` `textarea` `number` `checkbox` `select` `date` `color` `json` |
65
+ | `required` | Whether the parent object requires this property |
66
+ | `constraints` | `minLength`/`maxLength`/`pattern`/`format`/`minimum`/`maximum`/`multipleOf`/`minItems`/... |
67
+ | `rules` | The raw `x-form` rules annotation, if any (see below) |
68
+ | `enumValues` / `constValue` / `defaultValue` / `placeholder` | Values for the UI |
69
+ | `children` | Child fields (object kinds) |
70
+ | `item` / `tuple` | Item template and tuple prefix fields (array kinds) |
71
+
72
+ Field kinds are inferred from structural keywords when `type` is absent, and `format` maps to input controls and placeholders through the same registry the preemptive validation uses (`getFormatInfo`).
73
+
74
+ ## Layer 1 — preemptive per-field validation
75
+
76
+ `validateField(field, value)` returns `[{ keyword, message }]` using `@jarenjs/core` directly:
77
+
78
+ - **strings**: grapheme-aware `minLength`/`maxLength` (`getStringLength`), unicode `pattern` (`createRegExp`, cached), and 50+ `format` testers from the [`@jarenjs/formats`](../formats) `formatTesters` registry — the same name → predicate table the authoritative validator's format compilers wrap, so both layers accept exactly the same strings
79
+ - **numbers**: type/integer checks, bounds, `multipleOf`
80
+ - **enum/const**: deep equality (`equalsDeep`)
81
+ - **arrays**: `minItems`/`maxItems`/`uniqueItems` (`isUniqueDeepArray`)
82
+
83
+ `validateAllFields(model, data)` walks the whole tree and returns a `{ '/pointer': errors }` map — ideal for rendering inline errors next to every field.
84
+
85
+ ## Layer 2 — `x-form` rules: cross-field behavior per keystroke
86
+
87
+ One namespaced annotation keyword — safe under every metaschema, invisible to validators — on any subschema. Its members are [Jaren JSON Query](../json/docs/QUERY-FORMAT.md) documents (a bare RFC 9535 JSONPath string is the degenerate query):
88
+
89
+ ```json
90
+ { "type": "object",
91
+ "properties": {
92
+ "company": { "type": "string" },
93
+ "vatId": { "type": "string",
94
+ "x-form": { "visible": { "$ne": ["$.company", ""] },
95
+ "assert": { "$or": [ { "$eq": ["$.company", ""] },
96
+ { "$ne": ["$.vatId", ""] } ] },
97
+ "message": "VAT id is required for companies" } },
98
+ "total": { "type": "number",
99
+ "x-form": { "computed": { "$sum": "$.lines[*].amount" } } }
100
+ } }
101
+ ```
102
+
103
+ Recognized members — unknown members are ignored for forward compatibility:
104
+
105
+ | Member | Kind | Meaning |
106
+ |---|---|---|
107
+ | `visible` | EBV query | Should the field be shown? |
108
+ | `enabled` | EBV query | Should the field accept input? |
109
+ | `assert` | EBV query | Cross-field preemptive validation |
110
+ | `computed` | query | The field's derived value, mapped to plain JSON |
111
+ | `message` | string | Shown when `assert` fails |
112
+
113
+ Rules compile **once per model** and evaluate per keystroke:
114
+
115
+ ```javascript
116
+ import { buildFormModel, compileFormRules, evaluateFormRules } from '@jarenjs/forms';
117
+
118
+ const model = buildFormModel(schema);
119
+ const rules = compileFormRules(model); // throws on malformed rules, with the field pointer
120
+
121
+ // per keystroke, after updating `data`:
122
+ const state = evaluateFormRules(rules, data);
123
+ // { '/vatId': { visible: true, errors: [{ keyword: 'x-form/assert',
124
+ // message: 'VAT id is required for companies' }] },
125
+ // '/total': { computed: 20 } }
126
+ ```
127
+
128
+ ### The rule query context
129
+
130
+ Every rule kind shares one context:
131
+
132
+ - **`$`** — the input document is the **whole form data root**: cross-field is the point.
133
+ - **`$value`** — the field's current value, bound as an external per evaluation. An absent field binds `null` (`undefined` is not a JSON value).
134
+ - **`$pointer`** — the field's data pointer string (`'/vatId'`).
135
+
136
+ These two externals are the whole vocabulary: any other free name in a rule is a **compile-time** error naming it.
137
+
138
+ `visible`/`enabled`/`assert` are asserted by **effective boolean value** (EBV, [QUERY-FORMAT.md §2.2](../json/docs/QUERY-FORMAT.md)): the empty sequence is `false`, a singleton counts per its type, a multi-item sequence is runtime error `JQ2003`. Runtime errors follow a fixed policy: `visible`/`enabled` **fail open** (evaluate to `true` — a broken rule must never hide data or lock a control), `assert` **fails closed** (an assertion that cannot be computed has not been satisfied), and `computed` leaves the value absent.
139
+
140
+ ### Array item templates
141
+
142
+ A rule on an array item template (`/lines/-/amount`) compiles **once** and evaluates **per element** of the actual array, binding `$value`/`$pointer` per index — results are keyed by the expanded pointer (`/lines/2/amount`). That compiled-once/dispatch-per-node generalization now exists as the [`@jarenjs/json/jslt`](../json/docs/JSLT-FORMAT.md) `$apply` engine: a future forms computed-view layer can generalize `x-form.computed` into schema-dispatched view-model stylesheets without changing forms' validator-independent boundary.
143
+
144
+ ### Schema literals in rules
145
+
146
+ Rules may use the query engine's schema operators (`$valid`/`$as`) by passing the same `compileTypeTest` hook the engine defines ([QUERY-FORMAT.md §8.11](../json/docs/QUERY-FORMAT.md)) — this is the only door through which a validator reaches forms, and the app holds the key:
147
+
148
+ ```javascript
149
+ import { createTypeTestCompiler } from '@jarenjs/validate/query'; // app-side, not a forms dependency
150
+ const rules = compileFormRules(model, { compileTypeTest: createTypeTestCompiler() });
151
+ ```
152
+
153
+ ### Composing rule errors with field errors
154
+
155
+ `validateAllFields` and `evaluateFormRules` stay separate on purpose (a render loop usually wants them at different times). Both speak the same error shape, so merging is one spread per pointer:
156
+
157
+ ```javascript
158
+ const fieldErrors = validateAllFields(model, data); // layer 1
159
+ const ruleState = evaluateFormRules(rules, data); // layer 2
160
+ const errorsAt = (pointer) => [
161
+ ...(fieldErrors[pointer] ?? []),
162
+ ...(ruleState[pointer]?.errors ?? []),
163
+ ];
164
+ ```
165
+
166
+ ## Layer 3 — write the rule once, enforce it on submit
167
+
168
+ The same constraint can be spelled twice — `x-form.assert` for keystroke feedback, the [`$query` keyword](../validate) for authoritative submit validation — or written **once** and copied:
169
+
170
+ ```javascript
171
+ import { formRulesToQueryAssertions } from '@jarenjs/forms';
172
+
173
+ // pure schema-to-schema transform: every x-form.assert is copied into a
174
+ // root-level $query (joined to an existing one through allOf), with
175
+ // value/pointer rebound to the field's location
176
+ const submitSchema = formRulesToQueryAssertions(schema);
177
+
178
+ import { JarenValidator } from '@jarenjs/validate'; // app-side
179
+ const validate = new JarenValidator().compile(submitSchema);
180
+ validate({ company: 'ACME', vatId: '' }); // false - the vatId assert, now authoritative
181
+ ```
182
+
183
+ Item-template asserts quantify with `$every` over the actual elements. Two divergences from the keystroke path are inherent to the copy: on submit an absent field binds `$value` to the empty sequence (not `null`), and `$pointer` for template elements stays the template pointer (element indexes are a render-time notion).
184
+
185
+ ## Data helpers
186
+
187
+ Form data keeps plain JSON semantics — an untouched field is *absent*, not an empty string. Pointers parse and read through the [`@jarenjs/json`](../json) compiled pointer engine (RFC 6901, one implementation repo-wide); reads hit a compiled-getter cache and allocate nothing:
188
+
189
+ - `createInitialData(model)` — defaults and `const` values filled in, everything else absent
190
+ - `parseFieldInput(field, raw)` — input coercion (`''` → undefined, numeric strings → numbers, enum options → typed values)
191
+ - `getValueAtPointer` / `setValueAtPointer` / `appendItem` / `removeItemAt` — immutable updates addressed by JSON pointer
192
+ - `createItemValue(field.item)` — starter value for a new array item
193
+
194
+ ## Development
195
+
196
+ Unit tests live in `test/forms/` at the repository root. See the repository [README](../../README.md) for the full Jaren documentation, and the [ROADMAP](../../ROADMAP.md) for planned forms work (rule dependency memoization, hidden-field pruning on submit, computed views through JSLT).
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Split a JSON pointer into decoded segments per RFC 6901. '' -> [].
3
+ * @param {string} pointer
4
+ * @returns {string[]}
5
+ * @throws {import('@jarenjs/json/pointer').JSONPointerSyntaxError}
6
+ * When the pointer violates the RFC 6901 grammar
7
+ */
8
+ export declare function parsePointer(pointer: string): string[];
9
+ /**
10
+ * Read the value at a JSON pointer.
11
+ * @param {any} data
12
+ * @param {string} pointer - e.g. '/user/address/0/street'
13
+ * @returns {any} The value, or undefined when the path does not exist
14
+ */
15
+ export declare function getValueAtPointer(data: any, pointer: string): any;
16
+ /**
17
+ * Return a copy of `data` with the value at `pointer` replaced.
18
+ * Setting `undefined` REMOVES the property (array items become undefined
19
+ * holes only when explicitly set; use removeItemAt to delete them).
20
+ * Missing intermediate containers are created (objects for name segments,
21
+ * arrays for numeric segments).
22
+ * @param {any} data
23
+ * @param {string} pointer
24
+ * @param {any} value
25
+ * @returns {any} The new root value
26
+ */
27
+ export declare function setValueAtPointer(data: any, pointer: string, value: any): any;
28
+ /**
29
+ * Return a copy of `data` with the item at `index` removed from the array
30
+ * at `pointer`.
31
+ * @param {any} data
32
+ * @param {string} pointer - Pointer to the ARRAY
33
+ * @param {number} index
34
+ * @returns {any}
35
+ */
36
+ export declare function removeItemAt(data: any, pointer: string, index: number): any;
37
+ /**
38
+ * Return a copy of `data` with `value` appended to the array at `pointer`
39
+ * (the array is created when absent).
40
+ * @param {any} data
41
+ * @param {string} pointer - Pointer to the ARRAY
42
+ * @param {any} value
43
+ * @returns {any}
44
+ */
45
+ export declare function appendItem(data: any, pointer: string, value: any): any;
46
+ /**
47
+ * Create initial data for a form model: schema defaults and const values
48
+ * are filled in, everything else stays absent.
49
+ * @param {import('./model.js').FormField} field - A field from buildFormModel
50
+ * @returns {any}
51
+ */
52
+ export declare function createInitialData(field: import('./model.js').FormField): any;
53
+ /**
54
+ * Create a sensible starter value for one array item of the given field.
55
+ * @param {import('./model.js').FormField} itemField
56
+ * @returns {any}
57
+ */
58
+ export declare function createItemValue(itemField: import('./model.js').FormField): any;
59
+ /**
60
+ * Coerce a raw input string (what an HTML input yields) into the typed
61
+ * value for a field. An empty string means "absent" (undefined) so that
62
+ * required/optional semantics stay correct.
63
+ * @param {import('./model.js').FormField} field
64
+ * @param {any} raw - Raw input value (string, or boolean for checkboxes)
65
+ * @returns {any}
66
+ */
67
+ export declare function parseFieldInput(field: import('./model.js').FormField, raw: any): any;
@@ -0,0 +1,22 @@
1
+ export type FormatInfo = {
2
+ /**
3
+ * - Synchronous validity test
4
+ */
5
+ test: (value: string) => boolean;
6
+ /**
7
+ * - Suggested HTML input control
8
+ */
9
+ control: string;
10
+ /**
11
+ * - Suggested placeholder text
12
+ */
13
+ placeholder?: string;
14
+ };
15
+ /** @type {Record<string, FormatInfo>} */
16
+ export declare const FORM_FORMATS: Record<string, FormatInfo>;
17
+ /**
18
+ * Look up the format info for a JSON Schema format name.
19
+ * @param {string|undefined} format - The format name
20
+ * @returns {FormatInfo|null} The format info, or null when unknown
21
+ */
22
+ export declare function getFormatInfo(format: string | undefined): FormatInfo | null;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @jarenjs/forms - framework-agnostic form model generation for JSON Schema.
3
+ *
4
+ * buildFormModel(schema) turns a schema into a renderable field tree;
5
+ * validateField gives immediate per-field feedback powered by @jarenjs/core
6
+ * primitives, and the `x-form` rules (rules.js) add cross-field behavior -
7
+ * visibility, enablement, computed values, preemptive assertions - as
8
+ * compiled Jaren JSON Queries, before the complete compiled schema
9
+ * validation runs (app-wired; forms never imports the validator). The
10
+ * data helpers keep form values in plain JSON semantics, addressed by
11
+ * JSON pointer through the @jarenjs/json compiled pointer engine.
12
+ */
13
+ export { buildFormModel, resolveSchema, getFieldKind, humanizeKey, escapePointerKey, } from './model.js';
14
+ export { compileFormRules, evaluateFormRules, formRulesToQueryAssertions, } from './rules.js';
15
+ export { validateField, validateAllFields, } from './validate.js';
16
+ export { createInitialData, createItemValue, parseFieldInput, parsePointer, getValueAtPointer, setValueAtPointer, appendItem, removeItemAt, } from './data.js';
17
+ export { FORM_FORMATS, getFormatInfo, } from './formats.js';
@@ -0,0 +1,125 @@
1
+ export type FormField = {
2
+ /**
3
+ * - JSON pointer into the DATA (e.g. '/user/name')
4
+ */
5
+ pointer: string;
6
+ /**
7
+ * - Property name (or '-' for an array item template)
8
+ */
9
+ key: string;
10
+ /**
11
+ * - Human friendly label (schema title or humanized key)
12
+ */
13
+ label: string;
14
+ description: string | undefined;
15
+ /**
16
+ * - The resolved subschema for this field
17
+ */
18
+ schema: object;
19
+ /**
20
+ * - 'string'|'number'|'integer'|'boolean'|'enum'|'const'|'object'|'array'|'unknown'
21
+ */
22
+ kind: string;
23
+ /**
24
+ * - Suggested control: 'text'|'email'|'url'|'password'|'textarea'|'number'|'checkbox'|'select'|'date'|'color'|'json'
25
+ */
26
+ control: string;
27
+ /**
28
+ * - Whether the parent object requires this property
29
+ */
30
+ required: boolean;
31
+ readOnly: boolean;
32
+ /**
33
+ * - Options for a select control
34
+ */
35
+ enumValues: Array<any> | null;
36
+ /**
37
+ * - Fixed value when the schema is a const
38
+ */
39
+ constValue: any;
40
+ defaultValue: any;
41
+ placeholder: string | undefined;
42
+ /**
43
+ * - minLength/maxLength/pattern/minimum/... extracted for the UI
44
+ */
45
+ constraints: object;
46
+ /**
47
+ * - The raw `x-form` rules annotation, if any (see rules.js)
48
+ */
49
+ rules: object | null;
50
+ /**
51
+ * - Child fields for object kinds
52
+ */
53
+ children: Array<FormField> | null;
54
+ /**
55
+ * - Template field for array items
56
+ */
57
+ item: FormField | null;
58
+ /**
59
+ * - Fixed prefix fields for tuple arrays
60
+ */
61
+ tuple: Array<FormField> | null;
62
+ };
63
+ /**
64
+ * @typedef {object} FormField
65
+ * @property {string} pointer - JSON pointer into the DATA (e.g. '/user/name')
66
+ * @property {string} key - Property name (or '-' for an array item template)
67
+ * @property {string} label - Human friendly label (schema title or humanized key)
68
+ * @property {string|undefined} description
69
+ * @property {object} schema - The resolved subschema for this field
70
+ * @property {string} kind - 'string'|'number'|'integer'|'boolean'|'enum'|'const'|'object'|'array'|'unknown'
71
+ * @property {string} control - Suggested control: 'text'|'email'|'url'|'password'|'textarea'|'number'|'checkbox'|'select'|'date'|'color'|'json'
72
+ * @property {boolean} required - Whether the parent object requires this property
73
+ * @property {boolean} readOnly
74
+ * @property {Array<any>|null} enumValues - Options for a select control
75
+ * @property {any} constValue - Fixed value when the schema is a const
76
+ * @property {any} defaultValue
77
+ * @property {string|undefined} placeholder
78
+ * @property {object} constraints - minLength/maxLength/pattern/minimum/... extracted for the UI
79
+ * @property {object|null} rules - The raw `x-form` rules annotation, if any (see rules.js)
80
+ * @property {Array<FormField>|null} children - Child fields for object kinds
81
+ * @property {FormField|null} item - Template field for array items
82
+ * @property {Array<FormField>|null} tuple - Fixed prefix fields for tuple arrays
83
+ */
84
+ /**
85
+ * Convert 'firstName' / 'first_name' / 'first-name' to 'First Name'.
86
+ * @param {string} key
87
+ * @returns {string}
88
+ */
89
+ export declare function humanizeKey(key: string): string;
90
+ /**
91
+ * Resolve local $refs and shallowly merge allOf branches into a single
92
+ * effective schema object for form purposes.
93
+ * @param {object|boolean} schema
94
+ * @param {object} rootSchema
95
+ * @param {number} depth
96
+ * @returns {object|boolean}
97
+ */
98
+ export declare function resolveSchema(schema: object | boolean, rootSchema: object, depth?: number): object | boolean;
99
+ /**
100
+ * Derive the field kind from a resolved schema.
101
+ * @param {object|boolean} schema
102
+ * @returns {string}
103
+ */
104
+ export declare function getFieldKind(schema: object | boolean): string;
105
+ /**
106
+ * Encode a property name as an RFC 6901 reference token (`~` -> `~0`,
107
+ * `/` -> `~1`), the write-side inverse of the shared parse.
108
+ * @param {string} key
109
+ * @returns {string}
110
+ */
111
+ export declare function escapePointerKey(key: string): string;
112
+ /**
113
+ * Build the form model for a JSON schema.
114
+ *
115
+ * @param {object|boolean} schema - The root JSON schema
116
+ * @returns {FormField} The root field descriptor (kind 'object' for object schemas)
117
+ * @example
118
+ * const model = buildFormModel({
119
+ * type: 'object',
120
+ * properties: { email: { type: 'string', format: 'email' } },
121
+ * required: ['email'],
122
+ * });
123
+ * model.children[0].control; // 'email'
124
+ */
125
+ export declare function buildFormModel(schema: object | boolean): FormField;
@@ -0,0 +1,131 @@
1
+ export type RuleResult = {
2
+ /**
3
+ * - EBV of the field's `visible` rule
4
+ */
5
+ visible?: boolean;
6
+ /**
7
+ * - EBV of the field's `enabled` rule
8
+ */
9
+ enabled?: boolean;
10
+ /**
11
+ * - Plain-JSON result of the `computed` rule
12
+ */
13
+ computed?: any;
14
+ /**
15
+ * `[{ keyword: 'x-form/assert', message }]` when the `assert` rule fails
16
+ * (the validateField error shape, so error rendering works unchanged)
17
+ */
18
+ errors?: Array<import('./validate.js').FieldError>;
19
+ };
20
+ export type CompiledFieldRules = {
21
+ /**
22
+ * - The field's data pointer (template pointers keep `-`)
23
+ */
24
+ pointer: string;
25
+ /**
26
+ * - Decoded segments; ITEM marks an array-item slot
27
+ */
28
+ parts: Array<string | symbol>;
29
+ /**
30
+ * - Whether `parts` contains an ITEM slot
31
+ */
32
+ templated: boolean;
33
+ /**
34
+ * - Compiled getter (non-template fields)
35
+ */
36
+ getValue: ((root: any) => any) | null;
37
+ visible: Function | null;
38
+ enabled: Function | null;
39
+ assert: Function | null;
40
+ computed: Function | null;
41
+ message: string | null;
42
+ };
43
+ export type CompiledRules = {
44
+ rules: Array<CompiledFieldRules>;
45
+ };
46
+ /**
47
+ * @typedef {object} CompiledFieldRules
48
+ * @property {string} pointer - The field's data pointer (template pointers keep `-`)
49
+ * @property {Array<string|symbol>} parts - Decoded segments; ITEM marks an array-item slot
50
+ * @property {boolean} templated - Whether `parts` contains an ITEM slot
51
+ * @property {((root: any) => any)|null} getValue - Compiled getter (non-template fields)
52
+ * @property {function|null} visible
53
+ * @property {function|null} enabled
54
+ * @property {function|null} assert
55
+ * @property {function|null} computed
56
+ * @property {string|null} message
57
+ */
58
+ /**
59
+ * @typedef {object} CompiledRules
60
+ * @property {Array<CompiledFieldRules>} rules
61
+ */
62
+ /**
63
+ * Compile every `x-form` rule of a form model into reusable closures.
64
+ *
65
+ * Walks the field tree once and runs `compileJsonQuery` per rule
66
+ * document. `options.compileTypeTest` passes through to the query
67
+ * compiler, so rules may use `$valid`/`$assert`/`$as` with a
68
+ * caller-supplied type-test compiler (the validator package's `query`
69
+ * module exports `createTypeTestCompiler()`; forms itself never imports
70
+ * the validator).
71
+ * Without the hook, a schema-using rule surfaces the engine's JQ0008.
72
+ *
73
+ * @param {import('./model.js').FormField} model - Root field from buildFormModel
74
+ * @param {object} [options]
75
+ * @param {(schemaJson: any, docPath: string) => ((value: any) => boolean)}
76
+ * [options.compileTypeTest] - hook for schema literals inside rules
77
+ * @returns {CompiledRules}
78
+ * @throws {Error} On a malformed rule document (field pointer prepended)
79
+ * or a rule referencing an external other than `value`/`pointer`
80
+ * @example
81
+ * const compiled = compileFormRules(model);
82
+ * const results = evaluateFormRules(compiled, data);
83
+ * results['/vatId']; // { visible: true, errors: [{ keyword: 'x-form/assert', ... }] }
84
+ */
85
+ export declare function compileFormRules(model: import('./model.js').FormField, options?: {
86
+ compileTypeTest?: (schemaJson: any, docPath: string) => ((value: any) => boolean);
87
+ }): CompiledRules;
88
+ /**
89
+ * Evaluate compiled form rules against the current data root.
90
+ *
91
+ * Returns a map of data pointer -> RuleResult holding only the rules each
92
+ * field declares. Rules on array item templates are evaluated once per
93
+ * element of the actual array, keyed by the expanded pointer.
94
+ *
95
+ * @param {CompiledRules} compiled - From compileFormRules
96
+ * @param {any} data - The form data root (the query input `$`)
97
+ * @returns {Record<string, RuleResult>}
98
+ * @example
99
+ * const results = evaluateFormRules(compiled, { company: 'ACME', vatId: '' });
100
+ * results['/vatId'].errors; // [{ keyword: 'x-form/assert', message: '...' }]
101
+ */
102
+ export declare function evaluateFormRules(compiled: CompiledRules, data: any): Record<string, RuleResult>;
103
+ /**
104
+ * Copy every `x-form.assert` of a schema into a `$query` assertion, so a
105
+ * rule authored once for per-keystroke feedback is also enforced by the
106
+ * authoritative submit validation (the validator's `$query` keyword).
107
+ * Pure schema-to-schema transform - no validator import; the output only
108
+ * spells the keyword.
109
+ *
110
+ * The `$query` lands on the ROOT schema (where the query input `$` is the
111
+ * instance root, matching the rule context), with each assert wrapped to
112
+ * rebuild its bindings: `value` binds to the field's location, `pointer`
113
+ * to its pointer string. An assert on an array item template quantifies
114
+ * with `$every` over the actual elements (`pointer` then stays the
115
+ * template pointer - element indexes are a render-time notion). Multiple
116
+ * asserts conjoin under `$and`; an existing root `$query` is preserved by
117
+ * wrapping the new one in an `allOf` branch.
118
+ *
119
+ * The transform follows the same structural spine as buildFormModel
120
+ * (`properties`, `items`, `prefixItems`, `allOf`) but does not resolve
121
+ * `$ref`s - a `$def`'s data location depends on its use site.
122
+ *
123
+ * @param {object|boolean} schema - The root JSON schema
124
+ * @returns {object|boolean} A new root schema (input is not mutated;
125
+ * untouched subtrees are shared) with the collected `$query`, or the
126
+ * input itself when there is nothing to copy
127
+ * @example
128
+ * const submitSchema = formRulesToQueryAssertions(schema);
129
+ * const validate = new JarenValidator().compile(submitSchema); // caller-side
130
+ */
131
+ export declare function formRulesToQueryAssertions(schema: object | boolean): object | boolean;
@@ -0,0 +1,29 @@
1
+ export type FieldError = {
2
+ /**
3
+ * - The JSON Schema keyword that failed
4
+ */
5
+ keyword: string;
6
+ /**
7
+ * - Human readable message
8
+ */
9
+ message: string;
10
+ };
11
+ /**
12
+ * Validate a single field value against its own constraints.
13
+ *
14
+ * @param {import('./model.js').FormField} field - Field from buildFormModel
15
+ * @param {any} value - The TYPED value (see parseFieldInput); undefined = absent
16
+ * @returns {FieldError[]} Empty when the value passes every per-field check
17
+ * @example
18
+ * const errors = validateField(emailField, 'not-an-email');
19
+ * // [{ keyword: 'format', message: 'Must be a valid email' }]
20
+ */
21
+ export declare function validateField(field: import('./model.js').FormField, value: any): FieldError[];
22
+ /**
23
+ * Validate every leaf field of a model against the current data.
24
+ * Returns a map of data-pointer -> FieldError[] for fields that fail.
25
+ * @param {import('./model.js').FormField} model - Root field from buildFormModel
26
+ * @param {any} data - Current form data
27
+ * @returns {Record<string, FieldError[]>}
28
+ */
29
+ export declare function validateAllFields(model: import('./model.js').FormField, data: any): Record<string, FieldError[]>;
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@jarenjs/forms",
3
+ "private": false,
4
+ "version": "0.9.2",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./dist/types/index.d.ts",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types/index.d.ts",
12
+ "default": "./src/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "dist/types/",
18
+ "src/"
19
+ ],
20
+ "description": "Framework-agnostic form model generator for JSON Schema with preemptive per-field validation powered by @jarenjs/core",
21
+ "author": "joham",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/jklarenbeek/jarenjs.git",
25
+ "directory": "packages/forms"
26
+ },
27
+ "license": "MIT",
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "registry": "https://registry.npmjs.org/"
34
+ },
35
+ "keywords": [
36
+ "jaren",
37
+ "json",
38
+ "schema",
39
+ "form",
40
+ "forms",
41
+ "generator",
42
+ "validation"
43
+ ],
44
+ "scripts": {
45
+ "build": "npm run build:types",
46
+ "build:types": "tsc -p tsconfig.json",
47
+ "prepack": "npm run build:types"
48
+ },
49
+ "dependencies": {
50
+ "@jarenjs/core": "^0.9.2",
51
+ "@jarenjs/formats": "^0.9.2",
52
+ "@jarenjs/json": "^0.9.2"
53
+ }
54
+ }