@jarenjs/validate 0.8.4 → 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.
Files changed (53) hide show
  1. package/ARCHITECTURE.md +1131 -0
  2. package/LICENSE +21 -0
  3. package/README.md +796 -2
  4. package/dist/types/array.d.ts +2 -0
  5. package/dist/types/bigint.d.ts +1 -0
  6. package/dist/types/combine.d.ts +1 -0
  7. package/dist/types/condition.d.ts +1 -0
  8. package/dist/types/content.d.ts +3 -0
  9. package/dist/types/data.d.ts +7 -0
  10. package/dist/types/dollar-data.d.ts +11 -0
  11. package/dist/types/dynamic-ref.d.ts +44 -0
  12. package/dist/types/enum.d.ts +1 -0
  13. package/dist/types/format.d.ts +21 -0
  14. package/dist/types/index.d.ts +972 -0
  15. package/dist/types/messages.d.ts +142 -0
  16. package/dist/types/normalize.d.ts +107 -0
  17. package/dist/types/number.d.ts +1 -0
  18. package/dist/types/object.d.ts +3 -0
  19. package/dist/types/query-keyword.d.ts +19 -0
  20. package/dist/types/query.d.ts +29 -0
  21. package/dist/types/schema.d.ts +1 -0
  22. package/dist/types/string.d.ts +1 -0
  23. package/dist/types/tools.d.ts +109 -0
  24. package/dist/types/traverse.d.ts +32 -0
  25. package/dist/types/unevaluated.d.ts +12 -0
  26. package/docs/ERROR-MESSAGES.md +251 -0
  27. package/package.json +37 -7
  28. package/src/array.js +610 -0
  29. package/src/bigint.js +108 -0
  30. package/src/combine.js +276 -0
  31. package/src/condition.js +129 -0
  32. package/src/content.js +83 -0
  33. package/src/data.js +101 -0
  34. package/src/dollar-data.js +212 -0
  35. package/src/dynamic-ref.js +121 -0
  36. package/src/enum.js +147 -0
  37. package/src/format.js +108 -0
  38. package/src/index.js +1896 -0
  39. package/src/messages.js +497 -0
  40. package/src/normalize.js +585 -0
  41. package/src/number.js +169 -0
  42. package/src/object.js +848 -0
  43. package/src/query-keyword.js +99 -0
  44. package/src/query.js +85 -0
  45. package/src/schema.js +690 -0
  46. package/src/string.js +164 -0
  47. package/src/tools.js +397 -0
  48. package/src/traverse.js +442 -0
  49. package/src/unevaluated.js +173 -0
  50. package/dist/index.js +0 -1998
  51. package/dist/index.js.map +0 -7
  52. package/dist/index.min.js +0 -2
  53. package/dist/index.min.js.map +0 -7
@@ -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,20 +1,47 @@
1
1
  {
2
2
  "name": "@jarenjs/validate",
3
3
  "private": false,
4
- "version": "0.8.4",
4
+ "version": "0.34.0",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
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
+ "./query": {
15
+ "types": "./dist/types/query.d.ts",
16
+ "default": "./src/query.js"
17
+ },
18
+ "./normalize": {
19
+ "types": "./dist/types/normalize.d.ts",
20
+ "default": "./src/normalize.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
7
24
  "files": [
8
- "dist"
25
+ "dist/types/",
26
+ "src/",
27
+ "docs/",
28
+ "ARCHITECTURE.md"
9
29
  ],
10
30
  "description": "Jaren is a JavaScript JSON Schema Validator",
11
31
  "author": "joham",
12
32
  "repository": {
13
33
  "type": "git",
14
- "url": "https://github.com/jklarenbeek/jarenjs.git",
34
+ "url": "git+https://github.com/jklarenbeek/jarenjs.git",
15
35
  "directory": "packages/validate"
16
36
  },
17
37
  "license": "MIT",
38
+ "engines": {
39
+ "node": ">=24"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "registry": "https://registry.npmjs.org/"
44
+ },
18
45
  "keywords": [
19
46
  "jaren",
20
47
  "json",
@@ -23,9 +50,12 @@
23
50
  "validator"
24
51
  ],
25
52
  "scripts": {
26
- "build": "node esbuild.config.js"
53
+ "build": "node esbuild.config.js && npm run build:types",
54
+ "build:types": "tsc -p tsconfig.json",
55
+ "prepack": "npm run build:types"
27
56
  },
28
- "devDependencies": {
29
- "esbuild": "^0.23.1"
57
+ "dependencies": {
58
+ "@jarenjs/core": "^0.34.0",
59
+ "@jarenjs/json": "^0.34.0"
30
60
  }
31
61
  }