@jarenjs/validate 0.8.3 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/ARCHITECTURE.md +1067 -0
  2. package/LICENSE +21 -0
  3. package/README.md +339 -2
  4. package/dist/types/array.d.ts +2 -0
  5. package/dist/types/bigint.d.ts +1 -0
  6. package/dist/types/combine.d.ts +1 -0
  7. package/dist/types/condition.d.ts +1 -0
  8. package/dist/types/content.d.ts +3 -0
  9. package/dist/types/data.d.ts +7 -0
  10. package/dist/types/dollar-data.d.ts +20 -0
  11. package/dist/types/dynamic-ref.d.ts +44 -0
  12. package/dist/types/enum.d.ts +1 -0
  13. package/dist/types/format.d.ts +21 -0
  14. package/dist/types/index.d.ts +874 -0
  15. package/dist/types/number.d.ts +1 -0
  16. package/dist/types/object.d.ts +3 -0
  17. package/dist/types/query-keyword.d.ts +19 -0
  18. package/dist/types/query.d.ts +29 -0
  19. package/dist/types/schema.d.ts +1 -0
  20. package/dist/types/string.d.ts +1 -0
  21. package/dist/types/tools.d.ts +51 -0
  22. package/dist/types/traverse.d.ts +32 -0
  23. package/dist/types/unevaluated.d.ts +12 -0
  24. package/package.json +32 -7
  25. package/src/array.js +565 -0
  26. package/src/bigint.js +97 -0
  27. package/src/combine.js +226 -0
  28. package/src/condition.js +109 -0
  29. package/src/content.js +83 -0
  30. package/src/data.js +477 -0
  31. package/src/dollar-data.js +629 -0
  32. package/src/dynamic-ref.js +121 -0
  33. package/src/enum.js +148 -0
  34. package/src/format.js +66 -0
  35. package/src/index.js +1854 -0
  36. package/src/number.js +159 -0
  37. package/src/object.js +755 -0
  38. package/src/query-keyword.js +99 -0
  39. package/src/query.js +59 -0
  40. package/src/schema.js +645 -0
  41. package/src/string.js +152 -0
  42. package/src/tools.js +205 -0
  43. package/src/traverse.js +433 -0
  44. package/src/unevaluated.js +151 -0
  45. package/dist/index.js +0 -1802
  46. package/dist/index.js.map +0 -7
  47. package/dist/index.min.js +0 -2
  48. package/dist/index.min.js.map +0 -7
