@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/bigint.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isBigIntType,
|
|
5
|
+
getBigIntType,
|
|
6
|
+
getInclusiveExclusiveBounds,
|
|
7
|
+
} from '@jarenjs/core';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
trueThat,
|
|
11
|
+
} from '@jarenjs/core/function';
|
|
12
|
+
|
|
13
|
+
function compileBigIntMaximum(schemaObj, jsonSchema) {
|
|
14
|
+
const [max, emax] = getInclusiveExclusiveBounds(
|
|
15
|
+
getBigIntType,
|
|
16
|
+
jsonSchema.maximum,
|
|
17
|
+
jsonSchema.exclusiveMaximum,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (emax != null) {
|
|
21
|
+
const addError = schemaObj.createErrorHandler(emax, 'exclusiveMaximum');
|
|
22
|
+
|
|
23
|
+
return function validateExclusiveMaximumBigInt(data, dataPath) {
|
|
24
|
+
return data < emax
|
|
25
|
+
|| addError(data, dataPath);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
else if (max != null) {
|
|
29
|
+
const addError = schemaObj.createErrorHandler(max, 'maximum');
|
|
30
|
+
|
|
31
|
+
return function validateMaximumBigInt(data, dataPath) {
|
|
32
|
+
return data <= max
|
|
33
|
+
|| addError(data, dataPath);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compileBigIntMinimum(schemaObj, jsonSchema) {
|
|
41
|
+
const [min, emin] = getInclusiveExclusiveBounds(
|
|
42
|
+
getBigIntType,
|
|
43
|
+
jsonSchema.minimum,
|
|
44
|
+
jsonSchema.exclusiveMinimum,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (emin != null) {
|
|
48
|
+
const addError = schemaObj.createErrorHandler(emin, 'exclusiveMinimum');
|
|
49
|
+
|
|
50
|
+
return function validateExclusiveMinimumBigInt(data, dataPath) {
|
|
51
|
+
return data > emin
|
|
52
|
+
|| addError(data, dataPath);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
else if (min != null) {
|
|
56
|
+
const addError = schemaObj.createErrorHandler(min, 'minimum');
|
|
57
|
+
|
|
58
|
+
return function validateMinimumBigInt(data, dataPath) {
|
|
59
|
+
return data >= min
|
|
60
|
+
|| addError(data, dataPath);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function compileBigIntMultipleOf(schemaObj, jsonSchema) {
|
|
68
|
+
const mulOf = getBigIntType(jsonSchema.multipleOf);
|
|
69
|
+
if (mulOf == null) return undefined;
|
|
70
|
+
|
|
71
|
+
const addError = schemaObj.createErrorHandler(mulOf, 'multipleOf');
|
|
72
|
+
|
|
73
|
+
return function validateMultipleOfBigInt(data, dataPath) {
|
|
74
|
+
return data % mulOf === BigInt(0)
|
|
75
|
+
|| addError(data, dataPath);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function compileBigIntBasic(schemaObj, jsonSchema) {
|
|
80
|
+
const maximum = compileBigIntMaximum(schemaObj, jsonSchema);
|
|
81
|
+
const minimum = compileBigIntMinimum(schemaObj, jsonSchema);
|
|
82
|
+
const multipleOf = compileBigIntMultipleOf(schemaObj, jsonSchema);
|
|
83
|
+
if (maximum == null && minimum == null && multipleOf == null) return undefined;
|
|
84
|
+
|
|
85
|
+
const isMax = maximum || trueThat;
|
|
86
|
+
const isMin = minimum || trueThat;
|
|
87
|
+
const isMul = multipleOf || trueThat;
|
|
88
|
+
|
|
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;
|
|
107
|
+
};
|
|
108
|
+
}
|
package/src/combine.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isFn,
|
|
5
|
+
} from '@jarenjs/core';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
falseThat,
|
|
9
|
+
trueThat,
|
|
10
|
+
} from '@jarenjs/core/function';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
combineIndependent,
|
|
14
|
+
getBoolOrObjectClass,
|
|
15
|
+
getArrayClassMinItems,
|
|
16
|
+
} from './tools.js';
|
|
17
|
+
|
|
18
|
+
function compileAllOf(schemaObj, jsonSchema) {
|
|
19
|
+
const allOf = getArrayClassMinItems(jsonSchema.allOf, 1);
|
|
20
|
+
if (allOf == null) return undefined;
|
|
21
|
+
|
|
22
|
+
const validators = [];
|
|
23
|
+
for (let i = 0; i < allOf.length; ++i) {
|
|
24
|
+
const child = allOf[i];
|
|
25
|
+
if (child === true)
|
|
26
|
+
validators.push(trueThat);
|
|
27
|
+
else if (child === false)
|
|
28
|
+
validators.push(falseThat);
|
|
29
|
+
else {
|
|
30
|
+
const validator = schemaObj.createValidator(child, 'allOf', i);
|
|
31
|
+
validators.push(validator);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (validators.length === 0) return undefined;
|
|
35
|
+
|
|
36
|
+
const addError = schemaObj.createErrorHandler(allOf, 'allOf');
|
|
37
|
+
|
|
38
|
+
const stopAtFirst = schemaObj.options.skipErrors;
|
|
39
|
+
return function validateAllOf(data, dataPath, dataRoot, dataKey) {
|
|
40
|
+
if (data !== undefined) {
|
|
41
|
+
// EVERY allOf branch applies, so every branch's faults are real. Only
|
|
42
|
+
// the boolean answer may stop at the first failing branch.
|
|
43
|
+
let valid = true;
|
|
44
|
+
for (let i = 0; i < validators.length; ++i) {
|
|
45
|
+
const validator = validators[i];
|
|
46
|
+
if (validator(data, dataPath, dataRoot, dataKey) === false) {
|
|
47
|
+
valid = addError(data, dataPath);
|
|
48
|
+
if (stopAtFirst) return valid;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return valid;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function compileAnyOf(schemaObj, jsonSchema) {
|
|
58
|
+
const anyOf = getArrayClassMinItems(jsonSchema.anyOf, 1);
|
|
59
|
+
if (anyOf == null) return undefined;
|
|
60
|
+
|
|
61
|
+
const validators = [];
|
|
62
|
+
for (let i = 0; i < anyOf.length; ++i) {
|
|
63
|
+
const child = anyOf[i];
|
|
64
|
+
if (child === true)
|
|
65
|
+
validators.push(trueThat);
|
|
66
|
+
else if (child === false)
|
|
67
|
+
validators.push(falseThat);
|
|
68
|
+
else {
|
|
69
|
+
const validator = schemaObj.createValidator(child, 'anyOf', i);
|
|
70
|
+
validators.push(validator);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (validators.length === 0) return undefined;
|
|
74
|
+
|
|
75
|
+
const addError = schemaObj.createErrorHandler(anyOf, 'anyOf');
|
|
76
|
+
|
|
77
|
+
// With annotation tracking every branch must run (annotations from ALL
|
|
78
|
+
// successful branches count for unevaluated*), and annotations from
|
|
79
|
+
// failed branches are rolled back.
|
|
80
|
+
const root = schemaObj.root;
|
|
81
|
+
if (root.usesUnevaluated) {
|
|
82
|
+
return function validateAnyOfTracked(data, dataPath, dataRoot, dataKey) {
|
|
83
|
+
if (data === undefined) return true;
|
|
84
|
+
const log = root.evalLog;
|
|
85
|
+
const errors = root.errorMark();
|
|
86
|
+
let found = false;
|
|
87
|
+
for (let i = 0; i < validators.length; ++i) {
|
|
88
|
+
const mark = log.mark();
|
|
89
|
+
if (validators[i](data, dataPath, dataRoot, dataKey) === true)
|
|
90
|
+
found = true;
|
|
91
|
+
else
|
|
92
|
+
log.rollback(mark);
|
|
93
|
+
}
|
|
94
|
+
// Annotation tracking runs every branch, so a match leaves the failed
|
|
95
|
+
// branches' errors behind unless they are rolled back here too.
|
|
96
|
+
if (found) root.rollbackErrors(errors);
|
|
97
|
+
return found || addError(data, dataPath);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return function validateAnyOf(data, dataPath, dataRoot, dataKey) {
|
|
102
|
+
if (data !== undefined) {
|
|
103
|
+
// Every branch tried before a match is a SPECULATIVE probe: the document
|
|
104
|
+
// was never required to satisfy it. Its errors are kept only if no
|
|
105
|
+
// branch matched at all, where they are the explanation.
|
|
106
|
+
const mark = root.errorMark();
|
|
107
|
+
for (let i = 0; i < validators.length; ++i) {
|
|
108
|
+
if (validators[i](data, dataPath, dataRoot, dataKey) === true) {
|
|
109
|
+
root.rollbackErrors(mark);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return addError(data, dataPath);
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function compileOneOf(schemaObj, jsonSchema) {
|
|
120
|
+
const oneOf = getArrayClassMinItems(jsonSchema.oneOf, 1);
|
|
121
|
+
if (oneOf == null)
|
|
122
|
+
return undefined;
|
|
123
|
+
|
|
124
|
+
const validators = [];
|
|
125
|
+
for (let i = 0; i < oneOf.length; ++i) {
|
|
126
|
+
const child = oneOf[i];
|
|
127
|
+
if (child === true)
|
|
128
|
+
validators.push(trueThat);
|
|
129
|
+
else if (child === false)
|
|
130
|
+
validators.push(falseThat);
|
|
131
|
+
else {
|
|
132
|
+
const validator = schemaObj.createValidator(child, 'oneOf', i);
|
|
133
|
+
validators.push(validator);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (validators.length === 0)
|
|
137
|
+
return undefined;
|
|
138
|
+
|
|
139
|
+
const addError = schemaObj.createErrorHandler(oneOf, 'oneOf');
|
|
140
|
+
|
|
141
|
+
// With annotation tracking, annotations from failed branches are rolled back.
|
|
142
|
+
const root = schemaObj.root;
|
|
143
|
+
if (root.usesUnevaluated) {
|
|
144
|
+
return function validateOneOfTracked(data, dataPath, dataRoot, dataKey) {
|
|
145
|
+
const log = root.evalLog;
|
|
146
|
+
const errors = root.errorMark();
|
|
147
|
+
let found = false;
|
|
148
|
+
for (let i = 0; i < validators.length; ++i) {
|
|
149
|
+
const mark = log.mark();
|
|
150
|
+
if (validators[i](data, dataPath, dataRoot, dataKey) === true) {
|
|
151
|
+
if (found === true) {
|
|
152
|
+
root.rollbackErrors(errors);
|
|
153
|
+
return addError(data, dataPath);
|
|
154
|
+
}
|
|
155
|
+
found = true;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
log.rollback(mark);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Non-matching branches were speculative; their complaints survive only
|
|
162
|
+
// when nothing matched and they are the explanation.
|
|
163
|
+
if (found) root.rollbackErrors(errors);
|
|
164
|
+
return found || addError(data, dataPath);
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return function validateOneOf(data, dataPath, dataRoot, dataKey) {
|
|
169
|
+
const errors = root.errorMark();
|
|
170
|
+
let found = false;
|
|
171
|
+
for (let i = 0; i < validators.length; ++i) {
|
|
172
|
+
if (validators[i](data, dataPath, dataRoot, dataKey) === true) {
|
|
173
|
+
if (found === true) {
|
|
174
|
+
// Matching twice is the failure; the branches' own errors explain
|
|
175
|
+
// nothing about it.
|
|
176
|
+
root.rollbackErrors(errors);
|
|
177
|
+
return addError(data, dataPath);
|
|
178
|
+
}
|
|
179
|
+
found = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (found) root.rollbackErrors(errors);
|
|
183
|
+
return found || addError(data, dataPath);
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function compileNotOf(schemaObj, jsonSchema) {
|
|
188
|
+
const notOf = getBoolOrObjectClass(jsonSchema.not);
|
|
189
|
+
if (notOf == null) return undefined;
|
|
190
|
+
if (notOf === false) return trueThat;
|
|
191
|
+
|
|
192
|
+
const addError = schemaObj.createErrorHandler(notOf, 'not');
|
|
193
|
+
|
|
194
|
+
// `not: true` rejects every instance. It still has to SAY so: returning a
|
|
195
|
+
// bare false made the only schema that can fail without explanation, and a
|
|
196
|
+
// collector that reports "invalid" with an empty error list is unusable by
|
|
197
|
+
// a caller trying to render why.
|
|
198
|
+
if (notOf === true)
|
|
199
|
+
return function validateNotOfTrue(data, dataPath) {
|
|
200
|
+
return data === undefined ? true : addError(data, dataPath);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const validate = schemaObj.createValidator(notOf, 'not');
|
|
204
|
+
|
|
205
|
+
// Annotations produced inside a 'not' are never kept, whatever the outcome.
|
|
206
|
+
const root = schemaObj.root;
|
|
207
|
+
if (root.usesUnevaluated) {
|
|
208
|
+
return function validateNotOfTracked(data, dataPath, dataRoot, dataKey) {
|
|
209
|
+
if (data === undefined) return true;
|
|
210
|
+
const log = root.evalLog;
|
|
211
|
+
const mark = log.mark();
|
|
212
|
+
const errors = root.errorMark();
|
|
213
|
+
const valid = validate(data, dataPath, dataRoot, dataKey);
|
|
214
|
+
log.rollback(mark);
|
|
215
|
+
// A `not` whose subschema FAILS is a `not` that passed, so the
|
|
216
|
+
// subschema's complaints are never the caller's business either way.
|
|
217
|
+
root.rollbackErrors(errors);
|
|
218
|
+
return valid === false
|
|
219
|
+
? true
|
|
220
|
+
: addError(data, dataPath);
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return function validateNotOf(data, dataPath, dataRoot, dataKey) {
|
|
225
|
+
if (data === undefined) return true;
|
|
226
|
+
// The subschema is a probe: its failure is what makes `not` succeed, so
|
|
227
|
+
// its errors are discarded whichever way the probe went.
|
|
228
|
+
const errors = root.errorMark();
|
|
229
|
+
const valid = validate(data, dataPath, dataRoot, dataKey);
|
|
230
|
+
root.rollbackErrors(errors);
|
|
231
|
+
return valid === false
|
|
232
|
+
? true
|
|
233
|
+
: addError(data, dataPath);
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function compileCombineSchema(schemaObj, jsonSchema) {
|
|
238
|
+
const validators = [];
|
|
239
|
+
function addValidator(compiler) {
|
|
240
|
+
if (isFn(compiler))
|
|
241
|
+
validators.push(compiler);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
addValidator(compileAllOf(schemaObj, jsonSchema));
|
|
245
|
+
addValidator(compileAnyOf(schemaObj, jsonSchema));
|
|
246
|
+
addValidator(compileOneOf(schemaObj, jsonSchema));
|
|
247
|
+
addValidator(compileNotOf(schemaObj, jsonSchema));
|
|
248
|
+
|
|
249
|
+
if (validators.length === 0)
|
|
250
|
+
return undefined;
|
|
251
|
+
if (validators.length === 1)
|
|
252
|
+
return validators[0];
|
|
253
|
+
// allOf/anyOf/oneOf/not are independent applicator groups: a failing
|
|
254
|
+
// `oneOf` says nothing about whether `not` also failed.
|
|
255
|
+
if (!schemaObj.options.skipErrors)
|
|
256
|
+
return combineIndependent(validators);
|
|
257
|
+
|
|
258
|
+
if (validators.length === 2) {
|
|
259
|
+
const first = validators[0];
|
|
260
|
+
const second = validators[1];
|
|
261
|
+
return function validateCombinaSchemaPair(data, dataPath, dataRoot, dataKey) {
|
|
262
|
+
return first(data, dataPath, dataRoot, dataKey)
|
|
263
|
+
&& second(data, dataPath, dataRoot, dataKey);
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
return function validateCombineSchema(data, dataPath, dataRoot, dataKey) {
|
|
268
|
+
for (let i = 0; i < validators.length; ++i) {
|
|
269
|
+
const validator = validators[i];
|
|
270
|
+
if (validator(data, dataPath, dataRoot, dataKey) === false)
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
package/src/condition.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
fallbackFn,
|
|
5
|
+
} from '@jarenjs/core/function';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
isObjectClass,
|
|
9
|
+
} from '@jarenjs/core';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
hasRecursiveAnchor,
|
|
13
|
+
getDynamicAnchorName,
|
|
14
|
+
} from './dynamic-ref.js';
|
|
15
|
+
|
|
16
|
+
export function compileConditionSchema(schemaObj, jsonSchema) {
|
|
17
|
+
const root = schemaObj.root;
|
|
18
|
+
const validateIf = schemaObj.createValidator(jsonSchema.if, 'if');
|
|
19
|
+
const tmpThen = schemaObj.createValidator(jsonSchema.then, 'then');
|
|
20
|
+
const tmpElse = schemaObj.createValidator(jsonSchema.else, 'else');
|
|
21
|
+
|
|
22
|
+
if (validateIf == null) return undefined;
|
|
23
|
+
if (tmpThen == null && tmpElse == null) {
|
|
24
|
+
// A lone 'if' asserts nothing, but with annotation tracking its
|
|
25
|
+
// annotations (when it passes) must stay visible to unevaluated*.
|
|
26
|
+
if (root.usesUnevaluated) {
|
|
27
|
+
return function validateLoneIf(data, dataPath, dataRoot, dataKey) {
|
|
28
|
+
const log = root.evalLog;
|
|
29
|
+
const mark = log.mark();
|
|
30
|
+
// A lone `if` asserts NOTHING, so it can never explain a failure and
|
|
31
|
+
// its errors are always discarded. Only its annotations survive, and
|
|
32
|
+
// only when it passed.
|
|
33
|
+
const errors = root.errorMark();
|
|
34
|
+
if (validateIf(data, dataPath, dataRoot, dataKey) === false)
|
|
35
|
+
log.rollback(mark);
|
|
36
|
+
root.rollbackErrors(errors);
|
|
37
|
+
return true;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if then/else schemas have dynamic anchors
|
|
44
|
+
const thenSchema = jsonSchema.then;
|
|
45
|
+
const elseSchema = jsonSchema.else;
|
|
46
|
+
|
|
47
|
+
const thenHasRecAnchor = isObjectClass(thenSchema) && hasRecursiveAnchor(thenSchema);
|
|
48
|
+
const thenDynAnchorName = isObjectClass(thenSchema) ? getDynamicAnchorName(thenSchema) : null;
|
|
49
|
+
const elseHasRecAnchor = isObjectClass(elseSchema) && hasRecursiveAnchor(elseSchema);
|
|
50
|
+
const elseDynAnchorName = isObjectClass(elseSchema) ? getDynamicAnchorName(elseSchema) : null;
|
|
51
|
+
|
|
52
|
+
const thenHasAnchor = thenHasRecAnchor || thenDynAnchorName;
|
|
53
|
+
const elseHasAnchor = elseHasRecAnchor || elseDynAnchorName;
|
|
54
|
+
|
|
55
|
+
const validateThen = fallbackFn(tmpThen);
|
|
56
|
+
const validateElse = fallbackFn(tmpElse);
|
|
57
|
+
|
|
58
|
+
// With annotation tracking, annotations produced by a failing 'if' are
|
|
59
|
+
// rolled back; annotations from a passing 'if' are kept for unevaluated*.
|
|
60
|
+
if (root.usesUnevaluated && !thenHasAnchor && !elseHasAnchor) {
|
|
61
|
+
return function validateConditionTracked(data, dataPath, dataRoot, dataKey) {
|
|
62
|
+
const log = root.evalLog;
|
|
63
|
+
const mark = log.mark();
|
|
64
|
+
// `if` decides WHICH branch applies; the document was never required to
|
|
65
|
+
// satisfy it, so its complaints are discarded either way. Only `then`
|
|
66
|
+
// and `else` produce errors a caller should see.
|
|
67
|
+
const errors = root.errorMark();
|
|
68
|
+
const taken = validateIf(data, dataPath, dataRoot, dataKey);
|
|
69
|
+
root.rollbackErrors(errors);
|
|
70
|
+
if (taken) return validateThen(data, dataPath, dataRoot, dataKey);
|
|
71
|
+
log.rollback(mark);
|
|
72
|
+
return validateElse(data, dataPath, dataRoot, dataKey);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// If neither then nor else has dynamic anchors, use simple validation
|
|
77
|
+
if (!thenHasAnchor && !elseHasAnchor) {
|
|
78
|
+
// Every call site passes (data, dataPath, dataRoot, dataKey). Declaring
|
|
79
|
+
// fewer parameters does not drop the extra arguments, it MISBINDS them:
|
|
80
|
+
// a two-parameter form named its second parameter `dataRoot` and was
|
|
81
|
+
// handed the dataPath, so the branch validators ran with no root at all
|
|
82
|
+
// and a `$data` reference inside `then` silently resolved to nothing —
|
|
83
|
+
// a wrong verdict rather than an error.
|
|
84
|
+
return function validateCondition(data, dataPath, dataRoot, dataKey) {
|
|
85
|
+
const errors = root.errorMark();
|
|
86
|
+
const taken = validateIf(data, dataPath, dataRoot, dataKey);
|
|
87
|
+
root.rollbackErrors(errors);
|
|
88
|
+
return taken
|
|
89
|
+
? validateThen(data, dataPath, dataRoot, dataKey)
|
|
90
|
+
: validateElse(data, dataPath, dataRoot, dataKey);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// If then or else has dynamic anchors, wrap validation to register them
|
|
95
|
+
const track = root.usesUnevaluated;
|
|
96
|
+
return function validateConditionWithDynamicAnchors(data, dataPath, dataRoot, dataKey) {
|
|
97
|
+
const mark = track ? root.evalLog.mark() : 0;
|
|
98
|
+
const conditionErrors = root.errorMark();
|
|
99
|
+
const taken = validateIf(data, dataPath, dataRoot, dataKey);
|
|
100
|
+
root.rollbackErrors(conditionErrors);
|
|
101
|
+
if (taken) {
|
|
102
|
+
// Validate then branch with dynamic anchor registration
|
|
103
|
+
if (thenHasAnchor && tmpThen) {
|
|
104
|
+
const anchorName = thenDynAnchorName || '';
|
|
105
|
+
root.pushDynamicAnchorValidator(anchorName, tmpThen);
|
|
106
|
+
try {
|
|
107
|
+
return validateThen(data, dataPath, dataRoot, dataKey);
|
|
108
|
+
} finally {
|
|
109
|
+
root.popDynamicAnchorValidator(anchorName);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return validateThen(data, dataPath, dataRoot, dataKey);
|
|
113
|
+
} else {
|
|
114
|
+
// Annotations from the failed 'if' must not leak to unevaluated*
|
|
115
|
+
if (track) root.evalLog.rollback(mark);
|
|
116
|
+
// Validate else branch with dynamic anchor registration
|
|
117
|
+
if (elseHasAnchor && tmpElse) {
|
|
118
|
+
const anchorName = elseDynAnchorName || '';
|
|
119
|
+
root.pushDynamicAnchorValidator(anchorName, tmpElse);
|
|
120
|
+
try {
|
|
121
|
+
return validateElse(data, dataPath, dataRoot, dataKey);
|
|
122
|
+
} finally {
|
|
123
|
+
root.popDynamicAnchorValidator(anchorName);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return validateElse(data, dataPath, dataRoot, dataKey);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
package/src/content.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getStringType,
|
|
3
|
+
isStringType,
|
|
4
|
+
} from '@jarenjs/core';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
isValidBase64,
|
|
8
|
+
} from '@jarenjs/core/text';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
isValidJSON,
|
|
12
|
+
} from '@jarenjs/json';
|
|
13
|
+
|
|
14
|
+
export function compileContentEncoding(schemaObj, jsonSchema) {
|
|
15
|
+
const encoding = getStringType(jsonSchema.contentEncoding);
|
|
16
|
+
if (encoding == null) return undefined;
|
|
17
|
+
|
|
18
|
+
const addError = schemaObj.createErrorHandler(encoding, 'contentEncoding');
|
|
19
|
+
|
|
20
|
+
if (encoding === 'base64') {
|
|
21
|
+
return function validateBase64(data, dataPath) {
|
|
22
|
+
// contentEncoding only applies to strings - ignore non-strings
|
|
23
|
+
if (!isStringType(data)) return true;
|
|
24
|
+
return isValidBase64(data)
|
|
25
|
+
|| addError(data, dataPath);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Unknown encoding - treat as valid (per JSON Schema spec)
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function compileContentMediaType(schemaObj, jsonSchema) {
|
|
34
|
+
const mediaType = getStringType(jsonSchema.contentMediaType);
|
|
35
|
+
if (mediaType == null) return undefined;
|
|
36
|
+
|
|
37
|
+
// Check if contentEncoding is also specified
|
|
38
|
+
const encoding = getStringType(jsonSchema.contentEncoding);
|
|
39
|
+
|
|
40
|
+
const addError = schemaObj.createErrorHandler(mediaType, 'contentMediaType');
|
|
41
|
+
|
|
42
|
+
if (mediaType === 'application/json') {
|
|
43
|
+
if (encoding == null) {
|
|
44
|
+
return function validateJsonContent(data, dataPath) {
|
|
45
|
+
if (!isStringType(data)) return true;
|
|
46
|
+
return isValidJSON(data) || addError(data, dataPath);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
return function validateBase64JsonContent(data, dataPath) {
|
|
51
|
+
if (!isStringType(data)) return true;
|
|
52
|
+
// Check valid base64 first before decoding
|
|
53
|
+
if (!isValidBase64(data)) return addError(data, dataPath);
|
|
54
|
+
const decoded = Buffer.from(data, 'base64').toString('utf8');
|
|
55
|
+
return isValidJSON(decoded) || addError(data, dataPath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Unknown media type - treat as valid (per JSON Schema spec)
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function compileContentSchema(schemaObj, jsonSchema) {
|
|
65
|
+
const encoding = getStringType(jsonSchema.contentEncoding);
|
|
66
|
+
const mediaType = getStringType(jsonSchema.contentMediaType);
|
|
67
|
+
|
|
68
|
+
if (encoding == null && mediaType == null) return undefined;
|
|
69
|
+
|
|
70
|
+
// Per JSON Schema 2019-09/2020-12 spec, contentEncoding and contentMediaType
|
|
71
|
+
// are annotation keywords by default, not assertion keywords.
|
|
72
|
+
// They only validate when contentValidation option is explicitly enabled.
|
|
73
|
+
if (!schemaObj.options.contentValidation) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const contentEncodingValidator = compileContentEncoding(schemaObj, jsonSchema);
|
|
78
|
+
const contentMediaTypeValidator = compileContentMediaType(schemaObj, jsonSchema);
|
|
79
|
+
|
|
80
|
+
if (!contentEncodingValidator && !contentMediaTypeValidator) return undefined;
|
|
81
|
+
|
|
82
|
+
return contentMediaTypeValidator || contentEncodingValidator;
|
|
83
|
+
}
|
package/src/data.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Data keyword implementation for referencing instance data.
|
|
5
|
+
*
|
|
6
|
+
* This implements the "data" keyword from json-everything's data-2022 meta-schema,
|
|
7
|
+
* allowing schema values to be sourced from the instance data at validation time.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* {
|
|
11
|
+
* "type": "object",
|
|
12
|
+
* "properties": {
|
|
13
|
+
* "A": { "type": "number" },
|
|
14
|
+
* "B": {
|
|
15
|
+
* "type": "number",
|
|
16
|
+
* "data": {
|
|
17
|
+
* "minimum": "/A"
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Supports both absolute JSON Pointers (starting with /) and relative JSON Pointers.
|
|
24
|
+
* @see https://docs.json-everything.net/schema/examples/data-ref/
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
isObjectClass,
|
|
29
|
+
isStringType,
|
|
30
|
+
} from '@jarenjs/core';
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
compileDataRef,
|
|
34
|
+
} from '@jarenjs/json';
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
resolveNothing,
|
|
38
|
+
createDataRefCompilers,
|
|
39
|
+
} from './tools.js';
|
|
40
|
+
|
|
41
|
+
function compileRefResolver(ref) {
|
|
42
|
+
try {
|
|
43
|
+
return compileDataRef(ref);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return resolveNothing;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The fifteen keyword validators are shared with the `$data` keyword;
|
|
51
|
+
// only `compileRefResolver` above differs (see tools.js).
|
|
52
|
+
const KEYWORD_COMPILERS = createDataRefCompilers(compileRefResolver);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compile the data keyword schema
|
|
56
|
+
* @param {object} schemaObj - The validation object
|
|
57
|
+
* @param {object} jsonSchema - The JSON schema containing the data keyword
|
|
58
|
+
* @returns {function|undefined} The compiled validator function or undefined
|
|
59
|
+
*/
|
|
60
|
+
export function compileDataSchema(schemaObj, jsonSchema) {
|
|
61
|
+
const dataSchema = jsonSchema.data;
|
|
62
|
+
if (!isObjectClass(dataSchema)) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const validators = [];
|
|
67
|
+
|
|
68
|
+
// Compile each supported keyword within the data object
|
|
69
|
+
for (const [keyword, ref] of Object.entries(dataSchema)) {
|
|
70
|
+
if (!isStringType(ref)) {
|
|
71
|
+
continue; // Skip non-string references
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const compileKeyword = KEYWORD_COMPILERS[keyword];
|
|
75
|
+
if (compileKeyword === undefined) {
|
|
76
|
+
continue; // Unknown keyword in data, skip
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const validator = compileKeyword(schemaObj, ref);
|
|
80
|
+
if (validator) {
|
|
81
|
+
validators.push(validator);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (validators.length === 0) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (validators.length === 1) {
|
|
90
|
+
return validators[0];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return function validateDataSchema(data, dataPath, dataRoot) {
|
|
94
|
+
for (let i = 0; i < validators.length; i++) {
|
|
95
|
+
if (!validators[i](data, dataPath, dataRoot)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
};
|
|
101
|
+
}
|