@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/string.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isStringType,
|
|
5
|
+
} from '@jarenjs/core';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getIntishType,
|
|
9
|
+
} from '@jarenjs/core/number';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
createRegExp,
|
|
13
|
+
getStringLength,
|
|
14
|
+
} from '@jarenjs/core/string';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
trueThat,
|
|
18
|
+
} from '@jarenjs/core/function';
|
|
19
|
+
|
|
20
|
+
function compileMinLength(schemaObj, jsonSchema) {
|
|
21
|
+
const min = getIntishType(jsonSchema.minLength) || 0;
|
|
22
|
+
if (min < 1) return undefined;
|
|
23
|
+
|
|
24
|
+
const addError = schemaObj.createErrorHandler(min, 'minLength');
|
|
25
|
+
|
|
26
|
+
return function validateIsMinLength(len = 0, dataPath) {
|
|
27
|
+
return len >= min
|
|
28
|
+
|| addError(len, dataPath);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function compileMaxLength(schemaObj, jsonSchema) {
|
|
33
|
+
const max = getIntishType(jsonSchema.maxLength) || -1;
|
|
34
|
+
if (max < 0) return undefined;
|
|
35
|
+
|
|
36
|
+
const addError = schemaObj.createErrorHandler(max, 'maxLength');
|
|
37
|
+
|
|
38
|
+
return function validateIsMaxLength(len = 0, dataPath) {
|
|
39
|
+
return len <= max
|
|
40
|
+
|| addError(len, dataPath);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function compilePattern(schemaObj, jsonSchema) {
|
|
45
|
+
// Skip if pattern is a $data reference object (has $data property)
|
|
46
|
+
if (jsonSchema.pattern && typeof jsonSchema.pattern === 'object' && jsonSchema.pattern.$data) return undefined;
|
|
47
|
+
|
|
48
|
+
const pattern = createRegExp(jsonSchema.pattern);
|
|
49
|
+
if (pattern == null) return undefined;
|
|
50
|
+
|
|
51
|
+
const addError = schemaObj.createErrorHandler(pattern, 'pattern');
|
|
52
|
+
|
|
53
|
+
return function validateIsMatch(str = '', dataPath) {
|
|
54
|
+
return pattern.test(str)
|
|
55
|
+
|| addError(str, dataPath);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function compileStringIntern(schemaObj, jsonSchema) {
|
|
60
|
+
const minLength = compileMinLength(schemaObj, jsonSchema);
|
|
61
|
+
const maxLength = compileMaxLength(schemaObj, jsonSchema);
|
|
62
|
+
const pattern = compilePattern(schemaObj, jsonSchema);
|
|
63
|
+
|
|
64
|
+
if ((minLength || maxLength || pattern) == null) return undefined;
|
|
65
|
+
|
|
66
|
+
const isMinLength = minLength || trueThat;
|
|
67
|
+
const isMaxLength = maxLength || trueThat;
|
|
68
|
+
const isMatch = pattern || trueThat;
|
|
69
|
+
const useGrapheme = schemaObj.options.useGrapheme;
|
|
70
|
+
|
|
71
|
+
if (schemaObj.options.skipErrors) {
|
|
72
|
+
return function validateStringIntern(data, dataPath) {
|
|
73
|
+
const len = getStringLength(data, useGrapheme);
|
|
74
|
+
return isMinLength(len, dataPath)
|
|
75
|
+
&& isMaxLength(len, dataPath)
|
|
76
|
+
&& isMatch(data, dataPath);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Length and pattern are independent assertions over the same string:
|
|
81
|
+
// `len` is computed before any of them and `isMatch` reads the raw data,
|
|
82
|
+
// so a failed `minLength` says nothing about whether `pattern` holds.
|
|
83
|
+
// Short-circuiting them is a boolean-mode optimization; when errors are
|
|
84
|
+
// recorded it would hide half the reasons the value is wrong.
|
|
85
|
+
return function validateStringInternAll(data, dataPath) {
|
|
86
|
+
const len = getStringLength(data, useGrapheme);
|
|
87
|
+
let valid = isMinLength(len, dataPath);
|
|
88
|
+
valid = isMaxLength(len, dataPath) && valid;
|
|
89
|
+
return isMatch(data, dataPath) && valid;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function compileStringBasic(schemaObj, jsonSchema) {
|
|
94
|
+
const intern = compileStringIntern(schemaObj, jsonSchema);
|
|
95
|
+
if (intern == null) return undefined;
|
|
96
|
+
|
|
97
|
+
// Fast path for simple maxLength-only schemas (most common case)
|
|
98
|
+
// This inlines the validation to reduce function call overhead
|
|
99
|
+
const max = getIntishType(jsonSchema.maxLength) ?? -1;
|
|
100
|
+
const min = getIntishType(jsonSchema.minLength) || 0;
|
|
101
|
+
const hasPattern = jsonSchema.pattern != null;
|
|
102
|
+
const useGrapheme = schemaObj.options.useGrapheme;
|
|
103
|
+
|
|
104
|
+
if (!hasPattern) {
|
|
105
|
+
if (!useGrapheme) {
|
|
106
|
+
// Simple maxLength-only without grapheme counting
|
|
107
|
+
if (max >= 0 && min < 1) {
|
|
108
|
+
const addError = schemaObj.createErrorHandler(max, 'maxLength');
|
|
109
|
+
return function validateStringMaxLength(data, dataPath) {
|
|
110
|
+
if (!isStringType(data)) return true;
|
|
111
|
+
return data.length <= max || addError(data.length, dataPath);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Simple minLength-only without grapheme counting
|
|
116
|
+
if (min > 0 && max < 0) {
|
|
117
|
+
const addError = schemaObj.createErrorHandler(min, 'minLength');
|
|
118
|
+
return function validateStringMinLength(data, dataPath) {
|
|
119
|
+
if (!isStringType(data)) return true;
|
|
120
|
+
return data.length >= min || addError(data.length, dataPath);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// Fast path WITH grapheme counting - inline the ASCII fast-path logic
|
|
125
|
+
// This avoids the function call overhead of getStringLength for ASCII strings
|
|
126
|
+
if (max >= 0 && min < 1) {
|
|
127
|
+
const addError = schemaObj.createErrorHandler(max, 'maxLength');
|
|
128
|
+
return function validateStringMaxLengthGrapheme(data, dataPath) {
|
|
129
|
+
if (!isStringType(data)) return true;
|
|
130
|
+
const len = getStringLength(data, true);
|
|
131
|
+
return len <= max || addError(len, dataPath);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (min > 0 && max < 0) {
|
|
136
|
+
const addError = schemaObj.createErrorHandler(min, 'minLength');
|
|
137
|
+
return function validateStringMinLengthGrapheme(data, dataPath) {
|
|
138
|
+
if (!isStringType(data)) return true;
|
|
139
|
+
const len = getStringLength(data, true);
|
|
140
|
+
return len >= min || addError(len, dataPath);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Fast path for pattern-only schemas (common in ecmascript-regex tests)
|
|
147
|
+
// This eliminates the intermediate function call overhead
|
|
148
|
+
if (hasPattern && max < 0 && min < 1) {
|
|
149
|
+
const pattern = createRegExp(jsonSchema.pattern);
|
|
150
|
+
if (pattern != null) {
|
|
151
|
+
const addError = schemaObj.createErrorHandler(pattern, 'pattern');
|
|
152
|
+
return function validateStringPatternOnly(data, dataPath) {
|
|
153
|
+
if (!isStringType(data)) return true;
|
|
154
|
+
return pattern.test(data) || addError(data, dataPath);
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Generic case
|
|
160
|
+
return function validateStringBasic(data, dataPath) {
|
|
161
|
+
return !isStringType(data)
|
|
162
|
+
|| intern(data, dataPath);
|
|
163
|
+
};
|
|
164
|
+
}
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isBigIntType,
|
|
5
|
+
isNullValue,
|
|
6
|
+
isBooleanType,
|
|
7
|
+
isIntegerType,
|
|
8
|
+
isNumberType,
|
|
9
|
+
isStringType,
|
|
10
|
+
isObjectClass,
|
|
11
|
+
isObjectType,
|
|
12
|
+
isMapClass,
|
|
13
|
+
isArrayClass,
|
|
14
|
+
isSetClass,
|
|
15
|
+
} from '@jarenjs/core';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
isRegExpType,
|
|
19
|
+
isStringWhiteSpace,
|
|
20
|
+
} from '@jarenjs/core/string';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
isArrayish,
|
|
24
|
+
} from '@jarenjs/core/array';
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
isJsonObject,
|
|
28
|
+
} from '@jarenjs/core/object';
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
JSONPOINTER_NOTHING,
|
|
32
|
+
} from '@jarenjs/json';
|
|
33
|
+
|
|
34
|
+
//#region Object
|
|
35
|
+
export function isBoolOrObjectClass(obj) {
|
|
36
|
+
return isBooleanType(obj)
|
|
37
|
+
|| isObjectClass(obj);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* getBoolOrObjectClass
|
|
42
|
+
* extract the data of first parameter if data is boolean or object type otherwise return default
|
|
43
|
+
* @param {any} obj any data data has to be tested on boolean or object type
|
|
44
|
+
* @param {boolean | object | undefined} def default return type if not boolean or object type
|
|
45
|
+
* @returns {boolean | undefined} return value when boolean or object otherwise def
|
|
46
|
+
*/
|
|
47
|
+
export function getBoolOrObjectClass(obj, def = undefined) {
|
|
48
|
+
return isBoolOrObjectClass(obj) ? obj : def;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getArrayClassMinItems(obj, len = 1, def = undefined) {
|
|
52
|
+
return (isArrayClass(obj) && obj.length >= len && obj) || def;
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
|
|
56
|
+
//#region Schema Helpers
|
|
57
|
+
export function isOfSchemaType(schema, type) {
|
|
58
|
+
const stype = schema.type;
|
|
59
|
+
if (stype == null) return false;
|
|
60
|
+
if (stype === type) return true;
|
|
61
|
+
if (stype.constructor === Array) {
|
|
62
|
+
return stype.includes(type);
|
|
63
|
+
}
|
|
64
|
+
if (stype.constructor === Set) {
|
|
65
|
+
return stype.has(type);
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function hasSchemaRef(schema) {
|
|
71
|
+
return isObjectClass(schema)
|
|
72
|
+
&& isStringType(schema.$ref)
|
|
73
|
+
&& !isStringWhiteSpace(schema.$ref);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function hasSchemaRecursiveRef(schema) {
|
|
77
|
+
return isObjectClass(schema)
|
|
78
|
+
&& isStringType(schema.$recursiveRef)
|
|
79
|
+
&& !isStringWhiteSpace(schema.$recursiveRef);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function hasSchemaDynamicRef(schema) {
|
|
83
|
+
return isObjectClass(schema)
|
|
84
|
+
&& isStringType(schema.$dynamicRef)
|
|
85
|
+
&& !isStringWhiteSpace(schema.$dynamicRef);
|
|
86
|
+
}
|
|
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
|
+
|
|
114
|
+
export function createIsSchemaTypeHandler(type, isStrict = false) {
|
|
115
|
+
switch (type) {
|
|
116
|
+
case 'null': return isNullValue;
|
|
117
|
+
case 'boolean': return isBooleanType;
|
|
118
|
+
case 'integer': return isIntegerType;
|
|
119
|
+
case 'bigint': return isBigIntType;
|
|
120
|
+
case 'number': return isNumberType;
|
|
121
|
+
case 'string': return isStringType;
|
|
122
|
+
case 'object': return isStrict
|
|
123
|
+
? isObjectClass
|
|
124
|
+
: isObjectType;
|
|
125
|
+
case 'array': return isStrict
|
|
126
|
+
? isArrayish
|
|
127
|
+
: isArrayClass;
|
|
128
|
+
case 'set': return isSetClass;
|
|
129
|
+
case 'map': return isMapClass;
|
|
130
|
+
case 'tuple': return isArrayClass;
|
|
131
|
+
case 'regex': return isRegExpType;
|
|
132
|
+
default: break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (type === null)
|
|
136
|
+
return isNullValue;
|
|
137
|
+
|
|
138
|
+
if (typeof type === 'function')
|
|
139
|
+
throw new Error('This is interesting!');
|
|
140
|
+
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//#endregion
|
|
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
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Records which properties (string keys) and items (numeric indexes) of a
|
|
279
|
+
* data instance were successfully evaluated during validation, so that
|
|
280
|
+
* unevaluatedProperties/unevaluatedItems can be checked afterwards.
|
|
281
|
+
*
|
|
282
|
+
* Entries are (data reference, key) pairs appended in application order.
|
|
283
|
+
* Applicators that discard annotations (failed anyOf/oneOf branches, not,
|
|
284
|
+
* failed if) take a mark() before running and rollback(mark) afterwards.
|
|
285
|
+
* The numeric key -1 means "all items of this array were evaluated".
|
|
286
|
+
*/
|
|
287
|
+
export class EvalLog {
|
|
288
|
+
#data = [];
|
|
289
|
+
#keys = [];
|
|
290
|
+
#len = 0;
|
|
291
|
+
|
|
292
|
+
/** Clears the log; called at the start of each root validation. */
|
|
293
|
+
reset() {
|
|
294
|
+
this.#data.length = 0;
|
|
295
|
+
this.#keys.length = 0;
|
|
296
|
+
this.#len = 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** @returns {number} The current log position */
|
|
300
|
+
mark() {
|
|
301
|
+
return this.#len;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Discards all entries recorded after the given mark. */
|
|
305
|
+
rollback(mark) {
|
|
306
|
+
this.#len = mark;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Records that `key` of instance `data` was evaluated. */
|
|
310
|
+
add(data, key) {
|
|
311
|
+
this.#data[this.#len] = data;
|
|
312
|
+
this.#keys[this.#len] = key;
|
|
313
|
+
this.#len++;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** @returns {boolean} True when property `key` of `data` was evaluated at or after `from` */
|
|
317
|
+
hasKey(data, key, from) {
|
|
318
|
+
const len = this.#len;
|
|
319
|
+
const datas = this.#data;
|
|
320
|
+
const keys = this.#keys;
|
|
321
|
+
for (let i = from; i < len; ++i) {
|
|
322
|
+
if (datas[i] === data && keys[i] === key) return true;
|
|
323
|
+
}
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** @returns {boolean} True when item `index` of `data` was evaluated at or after `from` (-1 entries cover all items) */
|
|
328
|
+
hasItem(data, index, from) {
|
|
329
|
+
const len = this.#len;
|
|
330
|
+
const datas = this.#data;
|
|
331
|
+
const keys = this.#keys;
|
|
332
|
+
for (let i = from; i < len; ++i) {
|
|
333
|
+
if (datas[i] === data) {
|
|
334
|
+
const k = keys[i];
|
|
335
|
+
if (k === index || k === -1) return true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
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
|
+
|
|
369
|
+
export class ValidationResult {
|
|
370
|
+
static undefThat() {
|
|
371
|
+
return new ValidationResult();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
constructor(match = false, errors = 0) {
|
|
375
|
+
this.match = match;
|
|
376
|
+
this.errors = Number(errors);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
addValid(valid = true) {
|
|
380
|
+
if (valid === false)// this.errors += valid|0
|
|
381
|
+
this.errors++;
|
|
382
|
+
return this;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
addMatch(valid = true) {
|
|
386
|
+
this.match = true;
|
|
387
|
+
if (valid === false)// this.errors += valid|0
|
|
388
|
+
this.errors++;
|
|
389
|
+
return this;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
addResult(result = new ValidationResult()) {
|
|
393
|
+
this.match = this.match || result.match;
|
|
394
|
+
this.errors += result.errors;
|
|
395
|
+
return this;
|
|
396
|
+
}
|
|
397
|
+
}
|