package/src/combine.js ADDED
@@ -0,0 +1,226 @@
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
+ getBoolOrObjectClass,
14
+ getArrayClassMinItems,
15
+ } from './tools.js';
16
+
17
+ function compileAllOf(schemaObj, jsonSchema) {
18
+ const allOf = getArrayClassMinItems(jsonSchema.allOf, 1);
19
+ if (allOf == null) return undefined;
20
+
21
+ const validators = [];
22
+ for (let i = 0; i < allOf.length; ++i) {
23
+ const child = allOf[i];
24
+ if (child === true)
25
+ validators.push(trueThat);
26
+ else if (child === false)
27
+ validators.push(falseThat);
28
+ else {
29
+ const validator = schemaObj.createValidator(child, 'allOf', i);
30
+ validators.push(validator);
31
+ }
32
+ }
33
+ if (validators.length === 0) return undefined;
34
+
35
+ const addError = schemaObj.createErrorHandler(allOf, 'allOf');
36
+
37
+ return function validateAllOf(data, dataPath, dataRoot, dataKey) {
38
+ if (data !== undefined) {
39
+ for (let i = 0; i < validators.length; ++i) {
40
+ const validator = validators[i];
41
+ if (validator(data, dataPath, dataRoot, dataKey) === false) {
42
+ return addError(data, dataPath);
43
+ }
44
+ }
45
+ }
46
+ return true;
47
+ };
48
+ }
49
+
50
+ function compileAnyOf(schemaObj, jsonSchema) {
51
+ const anyOf = getArrayClassMinItems(jsonSchema.anyOf, 1);
52
+ if (anyOf == null) return undefined;
53
+
54
+ const validators = [];
55
+ for (let i = 0; i < anyOf.length; ++i) {
56
+ const child = anyOf[i];
57
+ if (child === true)
58
+ validators.push(trueThat);
59
+ else if (child === false)
60
+ validators.push(falseThat);
61
+ else {
62
+ const validator = schemaObj.createValidator(child, 'anyOf', i);
63
+ validators.push(validator);
64
+ }
65
+ }
66
+ if (validators.length === 0) return undefined;
67
+
68
+ const addError = schemaObj.createErrorHandler(anyOf, 'anyOf');
69
+
70
+ // With annotation tracking every branch must run (annotations from ALL
71
+ // successful branches count for unevaluated*), and annotations from
72
+ // failed branches are rolled back.
73
+ const root = schemaObj.root;
74
+ if (root.usesUnevaluated) {
75
+ return function validateAnyOfTracked(data, dataPath, dataRoot, dataKey) {
76
+ if (data === undefined) return true;
77
+ const log = root.evalLog;
78
+ let found = false;
79
+ for (let i = 0; i < validators.length; ++i) {
80
+ const mark = log.mark();
81
+ if (validators[i](data, dataPath, dataRoot, dataKey) === true)
82
+ found = true;
83
+ else
84
+ log.rollback(mark);
85
+ }
86
+ return found || addError(data, dataPath);
87
+ };
88
+ }
89
+
90
+ return function validateAnyOf(data, dataPath, dataRoot, dataKey) {
91
+ if (data !== undefined) {
92
+ for (let i = 0; i < validators.length; ++i) {
93
+ const validator = validators[i]; // TODO: how to silence errors of children?
94
+ if (validator(data, dataPath, dataRoot, dataKey) === true) return true;
95
+ }
96
+ return addError(data, dataPath);
97
+ }
98
+ return true;
99
+ };
100
+ }
101
+
102
+ function compileOneOf(schemaObj, jsonSchema) {
103
+ const oneOf = getArrayClassMinItems(jsonSchema.oneOf, 1);
104
+ if (oneOf == null)
105
+ return undefined;
106
+
107
+ const validators = [];
108
+ for (let i = 0; i < oneOf.length; ++i) {
109
+ const child = oneOf[i];
110
+ if (child === true)
111
+ validators.push(trueThat);
112
+ else if (child === false)
113
+ validators.push(falseThat);
114
+ else {
115
+ const validator = schemaObj.createValidator(child, 'oneOf', i);
116
+ validators.push(validator);
117
+ }
118
+ }
119
+ if (validators.length === 0)
120
+ return undefined;
121
+
122
+ const addError = schemaObj.createErrorHandler(oneOf, 'oneOf');
123
+
124
+ // With annotation tracking, annotations from failed branches are rolled back.
125
+ const root = schemaObj.root;
126
+ if (root.usesUnevaluated) {
127
+ return function validateOneOfTracked(data, dataPath, dataRoot, dataKey) {
128
+ const log = root.evalLog;
129
+ let found = false;
130
+ for (let i = 0; i < validators.length; ++i) {
131
+ const mark = log.mark();
132
+ if (validators[i](data, dataPath, dataRoot, dataKey) === true) {
133
+ if (found === true)
134
+ return addError(data, dataPath);
135
+ found = true;
136
+ }
137
+ else {
138
+ log.rollback(mark);
139
+ }
140
+ }
141
+ return found || addError(data, dataPath);
142
+ };
143
+ }
144
+
145
+ return function validateOneOf(data, dataPath, dataRoot, dataKey) {
146
+ let found = false;
147
+ for (let i = 0; i < validators.length; ++i) {
148
+ const validator = validators[i];
149
+ if (validator(data, dataPath, dataRoot, dataKey) === true) {
150
+ if (found === true)
151
+ return addError(data, dataPath);
152
+ found = true;
153
+ }
154
+ }
155
+ return found || addError(data, dataPath);
156
+ };
157
+ }
158
+
159
+ function compileNotOf(schemaObj, jsonSchema) {
160
+ const notOf = getBoolOrObjectClass(jsonSchema.not);
161
+ if (notOf == null) return undefined;
162
+ if (notOf === true) return falseThat;
163
+ if (notOf === false) return trueThat;
164
+
165
+ const validate = schemaObj.createValidator(notOf, 'not');
166
+
167
+ const addError = schemaObj.createErrorHandler(notOf, 'not');
168
+
169
+ // Annotations produced inside a 'not' are never kept, whatever the outcome.
170
+ const root = schemaObj.root;
171
+ if (root.usesUnevaluated) {
172
+ return function validateNotOfTracked(data, dataPath, dataRoot, dataKey) {
173
+ if (data === undefined) return true;
174
+ const log = root.evalLog;
175
+ const mark = log.mark();
176
+ const valid = validate(data, dataPath, dataRoot, dataKey);
177
+ log.rollback(mark);
178
+ return valid === false
179
+ ? true
180
+ : addError(data, dataPath);
181
+ };
182
+ }
183
+
184
+ return function validateNotOf(data, dataPath, dataRoot, dataKey) {
185
+ if (data === undefined) return true;
186
+ return validate(data, dataPath, dataRoot, dataKey) === false
187
+ ? true
188
+ : addError(data, dataPath);
189
+ };
190
+ }
191
+
192
+ export function compileCombineSchema(schemaObj, jsonSchema) {
193
+ const validators = [];
194
+ function addValidator(compiler) {
195
+ if (isFn(compiler))
196
+ validators.push(compiler);
197
+ }
198
+
199
+ addValidator(compileAllOf(schemaObj, jsonSchema));
200
+ addValidator(compileAnyOf(schemaObj, jsonSchema));
201
+ addValidator(compileOneOf(schemaObj, jsonSchema));
202
+ addValidator(compileNotOf(schemaObj, jsonSchema));
203
+
204
+ if (validators.length === 0)
205
+ return undefined;
206
+ if (validators.length === 1)
207
+ return validators[0];
208
+ if (validators.length === 2) {
209
+ const first = validators[0];
210
+ const second = validators[1];
211
+ return function validateCombinaSchemaPair(data, dataPath, dataRoot, dataKey) {
212
+ return first(data, dataPath, dataRoot, dataKey)
213
+ && second(data, dataPath, dataRoot, dataKey);
214
+ };
215
+ }
216
+ else {
217
+ return function validateCombineSchema(data, dataPath, dataRoot, dataKey) {
218
+ for (let i = 0; i < validators.length; ++i) {
219
+ const validator = validators[i];
220
+ if (validator(data, dataPath, dataRoot, dataKey) === false)
221
+ return false;
222
+ }
223
+ return true;
224
+ };
225
+ }
226
+ }
@@ -0,0 +1,109 @@
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
+ if (validateIf(data, dataPath, dataRoot, dataKey) === false)
31
+ log.rollback(mark);
32
+ return true;
33
+ };
34
+ }
35
+ return undefined;
36
+ }
37
+
38
+ // Check if then/else schemas have dynamic anchors
39
+ const thenSchema = jsonSchema.then;
40
+ const elseSchema = jsonSchema.else;
41
+
42
+ const thenHasRecAnchor = isObjectClass(thenSchema) && hasRecursiveAnchor(thenSchema);
43
+ const thenDynAnchorName = isObjectClass(thenSchema) ? getDynamicAnchorName(thenSchema) : null;
44
+ const elseHasRecAnchor = isObjectClass(elseSchema) && hasRecursiveAnchor(elseSchema);
45
+ const elseDynAnchorName = isObjectClass(elseSchema) ? getDynamicAnchorName(elseSchema) : null;
46
+
47
+ const thenHasAnchor = thenHasRecAnchor || thenDynAnchorName;
48
+ const elseHasAnchor = elseHasRecAnchor || elseDynAnchorName;
49
+
50
+ const validateThen = fallbackFn(tmpThen);
51
+ const validateElse = fallbackFn(tmpElse);
52
+
53
+ // With annotation tracking, annotations produced by a failing 'if' are
54
+ // rolled back; annotations from a passing 'if' are kept for unevaluated*.
55
+ if (root.usesUnevaluated && !thenHasAnchor && !elseHasAnchor) {
56
+ return function validateConditionTracked(data, dataPath, dataRoot, dataKey) {
57
+ const log = root.evalLog;
58
+ const mark = log.mark();
59
+ if (validateIf(data, dataPath, dataRoot, dataKey)) {
60
+ return validateThen(data, dataPath, dataRoot, dataKey);
61
+ }
62
+ log.rollback(mark);
63
+ return validateElse(data, dataPath, dataRoot, dataKey);
64
+ };
65
+ }
66
+
67
+ // If neither then nor else has dynamic anchors, use simple validation
68
+ if (!thenHasAnchor && !elseHasAnchor) {
69
+ return function validateCondition(data, dataRoot) {
70
+ if (validateIf(data))
71
+ return validateThen(data, dataRoot);
72
+ else
73
+ return validateElse(data, dataRoot);
74
+ };
75
+ }
76
+
77
+ // If then or else has dynamic anchors, wrap validation to register them
78
+ const track = root.usesUnevaluated;
79
+ return function validateConditionWithDynamicAnchors(data, dataPath, dataRoot) {
80
+ const mark = track ? root.evalLog.mark() : 0;
81
+ if (validateIf(data)) {
82
+ // Validate then branch with dynamic anchor registration
83
+ if (thenHasAnchor && tmpThen) {
84
+ const anchorName = thenDynAnchorName || '';
85
+ root.pushDynamicAnchorValidator(anchorName, tmpThen);
86
+ try {
87
+ return validateThen(data, dataRoot);
88
+ } finally {
89
+ root.popDynamicAnchorValidator(anchorName);
90
+ }
91
+ }
92
+ return validateThen(data, dataRoot);
93
+ } else {
94
+ // Annotations from the failed 'if' must not leak to unevaluated*
95
+ if (track) root.evalLog.rollback(mark);
96
+ // Validate else branch with dynamic anchor registration
97
+ if (elseHasAnchor && tmpElse) {
98
+ const anchorName = elseDynAnchorName || '';
99
+ root.pushDynamicAnchorValidator(anchorName, tmpElse);
100
+ try {
101
+ return validateElse(data, dataRoot);
102
+ } finally {
103
+ root.popDynamicAnchorValidator(anchorName);
104
+ }
105
+ }
106
+ return validateElse(data, dataRoot);
107
+ }
108
+ };
109
+ }
package/src/content.js ADDED
@@ -0,0 +1,83 @@
1
+ import {
2
+ getStringType,
3
+ isStringType,
4
+ } from '@jarenjs/core';
5
+
6
+ import {
7
+ isValidBase64Fast,
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 isValidBase64Fast(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 (!isValidBase64Fast(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
+ }