@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.
- package/ARCHITECTURE.md +1131 -0
- package/LICENSE +21 -0
- package/README.md +796 -2
- package/dist/types/array.d.ts +2 -0
- package/dist/types/bigint.d.ts +1 -0
- package/dist/types/combine.d.ts +1 -0
- package/dist/types/condition.d.ts +1 -0
- package/dist/types/content.d.ts +3 -0
- package/dist/types/data.d.ts +7 -0
- package/dist/types/dollar-data.d.ts +11 -0
- package/dist/types/dynamic-ref.d.ts +44 -0
- package/dist/types/enum.d.ts +1 -0
- package/dist/types/format.d.ts +21 -0
- package/dist/types/index.d.ts +972 -0
- package/dist/types/messages.d.ts +142 -0
- package/dist/types/normalize.d.ts +107 -0
- package/dist/types/number.d.ts +1 -0
- package/dist/types/object.d.ts +3 -0
- package/dist/types/query-keyword.d.ts +19 -0
- package/dist/types/query.d.ts +29 -0
- package/dist/types/schema.d.ts +1 -0
- package/dist/types/string.d.ts +1 -0
- package/dist/types/tools.d.ts +109 -0
- package/dist/types/traverse.d.ts +32 -0
- package/dist/types/unevaluated.d.ts +12 -0
- package/docs/ERROR-MESSAGES.md +251 -0
- package/package.json +37 -7
- package/src/array.js +610 -0
- package/src/bigint.js +108 -0
- package/src/combine.js +276 -0
- package/src/condition.js +129 -0
- package/src/content.js +83 -0
- package/src/data.js +101 -0
- package/src/dollar-data.js +212 -0
- package/src/dynamic-ref.js +121 -0
- package/src/enum.js +147 -0
- package/src/format.js +108 -0
- package/src/index.js +1896 -0
- package/src/messages.js +497 -0
- package/src/normalize.js +585 -0
- package/src/number.js +169 -0
- package/src/object.js +848 -0
- package/src/query-keyword.js +99 -0
- package/src/query.js +85 -0
- package/src/schema.js +690 -0
- package/src/string.js +164 -0
- package/src/tools.js +397 -0
- package/src/traverse.js +442 -0
- package/src/unevaluated.js +173 -0
- package/dist/index.js +0 -1998
- package/dist/index.js.map +0 -7
- package/dist/index.min.js +0 -2
- package/dist/index.min.js.map +0 -7
package/src/messages.js
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Structured error messages & i18n for the validator.
|
|
5
|
+
*
|
|
6
|
+
* The validation hot path never touches this module: message rendering
|
|
7
|
+
* happens only in collect mode, only over the already-failed set, at
|
|
8
|
+
* conversion time (`convertInternalErrors`) or afterwards
|
|
9
|
+
* (`localizeErrors`). Every failure is identified by a stable message key
|
|
10
|
+
* (`msgid`) plus raw structured `params`; human text is produced by a
|
|
11
|
+
* locale catalog - a plain flat object of closures (or template strings
|
|
12
|
+
* that compile into closures). The built-in English catalog lives here;
|
|
13
|
+
* non-English packs live in `@jarenjs/locales`.
|
|
14
|
+
*
|
|
15
|
+
* The catalog contract itself - template syntax, compilation, parameter
|
|
16
|
+
* rendering - is kernel property (`@jarenjs/core/message`), shared with
|
|
17
|
+
* `@jarenjs/forms` without either package depending on the other. Both
|
|
18
|
+
* compilers are re-exported here so the validator's public surface stays
|
|
19
|
+
* self-contained.
|
|
20
|
+
*
|
|
21
|
+
* The normative spec is `packages/validate/docs/ERROR-MESSAGES.md`:
|
|
22
|
+
* the MessageSpec grammar, the message-key registry, the resolution
|
|
23
|
+
* precedence chain and the `errorMessage` matching semantics.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
compileMessageTemplate,
|
|
28
|
+
compileMessageCatalog,
|
|
29
|
+
} from '@jarenjs/core/message';
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
compileMessageTemplate,
|
|
33
|
+
compileMessageCatalog,
|
|
34
|
+
} from '@jarenjs/core/message';
|
|
35
|
+
|
|
36
|
+
//#region English catalog
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The built-in English catalog: one entry per message key the validator
|
|
40
|
+
* produces, rendering the exact strings of the historical `#convertErrors`
|
|
41
|
+
* if/else chain. Keywords without an entry (`const`, `enum`,
|
|
42
|
+
* `dependentRequired`, ...) fall back to the generic
|
|
43
|
+
* `validation failed for keyword '<keyword>'` - as before.
|
|
44
|
+
* @type {Record<string, string | ((params: object, error?: object) => string)>}
|
|
45
|
+
*/
|
|
46
|
+
export const messagesEn = {
|
|
47
|
+
type: (p) => p.types
|
|
48
|
+
? `must be one of the following types: ${p.types.join(', ')}`
|
|
49
|
+
: `must be ${p.type === 'integer' ? 'an' : 'a'} ${p.type}`,
|
|
50
|
+
required: (p) => p.missingProperty
|
|
51
|
+
? `must have required property '${p.missingProperty}'`
|
|
52
|
+
: 'must have required properties',
|
|
53
|
+
minimum: 'must be {comparison} {limit}',
|
|
54
|
+
maximum: 'must be {comparison} {limit}',
|
|
55
|
+
exclusiveMinimum: 'must be {comparison} {limit}',
|
|
56
|
+
exclusiveMaximum: 'must be {comparison} {limit}',
|
|
57
|
+
multipleOf: 'must be multiple of {multipleOf}',
|
|
58
|
+
minLength: 'must NOT have fewer than {limit} characters',
|
|
59
|
+
maxLength: 'must NOT have more than {limit} characters',
|
|
60
|
+
pattern: 'must match pattern "{pattern}"',
|
|
61
|
+
additionalProperties: (p) => p.additionalProperty
|
|
62
|
+
? `must NOT have additional property '${p.additionalProperty}'`
|
|
63
|
+
: 'must NOT have additional properties',
|
|
64
|
+
minProperties: 'must NOT have fewer than {limit} properties',
|
|
65
|
+
maxProperties: 'must NOT have more than {limit} properties',
|
|
66
|
+
minItems: 'must NOT have fewer than {limit} items',
|
|
67
|
+
maxItems: 'must NOT have more than {limit} items',
|
|
68
|
+
uniqueItems: 'must NOT have duplicate items',
|
|
69
|
+
contains: 'must contain at least one valid item',
|
|
70
|
+
items: 'array items are invalid',
|
|
71
|
+
allOf: 'must match all of the subschemas',
|
|
72
|
+
anyOf: 'must match a subschema in anyOf',
|
|
73
|
+
oneOf: 'must match exactly one subschema in oneOf',
|
|
74
|
+
not: 'must NOT match the subschema',
|
|
75
|
+
format: 'must match format "{format}"',
|
|
76
|
+
if: 'must match "if" schema',
|
|
77
|
+
then: 'must match "then" schema',
|
|
78
|
+
else: 'must match "else" schema',
|
|
79
|
+
'false schema': 'boolean schema false is always invalid',
|
|
80
|
+
$query: (p) => p.code
|
|
81
|
+
? `'$query' assertion raised ${p.code} at '${p.docPath}'`
|
|
82
|
+
: "must satisfy the '$query' assertion",
|
|
83
|
+
// Query runtime codes reachable through '$query' (JQ2001-class operator
|
|
84
|
+
// errors and the JQ2003 multi-item EBV); an uncovered JQ2xxx code falls
|
|
85
|
+
// back to the '$query' entry above, which renders the same string.
|
|
86
|
+
JQ2001: (p) => `'$query' assertion raised ${p.code} at '${p.docPath}'`,
|
|
87
|
+
JQ2003: (p) => `'$query' assertion raised ${p.code} at '${p.docPath}'`,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** The compiled built-in English catalog (module-level singleton). */
|
|
91
|
+
const EN = compileMessageCatalog(messagesEn);
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
|
|
95
|
+
//#region Public error record & rendering
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* JSON Schema Validation Error
|
|
99
|
+
* Represents a validation error according to the JSON Schema specification.
|
|
100
|
+
* @see https://json-schema.org/draft/2020-12/json-schema-core.html#output
|
|
101
|
+
*/
|
|
102
|
+
export class ValidationError {
|
|
103
|
+
/**
|
|
104
|
+
* @param {object} options - Error options
|
|
105
|
+
* @param {string} options.keyword - The keyword that failed validation
|
|
106
|
+
* @param {string} options.instancePath - JSON Pointer to the data location
|
|
107
|
+
* @param {string} options.schemaPath - JSON Pointer to the schema location
|
|
108
|
+
* @param {object} options.params - Keyword-specific parameters
|
|
109
|
+
* @param {string} [options.msgid] - Stable message key resolving this error in a catalog
|
|
110
|
+
* @param {string} [options.message] - Human-readable error message
|
|
111
|
+
*/
|
|
112
|
+
constructor(options) {
|
|
113
|
+
this.keyword = options.keyword;
|
|
114
|
+
this.instancePath = options.instancePath || '';
|
|
115
|
+
this.schemaPath = options.schemaPath || '';
|
|
116
|
+
this.params = options.params || {};
|
|
117
|
+
this.msgid = options.msgid || options.keyword;
|
|
118
|
+
this.message = options.message || '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Convert error to a plain object
|
|
123
|
+
* @returns {object} Plain object representation
|
|
124
|
+
*/
|
|
125
|
+
toJSON() {
|
|
126
|
+
return {
|
|
127
|
+
keyword: this.keyword,
|
|
128
|
+
instancePath: this.instancePath,
|
|
129
|
+
schemaPath: this.schemaPath,
|
|
130
|
+
params: this.params,
|
|
131
|
+
msgid: this.msgid,
|
|
132
|
+
message: this.message,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Mark an error's message as inline schema-authored text (a MessageSpec
|
|
139
|
+
* without `$msgid`): single-language by definition, never re-rendered by
|
|
140
|
+
* `localizeErrors`. Non-enumerable so it stays out of serialization.
|
|
141
|
+
* @param {ValidationError} error - The error to mark
|
|
142
|
+
*/
|
|
143
|
+
function markInlineMessage(error) {
|
|
144
|
+
Object.defineProperty(error, 'inlineMessage', {
|
|
145
|
+
value: true,
|
|
146
|
+
enumerable: false,
|
|
147
|
+
configurable: true,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Render the message of an error through a catalog - the tail of the
|
|
153
|
+
* resolution precedence chain (no `errorMessage` registry involvement):
|
|
154
|
+
* catalog[msgid], built-in English[msgid], catalog[keyword],
|
|
155
|
+
* built-in English[keyword], then the generic fallback text.
|
|
156
|
+
* @param {ValidationError | {keyword: string, msgid?: string, params?: object}} error - The error to render
|
|
157
|
+
* @param {Readonly<Record<string, (params: object, error?: object) => string>>} [catalog] - A compiled catalog (see {@link compileMessageCatalog})
|
|
158
|
+
* @returns {string} The rendered message
|
|
159
|
+
*/
|
|
160
|
+
export function renderErrorMessage(error, catalog = undefined) {
|
|
161
|
+
const msgid = error.msgid || error.keyword;
|
|
162
|
+
const params = error.params || {};
|
|
163
|
+
let render = catalog !== undefined ? catalog[msgid] : undefined;
|
|
164
|
+
if (render === undefined) render = EN[msgid];
|
|
165
|
+
if (render === undefined && catalog !== undefined) render = catalog[error.keyword];
|
|
166
|
+
if (render === undefined) render = EN[error.keyword];
|
|
167
|
+
if (render === undefined) return `validation failed for keyword '${error.keyword}'`;
|
|
168
|
+
return render(params, error);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Re-render the `message` of every error from its `msgid` + `params`
|
|
173
|
+
* through the given catalog, with built-in English fallback. This is the
|
|
174
|
+
* whole post-hoc i18n story:
|
|
175
|
+
* `localizeErrors(validate(data).errors, compileMessageCatalog(nl))`.
|
|
176
|
+
*
|
|
177
|
+
* Inline schema-authored messages (a MessageSpec without `$msgid`) are
|
|
178
|
+
* single-language by definition and are NOT re-rendered - that is why
|
|
179
|
+
* `$msgid` exists. An error whose `msgid` resolves in no catalog keeps
|
|
180
|
+
* its current message (e.g. the spec's inline fallback text).
|
|
181
|
+
* @param {ValidationError[]} errors - Errors from a collect-mode validation
|
|
182
|
+
* @param {Readonly<Record<string, (params: object, error?: object) => string>>} catalog - A compiled catalog (see {@link compileMessageCatalog})
|
|
183
|
+
* @returns {ValidationError[]} The same array, messages re-rendered
|
|
184
|
+
*/
|
|
185
|
+
export function localizeErrors(errors, catalog) {
|
|
186
|
+
for (let i = 0; i < errors.length; ++i) {
|
|
187
|
+
const error = errors[i];
|
|
188
|
+
// @ts-ignore - marker property, non-enumerable
|
|
189
|
+
if (error.inlineMessage === true) continue;
|
|
190
|
+
const msgid = error.msgid || error.keyword;
|
|
191
|
+
const params = error.params || {};
|
|
192
|
+
let render = catalog !== undefined ? catalog[msgid] : undefined;
|
|
193
|
+
if (render === undefined) render = EN[msgid];
|
|
194
|
+
if (render === undefined && catalog !== undefined) render = catalog[error.keyword];
|
|
195
|
+
if (render === undefined) render = EN[error.keyword];
|
|
196
|
+
if (render !== undefined) {
|
|
197
|
+
error.message = render(params, error);
|
|
198
|
+
}
|
|
199
|
+
else if (error.message === '') {
|
|
200
|
+
error.message = `validation failed for keyword '${error.keyword}'`;
|
|
201
|
+
}
|
|
202
|
+
// else: keep the existing message (an unresolvable custom msgid whose
|
|
203
|
+
// text came from the spec's inline fallback).
|
|
204
|
+
}
|
|
205
|
+
return errors;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
//#endregion
|
|
209
|
+
|
|
210
|
+
//#region 'errorMessage' spec compilation (schema compile time)
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A compiled MessageSpec leaf.
|
|
214
|
+
* @typedef {object} CompiledMessageSpec
|
|
215
|
+
* @property {string|null} msgid - Catalog key to resolve at render time
|
|
216
|
+
* @property {((params: object, error?: object) => string)|null} render - Compiled inline template
|
|
217
|
+
* @property {object|null} params - Author params, merged OVER the error's params
|
|
218
|
+
*/
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Is this value a MessageSpec (string form, or object form carrying
|
|
222
|
+
* `$msgid`/`message`) rather than a keyword map?
|
|
223
|
+
* @param {unknown} value - The value to test
|
|
224
|
+
* @returns {boolean}
|
|
225
|
+
*/
|
|
226
|
+
function isMessageSpecValue(value) {
|
|
227
|
+
if (typeof value === 'string') return true;
|
|
228
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
229
|
+
return typeof (/** @type {any} */ (value).$msgid) === 'string'
|
|
230
|
+
|| typeof (/** @type {any} */ (value).message) === 'string';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Compile one MessageSpec value; throws on a malformed spec, carrying the
|
|
235
|
+
* schema path (the query-keyword compile-time throw idiom).
|
|
236
|
+
* @param {unknown} spec - The MessageSpec value
|
|
237
|
+
* @param {string} path - The schema path, for error messages
|
|
238
|
+
* @returns {CompiledMessageSpec} The compiled spec
|
|
239
|
+
*/
|
|
240
|
+
function compileMessageSpec(spec, path) {
|
|
241
|
+
if (typeof spec === 'string')
|
|
242
|
+
return { msgid: null, render: compileMessageTemplate(spec), params: null };
|
|
243
|
+
|
|
244
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec))
|
|
245
|
+
throw new Error(`invalid 'errorMessage' spec at '${path}': a MessageSpec must be a string or an object with '$msgid' and/or 'message'`);
|
|
246
|
+
|
|
247
|
+
const obj = /** @type {any} */ (spec);
|
|
248
|
+
const keys = Object.keys(obj);
|
|
249
|
+
for (let i = 0; i < keys.length; ++i) {
|
|
250
|
+
const key = keys[i];
|
|
251
|
+
if (key !== '$msgid' && key !== 'message' && key !== 'params')
|
|
252
|
+
throw new Error(`invalid 'errorMessage' spec at '${path}': unknown MessageSpec member '${key}'`);
|
|
253
|
+
}
|
|
254
|
+
if (obj.$msgid !== undefined && typeof obj.$msgid !== 'string')
|
|
255
|
+
throw new Error(`invalid 'errorMessage' spec at '${path}': '$msgid' must be a string`);
|
|
256
|
+
if (obj.message !== undefined && typeof obj.message !== 'string')
|
|
257
|
+
throw new Error(`invalid 'errorMessage' spec at '${path}': 'message' must be a string`);
|
|
258
|
+
if (obj.$msgid === undefined && obj.message === undefined)
|
|
259
|
+
throw new Error(`invalid 'errorMessage' spec at '${path}': a MessageSpec object needs '$msgid' and/or 'message'`);
|
|
260
|
+
if (obj.params !== undefined && (obj.params === null || typeof obj.params !== 'object' || Array.isArray(obj.params)))
|
|
261
|
+
throw new Error(`invalid 'errorMessage' spec at '${path}': 'params' must be an object`);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
msgid: obj.$msgid !== undefined ? obj.$msgid : null,
|
|
265
|
+
render: obj.message !== undefined ? compileMessageTemplate(obj.message) : null,
|
|
266
|
+
params: obj.params !== undefined ? obj.params : null,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* A compiled 'errorMessage' node registered on the ValidationRoot.
|
|
272
|
+
* @typedef {object} CompiledErrorMessageNode
|
|
273
|
+
* @property {CompiledMessageSpec|null} all - String-form spec: covers this node AND its subtree
|
|
274
|
+
* @property {Map<string, CompiledMessageSpec | {perKey: Map<string, CompiledMessageSpec>, fallback: CompiledMessageSpec|null}>|null} keywords - Map-form per-keyword specs (this node only)
|
|
275
|
+
* @property {CompiledMessageSpec|null} catchAll - The '_' entry (this node only)
|
|
276
|
+
*/
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Compile the value of an 'errorMessage' keyword into a registry node.
|
|
280
|
+
* Grammar (validated here, at schema compile time):
|
|
281
|
+
* - MessageSpec (string / `$msgid` object): covers the whole subtree;
|
|
282
|
+
* - map form: per-keyword MessageSpecs for this node, where `required`
|
|
283
|
+
* also accepts a per-missing-property map, `$query` a per-runtime-code
|
|
284
|
+
* map (with `default` for the EBV-false failure), and `_` is the
|
|
285
|
+
* node-level catch-all.
|
|
286
|
+
* @param {unknown} errorMessage - The keyword's value
|
|
287
|
+
* @param {string} path - The schema path, for compile error messages
|
|
288
|
+
* @returns {CompiledErrorMessageNode} The compiled node
|
|
289
|
+
*/
|
|
290
|
+
export function compileErrorMessageSpec(errorMessage, path) {
|
|
291
|
+
if (isMessageSpecValue(errorMessage))
|
|
292
|
+
return { all: compileMessageSpec(errorMessage, path), keywords: null, catchAll: null };
|
|
293
|
+
|
|
294
|
+
if (errorMessage === null || typeof errorMessage !== 'object' || Array.isArray(errorMessage))
|
|
295
|
+
throw new Error(`invalid 'errorMessage' at '${path}': must be a MessageSpec or a keyword map`);
|
|
296
|
+
|
|
297
|
+
/** @type {CompiledErrorMessageNode} */
|
|
298
|
+
const node = { all: null, keywords: null, catchAll: null };
|
|
299
|
+
const obj = /** @type {Record<string, unknown>} */ (errorMessage);
|
|
300
|
+
const keys = Object.keys(obj);
|
|
301
|
+
for (let i = 0; i < keys.length; ++i) {
|
|
302
|
+
const key = keys[i];
|
|
303
|
+
const value = obj[key];
|
|
304
|
+
if (key === '_') {
|
|
305
|
+
node.catchAll = compileMessageSpec(value, `${path}/errorMessage/_`);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (node.keywords === null) node.keywords = new Map();
|
|
309
|
+
const keyPath = `${path}/errorMessage/${key}`;
|
|
310
|
+
if ((key === 'required' || key === '$query') && !isMessageSpecValue(value)) {
|
|
311
|
+
// Per-key form: required -> per missing property, $query -> per
|
|
312
|
+
// runtime code with 'default' for the plain EBV-false failure.
|
|
313
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
314
|
+
throw new Error(`invalid 'errorMessage' spec at '${keyPath}': must be a MessageSpec or a map of MessageSpecs`);
|
|
315
|
+
const perKey = new Map();
|
|
316
|
+
/** @type {CompiledMessageSpec|null} */
|
|
317
|
+
let fallback = null;
|
|
318
|
+
const subKeys = Object.keys(value);
|
|
319
|
+
for (let j = 0; j < subKeys.length; ++j) {
|
|
320
|
+
const subKey = subKeys[j];
|
|
321
|
+
const compiled = compileMessageSpec(/** @type {any} */ (value)[subKey], `${keyPath}/${subKey}`);
|
|
322
|
+
if (key === '$query' && subKey === 'default') fallback = compiled;
|
|
323
|
+
else perKey.set(subKey, compiled);
|
|
324
|
+
}
|
|
325
|
+
node.keywords.set(key, { perKey, fallback });
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
node.keywords.set(key, compileMessageSpec(value, keyPath));
|
|
329
|
+
}
|
|
330
|
+
return node;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
//#endregion
|
|
334
|
+
|
|
335
|
+
//#region Internal error conversion (report time)
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Find the MessageSpec governing an error: nearest registered ancestor of
|
|
339
|
+
* the error's schema path wins (longest prefix, segment-aware); within one
|
|
340
|
+
* node, keyword-map entry beats '_' beats the string form; map-form
|
|
341
|
+
* entries apply only to errors AT the node, the string form covers the
|
|
342
|
+
* subtree. No match at the nearest node falls through to farther ancestors.
|
|
343
|
+
* @param {Map<string, CompiledErrorMessageNode>} registry - The root's errorMessage registry
|
|
344
|
+
* @param {string} errorPath - The error's schema path
|
|
345
|
+
* @param {string} keyword - The failed keyword
|
|
346
|
+
* @param {object} params - The extracted error params
|
|
347
|
+
* @returns {CompiledMessageSpec|null} The governing spec, or null
|
|
348
|
+
*/
|
|
349
|
+
function resolveErrorMessageSpec(registry, errorPath, keyword, params) {
|
|
350
|
+
/** @type {Array<{path: string, node: CompiledErrorMessageNode}>} */
|
|
351
|
+
const candidates = [];
|
|
352
|
+
for (const [path, node] of registry) {
|
|
353
|
+
if (path === errorPath
|
|
354
|
+
|| (errorPath.startsWith(path) && errorPath.charCodeAt(path.length) === 0x2f /* / */)) {
|
|
355
|
+
candidates.push({ path, node });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (candidates.length === 0) return null;
|
|
359
|
+
candidates.sort((a, b) => b.path.length - a.path.length);
|
|
360
|
+
|
|
361
|
+
for (let i = 0; i < candidates.length; ++i) {
|
|
362
|
+
const { path, node } = candidates[i];
|
|
363
|
+
if (path === errorPath) {
|
|
364
|
+
if (node.keywords !== null) {
|
|
365
|
+
const entry = node.keywords.get(keyword);
|
|
366
|
+
if (entry !== undefined) {
|
|
367
|
+
if ('perKey' in entry) {
|
|
368
|
+
const matchKey = keyword === 'required' ? params.missingProperty : params.code;
|
|
369
|
+
if (matchKey !== undefined && entry.perKey.has(matchKey))
|
|
370
|
+
return entry.perKey.get(matchKey);
|
|
371
|
+
if (keyword === '$query' && params.code === undefined && entry.fallback !== null)
|
|
372
|
+
return entry.fallback;
|
|
373
|
+
// no per-key match: fall through to '_' / string form
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
return entry;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (node.catchAll !== null) return node.catchAll;
|
|
381
|
+
}
|
|
382
|
+
if (node.all !== null) return node.all;
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Convert internal validation errors to the public ValidationError format.
|
|
389
|
+
* Params extraction is table-driven off the failed keyword; message text
|
|
390
|
+
* goes through the errorMessage registry (if any) and the built-in
|
|
391
|
+
* English catalog. With `options.messages === false` no message is
|
|
392
|
+
* rendered at all (`message: ''`, params and msgid still set).
|
|
393
|
+
* @param {Array<{object: any, key: string|string[], expected: any, dataKey: any, value: any, rest: any[]}>} internalErrors - The root's internal error records
|
|
394
|
+
* @returns {ValidationError[]} The public errors
|
|
395
|
+
*/
|
|
396
|
+
export function convertInternalErrors(internalErrors) {
|
|
397
|
+
return internalErrors.map(err => {
|
|
398
|
+
const keyword = Array.isArray(err.key) ? err.key[err.key.length - 1] : err.key;
|
|
399
|
+
|
|
400
|
+
// Build params based on error type
|
|
401
|
+
const params = {};
|
|
402
|
+
if (keyword === 'required') {
|
|
403
|
+
params.missingProperty = err.dataKey;
|
|
404
|
+
} else if (keyword === 'type') {
|
|
405
|
+
if (Array.isArray(err.expected)) {
|
|
406
|
+
params.types = err.expected;
|
|
407
|
+
} else {
|
|
408
|
+
params.type = err.expected;
|
|
409
|
+
}
|
|
410
|
+
} else if (['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'minLength', 'maxLength', 'minProperties', 'maxProperties', 'minItems', 'maxItems'].includes(keyword)) {
|
|
411
|
+
params.limit = err.expected;
|
|
412
|
+
if (keyword === 'minimum' || keyword === 'maximum') {
|
|
413
|
+
params.comparison = keyword === 'minimum' ? '>=' : '<=';
|
|
414
|
+
} else if (keyword === 'exclusiveMinimum' || keyword === 'exclusiveMaximum') {
|
|
415
|
+
params.comparison = keyword === 'exclusiveMinimum' ? '>' : '<';
|
|
416
|
+
}
|
|
417
|
+
} else if (keyword === 'multipleOf') {
|
|
418
|
+
params.multipleOf = err.expected;
|
|
419
|
+
} else if (keyword === 'pattern') {
|
|
420
|
+
params.pattern = err.expected?.source || err.expected;
|
|
421
|
+
} else if (keyword === 'additionalProperties') {
|
|
422
|
+
params.additionalProperty = err.dataKey;
|
|
423
|
+
} else if (keyword === 'format') {
|
|
424
|
+
params.format = err.expected || err.value;
|
|
425
|
+
} else if (keyword === '$query') {
|
|
426
|
+
// A '$query' runtime failure passes the JQ2xxx code and the query
|
|
427
|
+
// document pointer as extra meta arguments after the data path;
|
|
428
|
+
// a plain EBV-false failure passes neither.
|
|
429
|
+
if (err.rest != null && err.rest.length > 1) {
|
|
430
|
+
params.code = err.rest[1];
|
|
431
|
+
params.docPath = err.rest[2];
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Validators pass the instance data path as the first meta argument to
|
|
436
|
+
// the error handler (the handler-contract invariant); the charCode
|
|
437
|
+
// guard is a safety net that yields '' - never a wrong path.
|
|
438
|
+
const meta0 = err.rest?.[0];
|
|
439
|
+
const instancePath = (typeof meta0 === 'string' && (meta0 === '' || meta0.charCodeAt(0) === 0x2f))
|
|
440
|
+
? meta0
|
|
441
|
+
: '';
|
|
442
|
+
|
|
443
|
+
const schemaPath = err.object?.path || '';
|
|
444
|
+
const root = err.object?.root;
|
|
445
|
+
|
|
446
|
+
// Nearest-ancestor 'errorMessage' spec, if the schema registered any.
|
|
447
|
+
const registry = root?.errorMessages ?? null;
|
|
448
|
+
const spec = registry !== null
|
|
449
|
+
? resolveErrorMessageSpec(registry, schemaPath, keyword, params)
|
|
450
|
+
: null;
|
|
451
|
+
if (spec !== null && spec.params !== null) {
|
|
452
|
+
// Author params merge OVER the error's params - into the error
|
|
453
|
+
// record itself, so localizeErrors re-renders with them too.
|
|
454
|
+
Object.assign(params, spec.params);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const msgid = (spec !== null && spec.msgid !== null)
|
|
458
|
+
? spec.msgid
|
|
459
|
+
: (params.code !== undefined ? params.code : keyword);
|
|
460
|
+
|
|
461
|
+
const renderMessages = root?.options?.messages !== false;
|
|
462
|
+
let message = '';
|
|
463
|
+
let inline = false;
|
|
464
|
+
if (renderMessages) {
|
|
465
|
+
if (spec !== null) {
|
|
466
|
+
if (spec.msgid !== null) {
|
|
467
|
+
const render = EN[spec.msgid];
|
|
468
|
+
if (render !== undefined) {
|
|
469
|
+
message = render(params);
|
|
470
|
+
} else if (spec.render !== null) {
|
|
471
|
+
message = spec.render(params);
|
|
472
|
+
} else {
|
|
473
|
+
message = renderErrorMessage({ keyword, msgid, params });
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
message = /** @type {(params: object) => string} */ (spec.render)(params);
|
|
477
|
+
inline = true;
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
message = renderErrorMessage({ keyword, msgid, params });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const error = new ValidationError({
|
|
485
|
+
keyword,
|
|
486
|
+
instancePath,
|
|
487
|
+
schemaPath,
|
|
488
|
+
params,
|
|
489
|
+
msgid,
|
|
490
|
+
message,
|
|
491
|
+
});
|
|
492
|
+
if (inline) markInlineMessage(error);
|
|
493
|
+
return error;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
//#endregion
|