@jarenjs/validate 0.9.2 → 0.34.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/ARCHITECTURE.md +81 -17
- package/README.md +462 -5
- package/dist/types/dollar-data.d.ts +0 -9
- package/dist/types/index.d.ts +167 -69
- package/dist/types/messages.d.ts +142 -0
- package/dist/types/normalize.d.ts +107 -0
- package/dist/types/tools.d.ts +58 -0
- package/docs/ERROR-MESSAGES.md +251 -0
- package/package.json +9 -4
- package/src/array.js +68 -23
- package/src/bigint.js +18 -7
- package/src/combine.js +61 -11
- package/src/condition.js +34 -14
- package/src/content.js +3 -3
- package/src/data.js +11 -387
- package/src/dollar-data.js +55 -472
- package/src/enum.js +0 -1
- package/src/format.js +46 -4
- package/src/index.js +280 -238
- package/src/messages.js +497 -0
- package/src/normalize.js +585 -0
- package/src/number.js +19 -9
- package/src/object.js +126 -33
- package/src/query.js +28 -2
- package/src/schema.js +72 -27
- package/src/string.js +18 -6
- package/src/tools.js +192 -0
- package/src/traverse.js +13 -4
- package/src/unevaluated.js +27 -5
package/dist/types/tools.d.ts
CHANGED
|
@@ -14,7 +14,48 @@ export declare function isOfSchemaType(schema: any, type: any): boolean;
|
|
|
14
14
|
export declare function hasSchemaRef(schema: any): boolean;
|
|
15
15
|
export declare function hasSchemaRecursiveRef(schema: any): boolean;
|
|
16
16
|
export declare function hasSchemaDynamicRef(schema: any): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Whether a sibling keyword of unevaluatedProperties already evaluates every
|
|
19
|
+
* property of the instance. additionalProperties (boolean or schema) applies
|
|
20
|
+
* to each property not matched by properties/patternProperties, so once it
|
|
21
|
+
* has passed no property is left unevaluated.
|
|
22
|
+
* @param {object} schema - The schema holding the unevaluatedProperties keyword
|
|
23
|
+
* @returns {boolean} True when the unevaluatedProperties check can never match
|
|
24
|
+
*/
|
|
25
|
+
export declare function hasUnevaluatedPropertiesCoverage(schema: object): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Whether a sibling keyword of unevaluatedItems already evaluates every item
|
|
28
|
+
* of the instance: a uniform items schema (boolean or object) covers all
|
|
29
|
+
* items beyond any prefixItems, and a tuple-form items with additionalItems
|
|
30
|
+
* covers the items beyond the tuple.
|
|
31
|
+
* @param {object} schema - The schema holding the unevaluatedItems keyword
|
|
32
|
+
* @returns {boolean} True when the unevaluatedItems check can never match
|
|
33
|
+
*/
|
|
34
|
+
export declare function hasUnevaluatedItemsCoverage(schema: object): boolean;
|
|
17
35
|
export declare function createIsSchemaTypeHandler(type: any, isStrict?: boolean): typeof isBooleanType | typeof isRegExpType | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* The fallback resolver of the data-reference keywords (`data`, `$data`):
|
|
38
|
+
* a ref that fails the strict compile keeps the lax keyword semantics, so
|
|
39
|
+
* it resolves as not-found and the keyword asserts nothing.
|
|
40
|
+
* @returns {any} the JSON Pointer not-found sentinel
|
|
41
|
+
*/
|
|
42
|
+
export declare const resolveNothing: () => any;
|
|
43
|
+
/**
|
|
44
|
+
* Build the keyword validators the two data-reference keywords share
|
|
45
|
+
* verbatim. The `data` keyword (json-everything, absolute + relative
|
|
46
|
+
* pointers via `compileDataRef`) and the Ajv-style `$data` keyword
|
|
47
|
+
* (relative pointers only) differ ONLY in which pointer compiler
|
|
48
|
+
* resolves a ref, so each module passes its own `compileRefResolver`
|
|
49
|
+
* and gets the same fifteen compilers back.
|
|
50
|
+
*
|
|
51
|
+
* Every validator follows one lax contract: a data instance outside the
|
|
52
|
+
* keyword's type, an unresolvable ref, or a resolved constraint of the
|
|
53
|
+
* wrong type asserts nothing.
|
|
54
|
+
*
|
|
55
|
+
* @param {(ref: string) => (dataRoot: any, dataPath: string) => any} compileRefResolver
|
|
56
|
+
* @returns {Record<string, (schemaObj: object, ref: string) => ((data: any, dataPath: string, dataRoot: any) => boolean) | undefined>}
|
|
57
|
+
*/
|
|
58
|
+
export declare function createDataRefCompilers(compileRefResolver: (ref: string) => (dataRoot: any, dataPath: string) => any): Record<string, (schemaObj: object, ref: string) => ((data: any, dataPath: string, dataRoot: any) => boolean) | undefined>;
|
|
18
59
|
/**
|
|
19
60
|
* Records which properties (string keys) and items (numeric indexes) of a
|
|
20
61
|
* data instance were successfully evaluated during validation, so that
|
|
@@ -40,6 +81,23 @@ export declare class EvalLog {
|
|
|
40
81
|
/** @returns {boolean} True when item `index` of `data` was evaluated at or after `from` (-1 entries cover all items) */
|
|
41
82
|
hasItem(data: any, index: any, from: any): boolean;
|
|
42
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Combine INDEPENDENT keyword validators without short-circuiting.
|
|
86
|
+
*
|
|
87
|
+
* `a(...) && b(...)` is the right composition in boolean mode: the answer is
|
|
88
|
+
* known at the first failure and nothing is gained by continuing. When errors
|
|
89
|
+
* are recorded it is wrong, because each validator is the only thing that can
|
|
90
|
+
* report its own fault, so the first failure hides every sibling's. This runs
|
|
91
|
+
* all of them and ANDs the results — the boolean answer is identical, the
|
|
92
|
+
* error list is complete.
|
|
93
|
+
*
|
|
94
|
+
* Only use it where the validators genuinely are independent. A precondition
|
|
95
|
+
* (a type guard before a length check) must keep its short-circuit: running
|
|
96
|
+
* past it is meaningless at best and throws at worst.
|
|
97
|
+
* @param {Function[]} validators - Independent validators, in report order
|
|
98
|
+
* @returns {Function} A validator that runs every one of them
|
|
99
|
+
*/
|
|
100
|
+
export declare function combineIndependent(validators: Function[]): Function;
|
|
43
101
|
export declare class ValidationResult {
|
|
44
102
|
match: boolean;
|
|
45
103
|
errors: number;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Jaren Error Messages & i18n
|
|
2
|
+
|
|
3
|
+
**Status: normative.** This document specifies the error record shape, the
|
|
4
|
+
message-key space, the `MessageSpec` value type, the `errorMessage`
|
|
5
|
+
keyword, catalogs, and the resolution precedence chain implemented by
|
|
6
|
+
`@jarenjs/validate` (src/messages.js) and mirrored by `@jarenjs/forms`
|
|
7
|
+
(src/messages.js). Locale packs live in `@jarenjs/locales`.
|
|
8
|
+
|
|
9
|
+
The design premise: **the validator and the forms engine are not the place
|
|
10
|
+
where prose is born.** Every failure is identified by a stable message key
|
|
11
|
+
(`msgid`) plus raw structured `params`; human text is produced only at
|
|
12
|
+
report/render time, over the already-failed set, by a locale catalog. The
|
|
13
|
+
validation hot path (boolean mode, the `skipErrors` no-op handlers, the
|
|
14
|
+
fast-path node compilers) is untouched: switching locale never recompiles
|
|
15
|
+
a validator or a form model's rules.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. The error record
|
|
20
|
+
|
|
21
|
+
Collect-mode validation (`collectErrors: true`) returns
|
|
22
|
+
`{ valid, errors: ValidationError[] }` where every error is:
|
|
23
|
+
|
|
24
|
+
| member | type | meaning |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `keyword` | string | the failed keyword (`type`, `required`, `$query`, ...) |
|
|
27
|
+
| `instancePath` | string | RFC 6901 JSON Pointer to the failing data location |
|
|
28
|
+
| `schemaPath` | string | the schema location (the compiled node's URI path) |
|
|
29
|
+
| `params` | object | keyword-specific raw parameters (section 2) |
|
|
30
|
+
| `msgid` | string | the resolved message key (section 2) |
|
|
31
|
+
| `message` | string | human text — `''` when `messages: false` |
|
|
32
|
+
|
|
33
|
+
`toJSON()` serializes exactly these six members.
|
|
34
|
+
|
|
35
|
+
`msgid` resolution: the matched `errorMessage` spec's `$msgid` if any,
|
|
36
|
+
else `params.code` if present (the `$query` runtime codes), else the
|
|
37
|
+
keyword.
|
|
38
|
+
|
|
39
|
+
With the validator option `messages: false`, conversion skips message
|
|
40
|
+
rendering entirely (`message: ''`; `params` and `msgid` still set) — the
|
|
41
|
+
fast path for applications that render exclusively through
|
|
42
|
+
`localizeErrors` or their own resolver.
|
|
43
|
+
|
|
44
|
+
### The handler-contract invariant (internal)
|
|
45
|
+
|
|
46
|
+
Every internal error handler call site obeys: normal handlers
|
|
47
|
+
`addError(data, dataPath, ...extra)`, keyed handlers
|
|
48
|
+
`addKeyedError(dataKey, data, dataPath, ...extra)` — the first meta
|
|
49
|
+
argument is ALWAYS the instance data path, so `instancePath` is a straight
|
|
50
|
+
read. A charCode guard keeps a non-pointer value from ever becoming a
|
|
51
|
+
wrong path (it yields `''`).
|
|
52
|
+
|
|
53
|
+
### Deliberate `additionalProperties` divergence from ajv
|
|
54
|
+
|
|
55
|
+
An `additionalProperties: false` failure reports `instancePath` at the
|
|
56
|
+
**offending member** (`/nested/extra`), not at the parent object
|
|
57
|
+
(`/nested`). The object compiler passes the child path
|
|
58
|
+
(`${dataPath}/${dataKey}`) as the error's data path, so the pointer lands
|
|
59
|
+
on the disallowed property itself. This is spec-correct (the failing
|
|
60
|
+
location *is* that property) and friendlier for a UI that highlights the
|
|
61
|
+
field. ajv reports the parent object's path with the property name in
|
|
62
|
+
`params.additionalProperty`; Jaren carries the same `additionalProperty`
|
|
63
|
+
param, so a consumer diffing error sets against ajv should treat the
|
|
64
|
+
`instancePath` difference as intentional, not a bug.
|
|
65
|
+
|
|
66
|
+
## 2. The message-key registry
|
|
67
|
+
|
|
68
|
+
One flat namespace:
|
|
69
|
+
|
|
70
|
+
| Producer | Keys | Params carried |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| validate keywords | the keyword itself: `type`, `required`, `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`, `minLength`, `maxLength`, `pattern`, `additionalProperties`, `minProperties`, `maxProperties`, `minItems`, `maxItems`, `uniqueItems`, `contains`, `items`, `allOf`, `anyOf`, `oneOf`, `not`, `format`, `if`, `then`, `else`, `false schema`, `$query` | `type`/`types`; `missingProperty`; `limit` + `comparison`; `multipleOf`; `pattern`; `additionalProperty`; `format` |
|
|
73
|
+
| query runtime via `$query` | the `JQ2xxx` code (`JQ2001`, `JQ2003`, ...) | `{ code, docPath }` |
|
|
74
|
+
| forms field checks | `form/` + the emitted keyword: `form/required`, `form/type`, `form/const`, `form/enum`, `form/minLength`, `form/maxLength`, `form/pattern`, `form/format`, `form/minimum`, `form/maximum`, `form/exclusiveMinimum`, `form/exclusiveMaximum`, `form/multipleOf`, `form/minItems`, `form/maxItems`, `form/uniqueItems`, `form/minProperties`, `form/maxProperties` | per branch: `limit`, `len`, `type`, `format`, `enumValues`, `constValue`, `pattern`, `multipleOf` |
|
|
75
|
+
| forms rules | `x-form/assert` (the default), else the author's `$msgid` | `{ pointer, ...spec.params }` |
|
|
76
|
+
| applications | any `$msgid` they invent — recommend dotted names (`checkout.total-too-low`) | error params + spec params |
|
|
77
|
+
|
|
78
|
+
Keywords without a built-in English entry (`const`, `enum`,
|
|
79
|
+
`dependentRequired`, `dependencies`, `unevaluatedProperties`, ...) render
|
|
80
|
+
the generic `validation failed for keyword '<keyword>'`; their `msgid` is
|
|
81
|
+
still the keyword, so a catalog (or an application) may cover them.
|
|
82
|
+
|
|
83
|
+
## 3. MessageSpec
|
|
84
|
+
|
|
85
|
+
The value type used identically by `errorMessage` (validate),
|
|
86
|
+
`x-form.message` (forms), and anywhere else a message is authored:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
MessageSpec = string // inline template (author's language)
|
|
90
|
+
| { "$msgid": string, // catalog key to resolve at render time
|
|
91
|
+
"message"?: string, // inline template fallback on catalog miss
|
|
92
|
+
"params"?: object } // merged OVER the error's params
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The object form requires `$msgid` and/or `message`; any other member is a
|
|
96
|
+
compile-time error. Spec `params` merge over the error's params **into the
|
|
97
|
+
error record**, so post-hoc localization sees them too.
|
|
98
|
+
|
|
99
|
+
### Template syntax
|
|
100
|
+
|
|
101
|
+
Applies to inline templates and to string-valued catalog entries:
|
|
102
|
+
|
|
103
|
+
- `{name}` substitutes the merged params member `name` — `String(v)` for
|
|
104
|
+
primitives, `JSON.stringify(v)` otherwise;
|
|
105
|
+
- an unknown name leaves the placeholder literally (debuggability);
|
|
106
|
+
- `{{` escapes a literal `{`.
|
|
107
|
+
|
|
108
|
+
Templates compile ONCE into a closure (`compileMessageTemplate`) — the
|
|
109
|
+
two-stage house rule applies to messages too. There is **no pointer/data
|
|
110
|
+
interpolation in v1** (`${/foo}` ajv-style is a roadmap follow-up; params
|
|
111
|
+
already carry the relevant values).
|
|
112
|
+
|
|
113
|
+
## 4. Catalogs
|
|
114
|
+
|
|
115
|
+
A catalog is a plain flat object:
|
|
116
|
+
`{ [key]: (params, error) => string | templateString }`.
|
|
117
|
+
`compileMessageCatalog(catalogLike)` returns a functions-only frozen copy
|
|
118
|
+
(template strings compiled).
|
|
119
|
+
|
|
120
|
+
English catalogs are **built in**: validate's `messagesEn`
|
|
121
|
+
(src/messages.js) and forms' `formsMessagesEn` (src/messages.js) — neither
|
|
122
|
+
package gains a dependency. Non-English packs live in `@jarenjs/locales`
|
|
123
|
+
(`nl`, `fr`, `es`, `pt`, `de`, `ja`, `ko`, `zhTW`, `ru`, `tr`, `ar` —
|
|
124
|
+
each also a subpath export, e.g. `@jarenjs/locales/fr`,
|
|
125
|
+
`@jarenjs/locales/zh-tw`) and must have key parity with the built-in
|
|
126
|
+
English (enforced by repo tests).
|
|
127
|
+
|
|
128
|
+
### Pack authoring (globalization mechanics)
|
|
129
|
+
|
|
130
|
+
Catalog entries are functions precisely so packs can use the platform:
|
|
131
|
+
|
|
132
|
+
- `Intl.PluralRules` for plural category selection ("1 teken" /
|
|
133
|
+
"2 tekens" — see the `nl` pack's `minLength`),
|
|
134
|
+
- `Intl.NumberFormat` for `{limit}`-style numbers,
|
|
135
|
+
- `Intl.ListFormat` for enum lists.
|
|
136
|
+
|
|
137
|
+
Hold these as module-level singletons (allocation discipline). **Bidi:**
|
|
138
|
+
packs targeting RTL scripts should isolate interpolated user values with
|
|
139
|
+
FSI/PDI (U+2068/U+2069) — the pack author's call, not core's. A pack
|
|
140
|
+
depends on nothing outside the Jaren suite, and inside it on nothing but
|
|
141
|
+
`@jarenjs/core` — never on `@jarenjs/validate` or `@jarenjs/forms`, so
|
|
142
|
+
either consumer can serve any pack.
|
|
143
|
+
|
|
144
|
+
## 5. Resolution precedence
|
|
145
|
+
|
|
146
|
+
Implemented in validate's conversion (`convertInternalErrors`); forms
|
|
147
|
+
mirrors the tail of the chain:
|
|
148
|
+
|
|
149
|
+
1. the nearest `errorMessage` spec for the error (section 6 matching);
|
|
150
|
+
2. if the spec has `$msgid`: active catalog → built-in English catalog →
|
|
151
|
+
the spec's inline `message` template;
|
|
152
|
+
3. if the spec is inline (string / `message`-only): render it;
|
|
153
|
+
4. no spec: active catalog[`msgid`] → built-in English[`msgid`] →
|
|
154
|
+
catalog[`keyword`] → English[`keyword`] (so uncovered `JQ*` codes still
|
|
155
|
+
say something) → `validation failed for keyword '<keyword>'`.
|
|
156
|
+
|
|
157
|
+
At conversion time the "active catalog" is the built-in English; other
|
|
158
|
+
locales enter through `localizeErrors` / `renderErrorMessage` (section 7)
|
|
159
|
+
or, in forms, through the `catalog` parameters.
|
|
160
|
+
|
|
161
|
+
## 6. The `errorMessage` keyword
|
|
162
|
+
|
|
163
|
+
**Overrides text, never structure.** Deliberate divergences from
|
|
164
|
+
ajv-errors:
|
|
165
|
+
|
|
166
|
+
- errors are never removed, merged, or aggregated;
|
|
167
|
+
- no synthetic `keyword: "errorMessage"` error is created;
|
|
168
|
+
- originals are never moved into `params.errors`;
|
|
169
|
+
- no `${/pointer}` data interpolation (v1);
|
|
170
|
+
- no `properties`/`items` map forms — the subtree prefix rule covers what
|
|
171
|
+
those express (roadmap follow-up if demand appears).
|
|
172
|
+
|
|
173
|
+
An `errorMessage` spec only changes what `message` (and `msgid`) say on
|
|
174
|
+
the errors it matches.
|
|
175
|
+
|
|
176
|
+
### Grammar
|
|
177
|
+
|
|
178
|
+
Validated at schema compile time; malformed specs throw with the schema
|
|
179
|
+
path.
|
|
180
|
+
|
|
181
|
+
```jsonc
|
|
182
|
+
"errorMessage": MessageSpec // string form: whole subtree
|
|
183
|
+
"errorMessage": { // map form
|
|
184
|
+
"minLength": MessageSpec, // per failing keyword, this node
|
|
185
|
+
"required": MessageSpec // all required failures here
|
|
186
|
+
| { "vatId": MessageSpec, ... }, // or per missing property
|
|
187
|
+
"$query": MessageSpec // EBV-false and any runtime code
|
|
188
|
+
| { "default": MessageSpec, // EBV-false
|
|
189
|
+
"JQ2001": MessageSpec, ... }, // per runtime code
|
|
190
|
+
"_": MessageSpec // node-level catch-all
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Matching
|
|
195
|
+
|
|
196
|
+
The compiler registers each spec by schema location on the compilation
|
|
197
|
+
root — **no validator closure is emitted**; the keyword contributes zero
|
|
198
|
+
validation-time work and never knocks a node off the fast paths. At
|
|
199
|
+
report time:
|
|
200
|
+
|
|
201
|
+
- candidate nodes are the registered locations that equal the error's
|
|
202
|
+
`schemaPath` or are a segment-aware prefix of it (`/foo` never matches
|
|
203
|
+
`/foobar`); the longest prefix is tried first;
|
|
204
|
+
- **at the error's own node**: keyword-map entry (with `required`
|
|
205
|
+
per-property matching on `params.missingProperty`, `$query` per-code on
|
|
206
|
+
`params.code`, `default` on its absence) > `_` > string form;
|
|
207
|
+
- **at an ancestor**: only the string form applies (it covers the
|
|
208
|
+
subtree — this is what lets one string on a `oneOf` replace the branch
|
|
209
|
+
noise);
|
|
210
|
+
- no match at the nearest node falls through to farther ancestors.
|
|
211
|
+
|
|
212
|
+
## 7. Post-hoc localization
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
import { JarenValidator, compileMessageCatalog, localizeErrors } from '@jarenjs/validate';
|
|
216
|
+
import { nl } from '@jarenjs/locales';
|
|
217
|
+
|
|
218
|
+
const catalog = compileMessageCatalog(nl);
|
|
219
|
+
const result = validate(data); // English messages
|
|
220
|
+
localizeErrors(result.errors, catalog); // Dutch messages, same array
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`localizeErrors(errors, catalog)` re-renders `message` on each error from
|
|
224
|
+
`msgid` + `params` through the given compiled catalog with built-in
|
|
225
|
+
English fallback. Contract details:
|
|
226
|
+
|
|
227
|
+
- **inline schema-authored text without `$msgid` is single-language by
|
|
228
|
+
definition and is NOT re-rendered** — that is why `$msgid` exists;
|
|
229
|
+
- an error whose `msgid` resolves in no catalog keeps its current message
|
|
230
|
+
(e.g. a custom `$msgid`'s inline fallback text);
|
|
231
|
+
- `renderErrorMessage(error, catalog?)` is the single-error form of the
|
|
232
|
+
same chain (section 5 step 4).
|
|
233
|
+
|
|
234
|
+
Forms renders eagerly (failure-only, cheap, keeps UI consumers simple)
|
|
235
|
+
but through the same catalog contract: `validateField` /
|
|
236
|
+
`validateAllFields` / `evaluateFormRules` accept an optional compiled
|
|
237
|
+
catalog, default English, and every `FieldError` carries
|
|
238
|
+
`{ keyword, params, msgid, message }` so consumers can re-render.
|
|
239
|
+
|
|
240
|
+
## 8. Why not MessageFormat 2
|
|
241
|
+
|
|
242
|
+
`Intl.MessageFormat` (MessageFormat 2) is TC39 Stage 2 and stalled,
|
|
243
|
+
shipping in no runtime; adopting it would mean a runtime dependency or a
|
|
244
|
+
homegrown MF2 engine for pluralization the platform already provides
|
|
245
|
+
through `Intl.PluralRules`. Catalogs-as-functions cover the same ground
|
|
246
|
+
with no runtime dependency beyond the platform, and with full generality.
|
|
247
|
+
|
|
248
|
+
**Revisit trigger:** Intl.MessageFormat reaching TC39 Stage 3 or shipping
|
|
249
|
+
in a major runtime. At that point, MF2 syntax could become a supported
|
|
250
|
+
catalog *entry format* (compiled by `compileMessageCatalog`) without
|
|
251
|
+
changing the catalog contract.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/validate",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.34.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -15,11 +15,16 @@
|
|
|
15
15
|
"types": "./dist/types/query.d.ts",
|
|
16
16
|
"default": "./src/query.js"
|
|
17
17
|
},
|
|
18
|
+
"./normalize": {
|
|
19
|
+
"types": "./dist/types/normalize.d.ts",
|
|
20
|
+
"default": "./src/normalize.js"
|
|
21
|
+
},
|
|
18
22
|
"./package.json": "./package.json"
|
|
19
23
|
},
|
|
20
24
|
"files": [
|
|
21
25
|
"dist/types/",
|
|
22
26
|
"src/",
|
|
27
|
+
"docs/",
|
|
23
28
|
"ARCHITECTURE.md"
|
|
24
29
|
],
|
|
25
30
|
"description": "Jaren is a JavaScript JSON Schema Validator",
|
|
@@ -31,7 +36,7 @@
|
|
|
31
36
|
},
|
|
32
37
|
"license": "MIT",
|
|
33
38
|
"engines": {
|
|
34
|
-
"node": ">=
|
|
39
|
+
"node": ">=24"
|
|
35
40
|
},
|
|
36
41
|
"publishConfig": {
|
|
37
42
|
"access": "public",
|
|
@@ -50,7 +55,7 @@
|
|
|
50
55
|
"prepack": "npm run build:types"
|
|
51
56
|
},
|
|
52
57
|
"dependencies": {
|
|
53
|
-
"@jarenjs/core": "^0.
|
|
54
|
-
"@jarenjs/json": "^0.
|
|
58
|
+
"@jarenjs/core": "^0.34.0",
|
|
59
|
+
"@jarenjs/json": "^0.34.0"
|
|
55
60
|
}
|
|
56
61
|
}
|
package/src/array.js
CHANGED
|
@@ -225,22 +225,29 @@ function compileItemValidator(schemaObj, itemSchema, key, index) {
|
|
|
225
225
|
if (targetObj && targetObj.validate) {
|
|
226
226
|
return targetObj.validate;
|
|
227
227
|
}
|
|
228
|
-
} catch (
|
|
228
|
+
} catch (_e) {
|
|
229
229
|
// Fall through to default handling
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
// The two fast paths below return bare predicates with no error handler,
|
|
236
|
+
// so a failing item reports nothing of its own and the caller can only
|
|
237
|
+
// aggregate one error at the array path. When errors are recorded, fall
|
|
238
|
+
// through to full compilation so each failing item yields its own error
|
|
239
|
+
// at its own instancePath, matching the multi-keyword path.
|
|
240
|
+
const stopAtFirst = schemaObj.root.options.skipErrors;
|
|
241
|
+
|
|
235
242
|
// Fast path: type-only schema (most common case) - check property directly first
|
|
236
|
-
if (itemSchema.type !== undefined && Object.keys(itemSchema).length === 1) {
|
|
243
|
+
if (stopAtFirst && itemSchema.type !== undefined && Object.keys(itemSchema).length === 1) {
|
|
237
244
|
return compileTypeOnlyValidator(itemSchema.type);
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
// Fast path: required-only schema - check property directly first
|
|
241
|
-
if (itemSchema.required !== undefined && Object.keys(itemSchema).length === 1) {
|
|
248
|
+
if (stopAtFirst && itemSchema.required !== undefined && Object.keys(itemSchema).length === 1) {
|
|
242
249
|
const required = itemSchema.required;
|
|
243
|
-
return function validateRequiredOnly(data,
|
|
250
|
+
return function validateRequiredOnly(data, _dataPath, _dataRoot) {
|
|
244
251
|
// Required properties only apply to objects, not arrays or other types
|
|
245
252
|
if (typeof data !== 'object' || data === null || Array.isArray(data)) return true;
|
|
246
253
|
for (let i = 0; i < required.length; i++) {
|
|
@@ -337,11 +344,22 @@ export function compileArrayPrimitives(schemaObj, jsonSchema) {
|
|
|
337
344
|
const isMinItems = minItems || trueThat;
|
|
338
345
|
const isMaxItems = maxItems || trueThat;
|
|
339
346
|
|
|
340
|
-
|
|
347
|
+
if (schemaObj.options.skipErrors) {
|
|
348
|
+
return function validateArrayPrimitives(data, dataPath) {
|
|
349
|
+
const len = data.length;
|
|
350
|
+
return isMinItems(len, dataPath)
|
|
351
|
+
&& isMaxItems(len, dataPath)
|
|
352
|
+
&& uniqueItems(data, dataPath);
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Length and uniqueness are independent: a short array can also contain
|
|
357
|
+
// duplicates, and a caller fixing one wants to hear about the other.
|
|
358
|
+
return function validateArrayPrimitivesAll(data, dataPath) {
|
|
341
359
|
const len = data.length;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
360
|
+
let valid = isMinItems(len, dataPath);
|
|
361
|
+
valid = isMaxItems(len, dataPath) && valid;
|
|
362
|
+
return uniqueItems(data, dataPath) && valid;
|
|
345
363
|
};
|
|
346
364
|
}
|
|
347
365
|
|
|
@@ -392,7 +410,7 @@ function compileArrayChildren(schemaObj, jsonSchema) {
|
|
|
392
410
|
|
|
393
411
|
// Wrap single-item validator with index loop
|
|
394
412
|
const itemValidator = validateItem;
|
|
395
|
-
validateItem = function validateSingleItemSchema(data, dataPath, dataRoot,
|
|
413
|
+
validateItem = function validateSingleItemSchema(data, dataPath, dataRoot, _i) {
|
|
396
414
|
return itemValidator(data, dataPath, dataRoot);
|
|
397
415
|
};
|
|
398
416
|
} else if (items === true) {
|
|
@@ -421,7 +439,7 @@ function compileArrayChildren(schemaObj, jsonSchema) {
|
|
|
421
439
|
// items: true evaluates every item, which matters when annotations
|
|
422
440
|
// are tracked for unevaluatedItems.
|
|
423
441
|
if (track) {
|
|
424
|
-
return function validateArrayItemsTrue(data,
|
|
442
|
+
return function validateArrayItemsTrue(data, _dataPath, _dataRoot) {
|
|
425
443
|
root.evalLog.add(data, -1);
|
|
426
444
|
return true;
|
|
427
445
|
};
|
|
@@ -459,7 +477,9 @@ function compileArrayChildren(schemaObj, jsonSchema) {
|
|
|
459
477
|
return function validateArrayContainsOnly(data, dataPath) {
|
|
460
478
|
const len = resolveLength(data.length);
|
|
461
479
|
const arr = data;
|
|
462
|
-
|
|
480
|
+
// Each element is a CANDIDATE probe: the array only has to contain a
|
|
481
|
+
// match, so an element that is not one has done nothing wrong.
|
|
482
|
+
const errors = root.errorMark();
|
|
463
483
|
let contains = 0;
|
|
464
484
|
for (let i = 0; i < len; ++i) {
|
|
465
485
|
if (validator(arr[i], dataPath) === true) {
|
|
@@ -467,6 +487,7 @@ function compileArrayChildren(schemaObj, jsonSchema) {
|
|
|
467
487
|
if (trackContains) root.evalLog.add(data, i);
|
|
468
488
|
}
|
|
469
489
|
}
|
|
490
|
+
root.rollbackErrors(errors);
|
|
470
491
|
return validateMinMax(contains, dataPath);
|
|
471
492
|
};
|
|
472
493
|
}
|
|
@@ -475,6 +496,7 @@ function compileArrayChildren(schemaObj, jsonSchema) {
|
|
|
475
496
|
const itemValidator = validateItem;
|
|
476
497
|
const containsValidator = validateContains;
|
|
477
498
|
|
|
499
|
+
const stopAtFirstContains = schemaObj.options.skipErrors;
|
|
478
500
|
return function validateArrayChildren(data, dataPath, dataRoot) {
|
|
479
501
|
const len = resolveLength(data.length);
|
|
480
502
|
const arr = data;
|
|
@@ -491,13 +513,18 @@ function compileArrayChildren(schemaObj, jsonSchema) {
|
|
|
491
513
|
else if (track && i < evalLimit) {
|
|
492
514
|
root.evalLog.add(data, i);
|
|
493
515
|
}
|
|
516
|
+
// A contains candidate is a probe: not matching is not a fault.
|
|
517
|
+
const containsMark = root.errorMark();
|
|
494
518
|
if (containsValidator(obj, dataPath, dataRoot) === true) {
|
|
495
519
|
contains++;
|
|
496
520
|
if (trackContains) root.evalLog.add(data, i);
|
|
497
521
|
}
|
|
522
|
+
root.rollbackErrors(containsMark);
|
|
498
523
|
}
|
|
499
|
-
|
|
500
|
-
|
|
524
|
+
// Failing items and the contains count are independent tallies.
|
|
525
|
+
const itemsOk = invalid === 0;
|
|
526
|
+
if (stopAtFirstContains && !itemsOk) return false;
|
|
527
|
+
return validateMinMax(contains, dataPath) && itemsOk;
|
|
501
528
|
};
|
|
502
529
|
}
|
|
503
530
|
|
|
@@ -538,12 +565,21 @@ export function compileArraySchema(schemaObj, jsonSchema) {
|
|
|
538
565
|
if (parts.length === 2) {
|
|
539
566
|
const first = parts[0];
|
|
540
567
|
const second = parts[1];
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
568
|
+
if (schemaObj.options.skipErrors) {
|
|
569
|
+
return function validateArraySchemaDouble(data, dataPath, dataRoot) {
|
|
570
|
+
if (isArrayClass(data)) {
|
|
571
|
+
return first(data, dataPath, dataRoot)
|
|
572
|
+
&& second(data, dataPath, dataRoot);
|
|
573
|
+
}
|
|
574
|
+
return true;
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
// The two parts are independent array keyword groups (length/uniqueness
|
|
578
|
+
// versus the item walk); the array-class guard stays a precondition.
|
|
579
|
+
return function validateArraySchemaDoubleAll(data, dataPath, dataRoot) {
|
|
580
|
+
if (!isArrayClass(data)) return true;
|
|
581
|
+
const firstOk = first(data, dataPath, dataRoot);
|
|
582
|
+
return second(data, dataPath, dataRoot) && firstOk;
|
|
547
583
|
};
|
|
548
584
|
}
|
|
549
585
|
|
|
@@ -551,13 +587,22 @@ export function compileArraySchema(schemaObj, jsonSchema) {
|
|
|
551
587
|
const hasBooleanItems = compiledItemsBoolean || trueThat;
|
|
552
588
|
const hasBooleanContains = compiledContainsBoolean || trueThat;
|
|
553
589
|
const validateItems = compiledArrayChildren || trueThat;
|
|
590
|
+
const stopAtFirstSchema = schemaObj.options.skipErrors;
|
|
554
591
|
|
|
555
592
|
return function validateArraySchema(data, dataPath, dataRoot) {
|
|
556
593
|
if (isArrayClass(data)) {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
594
|
+
if (stopAtFirstSchema) {
|
|
595
|
+
return validatePrimitives(data, dataPath)
|
|
596
|
+
&& hasBooleanItems(data, dataPath, dataRoot)
|
|
597
|
+
&& hasBooleanContains(data, dataPath, dataRoot)
|
|
598
|
+
&& validateItems(data, dataPath, dataRoot);
|
|
599
|
+
}
|
|
600
|
+
// Length/uniqueness, the boolean items/contains forms and the item
|
|
601
|
+
// walk are independent; a length failure must not hide item faults.
|
|
602
|
+
let valid = validatePrimitives(data, dataPath);
|
|
603
|
+
valid = hasBooleanItems(data, dataPath, dataRoot) && valid;
|
|
604
|
+
valid = hasBooleanContains(data, dataPath, dataRoot) && valid;
|
|
605
|
+
return validateItems(data, dataPath, dataRoot) && valid;
|
|
561
606
|
}
|
|
562
607
|
return true;
|
|
563
608
|
};
|
package/src/bigint.js
CHANGED
|
@@ -86,12 +86,23 @@ export function compileBigIntBasic(schemaObj, jsonSchema) {
|
|
|
86
86
|
const isMin = minimum || trueThat;
|
|
87
87
|
const isMul = multipleOf || trueThat;
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
if (schemaObj.options.skipErrors) {
|
|
90
|
+
return function validateBigIntSchema(data, dataPath) {
|
|
91
|
+
if (isBigIntType(data)) {
|
|
92
|
+
return isMax(data, dataPath)
|
|
93
|
+
&& isMin(data, dataPath)
|
|
94
|
+
&& isMul(data, dataPath);
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The type guard stays a precondition; the three assertions inside it are
|
|
101
|
+
// independent and each must get to report its own fault.
|
|
102
|
+
return function validateBigIntSchemaAll(data, dataPath) {
|
|
103
|
+
if (!isBigIntType(data)) return true;
|
|
104
|
+
let valid = isMax(data, dataPath);
|
|
105
|
+
valid = isMin(data, dataPath) && valid;
|
|
106
|
+
return isMul(data, dataPath) && valid;
|
|
96
107
|
};
|
|
97
108
|
}
|