@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/src/tools.js
CHANGED
|
@@ -23,6 +23,14 @@ import {
|
|
|
23
23
|
isArrayish,
|
|
24
24
|
} from '@jarenjs/core/array';
|
|
25
25
|
|
|
26
|
+
import {
|
|
27
|
+
isJsonObject,
|
|
28
|
+
} from '@jarenjs/core/object';
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
JSONPOINTER_NOTHING,
|
|
32
|
+
} from '@jarenjs/json';
|
|
33
|
+
|
|
26
34
|
//#region Object
|
|
27
35
|
export function isBoolOrObjectClass(obj) {
|
|
28
36
|
return isBooleanType(obj)
|
|
@@ -77,6 +85,32 @@ export function hasSchemaDynamicRef(schema) {
|
|
|
77
85
|
&& !isStringWhiteSpace(schema.$dynamicRef);
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Whether a sibling keyword of unevaluatedProperties already evaluates every
|
|
90
|
+
* property of the instance. additionalProperties (boolean or schema) applies
|
|
91
|
+
* to each property not matched by properties/patternProperties, so once it
|
|
92
|
+
* has passed no property is left unevaluated.
|
|
93
|
+
* @param {object} schema - The schema holding the unevaluatedProperties keyword
|
|
94
|
+
* @returns {boolean} True when the unevaluatedProperties check can never match
|
|
95
|
+
*/
|
|
96
|
+
export function hasUnevaluatedPropertiesCoverage(schema) {
|
|
97
|
+
return isBoolOrObjectClass(schema.additionalProperties);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether a sibling keyword of unevaluatedItems already evaluates every item
|
|
102
|
+
* of the instance: a uniform items schema (boolean or object) covers all
|
|
103
|
+
* items beyond any prefixItems, and a tuple-form items with additionalItems
|
|
104
|
+
* covers the items beyond the tuple.
|
|
105
|
+
* @param {object} schema - The schema holding the unevaluatedItems keyword
|
|
106
|
+
* @returns {boolean} True when the unevaluatedItems check can never match
|
|
107
|
+
*/
|
|
108
|
+
export function hasUnevaluatedItemsCoverage(schema) {
|
|
109
|
+
const items = schema.items;
|
|
110
|
+
if (isBoolOrObjectClass(items)) return true;
|
|
111
|
+
return isArrayClass(items) && isBoolOrObjectClass(schema.additionalItems);
|
|
112
|
+
}
|
|
113
|
+
|
|
80
114
|
export function createIsSchemaTypeHandler(type, isStrict = false) {
|
|
81
115
|
switch (type) {
|
|
82
116
|
case 'null': return isNullValue;
|
|
@@ -109,6 +143,137 @@ export function createIsSchemaTypeHandler(type, isStrict = false) {
|
|
|
109
143
|
|
|
110
144
|
//#endregion
|
|
111
145
|
|
|
146
|
+
//#region Data references
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The fallback resolver of the data-reference keywords (`data`, `$data`):
|
|
150
|
+
* a ref that fails the strict compile keeps the lax keyword semantics, so
|
|
151
|
+
* it resolves as not-found and the keyword asserts nothing.
|
|
152
|
+
* @returns {any} the JSON Pointer not-found sentinel
|
|
153
|
+
*/
|
|
154
|
+
export const resolveNothing = () => JSONPOINTER_NOTHING;
|
|
155
|
+
|
|
156
|
+
const isDefined = (data) => data !== undefined;
|
|
157
|
+
const isJsonString = (data) => typeof data === 'string';
|
|
158
|
+
const isAnyValue = () => true;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build the keyword validators the two data-reference keywords share
|
|
162
|
+
* verbatim. The `data` keyword (json-everything, absolute + relative
|
|
163
|
+
* pointers via `compileDataRef`) and the Ajv-style `$data` keyword
|
|
164
|
+
* (relative pointers only) differ ONLY in which pointer compiler
|
|
165
|
+
* resolves a ref, so each module passes its own `compileRefResolver`
|
|
166
|
+
* and gets the same fifteen compilers back.
|
|
167
|
+
*
|
|
168
|
+
* Every validator follows one lax contract: a data instance outside the
|
|
169
|
+
* keyword's type, an unresolvable ref, or a resolved constraint of the
|
|
170
|
+
* wrong type asserts nothing.
|
|
171
|
+
*
|
|
172
|
+
* @param {(ref: string) => (dataRoot: any, dataPath: string) => any} compileRefResolver
|
|
173
|
+
* @returns {Record<string, (schemaObj: object, ref: string) => ((data: any, dataPath: string, dataRoot: any) => boolean) | undefined>}
|
|
174
|
+
*/
|
|
175
|
+
export function createDataRefCompilers(compileRefResolver) {
|
|
176
|
+
/**
|
|
177
|
+
* @param {string} keyword
|
|
178
|
+
* @param {(data: any) => boolean} accepts - Instance types the keyword constrains
|
|
179
|
+
* @param {(constraint: any) => boolean} expects - Resolved constraint types that assert
|
|
180
|
+
* @param {(data: any, constraint: any) => boolean} isValid
|
|
181
|
+
*/
|
|
182
|
+
const constraint = (keyword, accepts, expects, isValid) =>
|
|
183
|
+
(schemaObj, ref) => {
|
|
184
|
+
const addError = schemaObj.createErrorHandler(ref, keyword);
|
|
185
|
+
const resolveRef = compileRefResolver(ref);
|
|
186
|
+
|
|
187
|
+
return function validateDataRefConstraint(data, dataPath, dataRoot) {
|
|
188
|
+
if (!accepts(data)) return true;
|
|
189
|
+
|
|
190
|
+
const value = resolveRef(dataRoot, dataPath);
|
|
191
|
+
if (value === JSONPOINTER_NOTHING || !expects(value)) return true;
|
|
192
|
+
|
|
193
|
+
return isValid(data, value) || addError(data, dataPath, value);
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const compileFormat = (schemaObj, ref) => {
|
|
198
|
+
const formats = schemaObj.formats;
|
|
199
|
+
if (!formats) return undefined;
|
|
200
|
+
|
|
201
|
+
const addError = schemaObj.createErrorHandler(ref, 'format');
|
|
202
|
+
const resolveRef = compileRefResolver(ref);
|
|
203
|
+
|
|
204
|
+
// The registry holds format COMPILERS; compile (and cache) a validator
|
|
205
|
+
// per referenced format name at validation time.
|
|
206
|
+
const compiled = new Map();
|
|
207
|
+
const mockSchemaObj = {
|
|
208
|
+
createErrorHandler: () => () => false,
|
|
209
|
+
options: { skipErrors: true },
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return function validateDataRefFormat(data, dataPath, dataRoot) {
|
|
213
|
+
if (typeof data !== 'string') return true;
|
|
214
|
+
|
|
215
|
+
const formatName = resolveRef(dataRoot, dataPath);
|
|
216
|
+
if (formatName === JSONPOINTER_NOTHING || !isStringType(formatName)) return true;
|
|
217
|
+
|
|
218
|
+
let validator = compiled.get(formatName);
|
|
219
|
+
if (validator === undefined) {
|
|
220
|
+
const formatCompiler = formats[formatName];
|
|
221
|
+
validator = null;
|
|
222
|
+
if (formatCompiler) {
|
|
223
|
+
try {
|
|
224
|
+
const candidate = formatCompiler(mockSchemaObj, { format: formatName });
|
|
225
|
+
if (typeof candidate === 'function') validator = candidate;
|
|
226
|
+
} catch (_e) {
|
|
227
|
+
// An uncompilable format asserts nothing
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
compiled.set(formatName, validator);
|
|
231
|
+
}
|
|
232
|
+
if (validator === null) return true;
|
|
233
|
+
|
|
234
|
+
return validator(data, dataPath) || addError(data, dataPath, formatName);
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
__proto__: null,
|
|
240
|
+
minimum: constraint('minimum', isNumberType, isNumberType,
|
|
241
|
+
(data, min) => data >= min),
|
|
242
|
+
maximum: constraint('maximum', isNumberType, isNumberType,
|
|
243
|
+
(data, max) => data <= max),
|
|
244
|
+
exclusiveMinimum: constraint('exclusiveMinimum', isNumberType, isNumberType,
|
|
245
|
+
(data, min) => data > min),
|
|
246
|
+
exclusiveMaximum: constraint('exclusiveMaximum', isNumberType, isNumberType,
|
|
247
|
+
(data, max) => data < max),
|
|
248
|
+
multipleOf: constraint('multipleOf', isNumberType, isNumberType,
|
|
249
|
+
(data, multipleOf) => {
|
|
250
|
+
const q = data / multipleOf;
|
|
251
|
+
return Math.abs(q - Math.round(q)) < 1e-6;
|
|
252
|
+
}),
|
|
253
|
+
minLength: constraint('minLength', isJsonString, isNumberType,
|
|
254
|
+
(data, min) => data.length >= min),
|
|
255
|
+
maxLength: constraint('maxLength', isJsonString, isNumberType,
|
|
256
|
+
(data, max) => data.length <= max),
|
|
257
|
+
pattern: constraint('pattern', isJsonString, isStringType,
|
|
258
|
+
(data, pattern) => new RegExp(pattern, 'u').test(data)),
|
|
259
|
+
minItems: constraint('minItems', Array.isArray, isNumberType,
|
|
260
|
+
(data, min) => data.length >= min),
|
|
261
|
+
maxItems: constraint('maxItems', Array.isArray, isNumberType,
|
|
262
|
+
(data, max) => data.length <= max),
|
|
263
|
+
minProperties: constraint('minProperties', isJsonObject, isNumberType,
|
|
264
|
+
(data, min) => Object.keys(data).length >= min),
|
|
265
|
+
maxProperties: constraint('maxProperties', isJsonObject, isNumberType,
|
|
266
|
+
(data, max) => Object.keys(data).length <= max),
|
|
267
|
+
enum: constraint('enum', isDefined, Array.isArray,
|
|
268
|
+
(data, values) => values.includes(data)),
|
|
269
|
+
const: constraint('const', isDefined, isAnyValue,
|
|
270
|
+
(data, value) => data === value),
|
|
271
|
+
format: compileFormat,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
//#endregion
|
|
276
|
+
|
|
112
277
|
/**
|
|
113
278
|
* Records which properties (string keys) and items (numeric indexes) of a
|
|
114
279
|
* data instance were successfully evaluated during validation, so that
|
|
@@ -174,6 +339,33 @@ export class EvalLog {
|
|
|
174
339
|
}
|
|
175
340
|
}
|
|
176
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Combine INDEPENDENT keyword validators without short-circuiting.
|
|
344
|
+
*
|
|
345
|
+
* `a(...) && b(...)` is the right composition in boolean mode: the answer is
|
|
346
|
+
* known at the first failure and nothing is gained by continuing. When errors
|
|
347
|
+
* are recorded it is wrong, because each validator is the only thing that can
|
|
348
|
+
* report its own fault, so the first failure hides every sibling's. This runs
|
|
349
|
+
* all of them and ANDs the results — the boolean answer is identical, the
|
|
350
|
+
* error list is complete.
|
|
351
|
+
*
|
|
352
|
+
* Only use it where the validators genuinely are independent. A precondition
|
|
353
|
+
* (a type guard before a length check) must keep its short-circuit: running
|
|
354
|
+
* past it is meaningless at best and throws at worst.
|
|
355
|
+
* @param {Function[]} validators - Independent validators, in report order
|
|
356
|
+
* @returns {Function} A validator that runs every one of them
|
|
357
|
+
*/
|
|
358
|
+
export function combineIndependent(validators) {
|
|
359
|
+
return function validateIndependent(data, dataPath, dataRoot, dataKey) {
|
|
360
|
+
let valid = true;
|
|
361
|
+
for (let i = 0; i < validators.length; ++i) {
|
|
362
|
+
if (validators[i](data, dataPath, dataRoot, dataKey) === false)
|
|
363
|
+
valid = false;
|
|
364
|
+
}
|
|
365
|
+
return valid;
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
177
369
|
export class ValidationResult {
|
|
178
370
|
static undefThat() {
|
|
179
371
|
return new ValidationResult();
|
package/src/traverse.js
CHANGED
|
@@ -20,9 +20,14 @@ import {
|
|
|
20
20
|
isValidHtmlIdentifier,
|
|
21
21
|
} from '@jarenjs/core/text';
|
|
22
22
|
|
|
23
|
+
import {
|
|
24
|
+
encodeJSONPointerSegment,
|
|
25
|
+
decodeJSONPointerSegment,
|
|
26
|
+
} from '@jarenjs/json/pointer';
|
|
27
|
+
|
|
23
28
|
|
|
24
29
|
function encodeJsonPointerKey(key) {
|
|
25
|
-
return encodeURIComponent(key
|
|
30
|
+
return encodeURIComponent(encodeJSONPointerSegment(key));
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export function encodeJsonPointerPath(path, key, index) {
|
|
@@ -32,7 +37,11 @@ export function encodeJsonPointerPath(path, key, index) {
|
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
function decodeJsonPointerKey(key) {
|
|
35
|
-
|
|
40
|
+
// Lenient decode: `$ref` fragments arrive here unvalidated, so a stray
|
|
41
|
+
// `~` must pass through instead of throwing like the strict parser does.
|
|
42
|
+
// The percent-decode is this site's own: these keys arrive from a URI
|
|
43
|
+
// fragment, which the plain reference-token decode knows nothing about.
|
|
44
|
+
return decodeJSONPointerSegment(decodeURIComponent(key));
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
export function decodeJsonPointerPath(path) {
|
|
@@ -80,7 +89,7 @@ export function createJsonPointer(refUri, baseUri, opts = new JsonPointerOptions
|
|
|
80
89
|
url = new URL(refUri, 'http://example.com/' + effectiveBase);
|
|
81
90
|
// Restore the original baseUri in the result
|
|
82
91
|
const href = url.href.replace('http://example.com/', '');
|
|
83
|
-
const [
|
|
92
|
+
const [, fragment] = href.split('#');
|
|
84
93
|
return new JsonPointer(
|
|
85
94
|
effectiveBase + refUri,
|
|
86
95
|
undefined,
|
|
@@ -364,7 +373,7 @@ export function restoreSchemaRefsInMap(schemas, opts = new JsonPointerOptions())
|
|
|
364
373
|
if (finalId !== id && !schemas.has(finalId)) {
|
|
365
374
|
schemas.set(finalId, finalSchema);
|
|
366
375
|
}
|
|
367
|
-
} catch (
|
|
376
|
+
} catch (_e) {
|
|
368
377
|
// If deep resolution fails (remote ref not loaded), fall back to shallow resolution
|
|
369
378
|
// This preserves the original behavior for unresolved refs
|
|
370
379
|
const { schema } = resolveRefSchemaShallow(schemas, id, null, opts);
|
package/src/unevaluated.js
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
getBoolOrObjectClass,
|
|
10
|
+
hasUnevaluatedPropertiesCoverage,
|
|
11
|
+
hasUnevaluatedItemsCoverage,
|
|
10
12
|
} from './tools.js';
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -22,13 +24,20 @@ function compileUnevaluatedProperties(schemaObj, jsonSchema) {
|
|
|
22
24
|
const uneval = getBoolOrObjectClass(jsonSchema.unevaluatedProperties);
|
|
23
25
|
if (uneval == null) return undefined;
|
|
24
26
|
|
|
27
|
+
// In skipErrors mode reaching this final-stage check means every sibling
|
|
28
|
+
// keyword passed; with a sibling additionalProperties every property was
|
|
29
|
+
// then evaluated (and logged), so the check can never match and the
|
|
30
|
+
// sibling already produced the annotations any outer check consumes.
|
|
31
|
+
if (schemaObj.options.skipErrors && hasUnevaluatedPropertiesCoverage(jsonSchema))
|
|
32
|
+
return undefined;
|
|
33
|
+
|
|
25
34
|
const root = schemaObj.root;
|
|
26
35
|
const addError = schemaObj.createErrorHandler(uneval, 'unevaluatedProperties');
|
|
27
36
|
|
|
28
37
|
// true: everything left over is valid, but counts as evaluated for
|
|
29
38
|
// any unevaluatedProperties in an outer schema.
|
|
30
39
|
if (uneval === true) {
|
|
31
|
-
return function validateUnevaluatedPropertiesTrue(data,
|
|
40
|
+
return function validateUnevaluatedPropertiesTrue(data, _dataPath, _dataRoot, _mark) {
|
|
32
41
|
if (!isObjectType(data)) return true;
|
|
33
42
|
const log = root.evalLog;
|
|
34
43
|
const keys = Object.keys(data);
|
|
@@ -81,11 +90,19 @@ function compileUnevaluatedItems(schemaObj, jsonSchema) {
|
|
|
81
90
|
const uneval = getBoolOrObjectClass(jsonSchema.unevaluatedItems);
|
|
82
91
|
if (uneval == null) return undefined;
|
|
83
92
|
|
|
93
|
+
// In skipErrors mode reaching this final-stage check means every sibling
|
|
94
|
+
// keyword passed; with sibling coverage (uniform items, or tuple items
|
|
95
|
+
// plus additionalItems) every item was then evaluated (and logged), so
|
|
96
|
+
// the check can never match and the covering sibling already produced
|
|
97
|
+
// the annotations any outer check consumes.
|
|
98
|
+
if (schemaObj.options.skipErrors && hasUnevaluatedItemsCoverage(jsonSchema))
|
|
99
|
+
return undefined;
|
|
100
|
+
|
|
84
101
|
const root = schemaObj.root;
|
|
85
102
|
const addError = schemaObj.createErrorHandler(uneval, 'unevaluatedItems');
|
|
86
103
|
|
|
87
104
|
if (uneval === true) {
|
|
88
|
-
return function validateUnevaluatedItemsTrue(data,
|
|
105
|
+
return function validateUnevaluatedItemsTrue(data, _dataPath, _dataRoot, _mark) {
|
|
89
106
|
if (!isArrayClass(data)) return true;
|
|
90
107
|
root.evalLog.add(data, -1);
|
|
91
108
|
return true;
|
|
@@ -140,12 +157,17 @@ export function wrapUnevaluated(schemaObj, jsonSchema, validator) {
|
|
|
140
157
|
const unevalItems = compileUnevaluatedItems(schemaObj, jsonSchema);
|
|
141
158
|
if (unevalProps == null && unevalItems == null) return validator;
|
|
142
159
|
|
|
160
|
+
const stopAtFirst = root.options.skipErrors;
|
|
143
161
|
return function validateUnevaluatedSchema(data, dataPath, dataRoot, dataKey) {
|
|
144
162
|
const log = root.evalLog;
|
|
145
163
|
const mark = log.mark();
|
|
164
|
+
// The sibling result IS a precondition: unevaluated* reads annotations
|
|
165
|
+
// that a failed sibling may never have produced. But unevaluatedProperties
|
|
166
|
+
// and unevaluatedItems are independent of each other.
|
|
146
167
|
if (validator(data, dataPath, dataRoot, dataKey) === false) return false;
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
168
|
+
let valid = unevalProps == null || unevalProps(data, dataPath, dataRoot, mark) !== false;
|
|
169
|
+
if (stopAtFirst && !valid) return false;
|
|
170
|
+
if (unevalItems != null && unevalItems(data, dataPath, dataRoot, mark) === false) valid = false;
|
|
171
|
+
return valid;
|
|
150
172
|
};
|
|
151
173
|
}
|