@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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Ajv $data keyword implementation for referencing instance data.
|
|
5
|
+
*
|
|
6
|
+
* This implements the Ajv-style "$data" keyword, allowing constraint keyword values
|
|
7
|
+
* to be sourced from instance data at validation time using Relative JSON Pointers.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* {
|
|
11
|
+
* "type": "object",
|
|
12
|
+
* "properties": {
|
|
13
|
+
* "smaller": {
|
|
14
|
+
* "type": "number",
|
|
15
|
+
* "maximum": { "$data": "1/larger" }
|
|
16
|
+
* },
|
|
17
|
+
* "larger": {
|
|
18
|
+
* "type": "number"
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* The "$data" value is a Relative JSON Pointer that resolves from the current data location.
|
|
24
|
+
* Format: <non-negative-integer>("#" | <json-pointer>)
|
|
25
|
+
* - "0" - The current value itself
|
|
26
|
+
* - "0#" - The property name/index of the current value
|
|
27
|
+
* - "0/foo" - The "foo" property of the current value
|
|
28
|
+
* - "1" - The parent value
|
|
29
|
+
* - "1/foo" - The "foo" property of the parent value
|
|
30
|
+
* - "2/bar" - Go up 2 levels, then look for "bar"
|
|
31
|
+
*
|
|
32
|
+
* @see https://github.com/ajv-validator/ajv/tree/master/spec/extras/%24data
|
|
33
|
+
* @see https://datatracker.ietf.org/doc/html/draft-luff-relative-json-pointer-00
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
isObjectClass,
|
|
38
|
+
isStringType,
|
|
39
|
+
isObjectType,
|
|
40
|
+
} from '@jarenjs/core';
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
equalsDeep,
|
|
44
|
+
} from '@jarenjs/core/object';
|
|
45
|
+
|
|
46
|
+
import {
|
|
47
|
+
NUMERIC_CONSTRAINTS, STRING_CONSTRAINTS,
|
|
48
|
+
ARRAY_CONSTRAINTS, OBJECT_CONSTRAINTS,
|
|
49
|
+
} from '@jarenjs/core/schema';
|
|
50
|
+
|
|
51
|
+
import {
|
|
52
|
+
compileDataRef,
|
|
53
|
+
JSONPOINTER_NOTHING,
|
|
54
|
+
} from '@jarenjs/json';
|
|
55
|
+
|
|
56
|
+
import {
|
|
57
|
+
createDataRefCompilers,
|
|
58
|
+
} from './tools.js';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a `$data` reference.
|
|
62
|
+
*
|
|
63
|
+
* Ajv's `$data` takes the same three forms the `data` keyword does — empty for
|
|
64
|
+
* the root, a Relative JSON Pointer, or an absolute JSON Pointer — and this
|
|
65
|
+
* used to compile only the relative form. An absolute `$data: '/limit'` threw,
|
|
66
|
+
* was swallowed by the catch, and became `resolveNothing`: the constraint
|
|
67
|
+
* silently never asserted, so a document that should have failed passed. A
|
|
68
|
+
* disabled constraint is worse than a rejected schema, which is why an
|
|
69
|
+
* uncompilable reference is now a compile-time error.
|
|
70
|
+
* @param {string} ref
|
|
71
|
+
* @returns {(dataRoot: any, dataPath: string) => any}
|
|
72
|
+
*/
|
|
73
|
+
function compileRefResolver(ref) {
|
|
74
|
+
return compileDataRef(ref);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if a value is a $data reference object
|
|
79
|
+
* @param {*} value - The value to check
|
|
80
|
+
* @returns {boolean} True if the value is a $data reference object
|
|
81
|
+
*/
|
|
82
|
+
function isDollarDataRef(value) {
|
|
83
|
+
return isObjectClass(value) && isStringType(value.$data);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The fifteen keyword validators are shared with the `data` keyword;
|
|
87
|
+
// only `compileRefResolver` above differs (see tools.js). uniqueItems
|
|
88
|
+
// and required below are $data-only and stay local.
|
|
89
|
+
const KEYWORD_COMPILERS = createDataRefCompilers(compileRefResolver);
|
|
90
|
+
|
|
91
|
+
//#region $data-only Validators
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Compile $data reference for uniqueItems constraint
|
|
95
|
+
* @param {object} schemaObj - The validation object
|
|
96
|
+
* @param {string} ref - The $data reference (relative JSON Pointer)
|
|
97
|
+
* @returns {function|undefined} The compiled validator function
|
|
98
|
+
*/
|
|
99
|
+
function compileDollarDataUniqueItems(schemaObj, ref) {
|
|
100
|
+
const addError = schemaObj.createErrorHandler(ref, 'uniqueItems');
|
|
101
|
+
const resolveRef = compileRefResolver(ref);
|
|
102
|
+
|
|
103
|
+
return function validateDollarDataUniqueItems(data, dataPath, dataRoot) {
|
|
104
|
+
if (!Array.isArray(data)) return true;
|
|
105
|
+
|
|
106
|
+
const shouldBeUnique = resolveRef(dataRoot, dataPath);
|
|
107
|
+
if (shouldBeUnique === JSONPOINTER_NOTHING || !shouldBeUnique) return true;
|
|
108
|
+
|
|
109
|
+
// Check for duplicates using deep equality
|
|
110
|
+
for (let i = 0; i < data.length; i++) {
|
|
111
|
+
for (let j = i + 1; j < data.length; j++) {
|
|
112
|
+
if (equalsDeep(data[i], data[j])) {
|
|
113
|
+
return addError(data, dataPath);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Compile $data reference for required constraint
|
|
123
|
+
* @param {object} schemaObj - The validation object
|
|
124
|
+
* @param {string} ref - The $data reference (relative JSON Pointer)
|
|
125
|
+
* @returns {function|undefined} The compiled validator function
|
|
126
|
+
*/
|
|
127
|
+
function compileDollarDataRequired(schemaObj, ref) {
|
|
128
|
+
// Keyed handler: the missing property name travels as the dataKey, so
|
|
129
|
+
// error conversion extracts params.missingProperty like static required.
|
|
130
|
+
const addError = schemaObj.createErrorHandler(ref, ['required']);
|
|
131
|
+
const resolveRef = compileRefResolver(ref);
|
|
132
|
+
|
|
133
|
+
return function validateDollarDataRequired(data, dataPath, dataRoot) {
|
|
134
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) return true;
|
|
135
|
+
|
|
136
|
+
const requiredProps = resolveRef(dataRoot, dataPath);
|
|
137
|
+
if (requiredProps === JSONPOINTER_NOTHING || !Array.isArray(requiredProps)) return true;
|
|
138
|
+
|
|
139
|
+
for (const prop of requiredProps) {
|
|
140
|
+
if (!(prop in data)) {
|
|
141
|
+
return addError(prop, data, dataPath);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
|
|
150
|
+
//#region Main Compilation
|
|
151
|
+
|
|
152
|
+
/** Application order of the $data-capable keywords (error order is part
|
|
153
|
+
* of the observable behavior; the shared groups' internal order IS the
|
|
154
|
+
* historical per-keyword dispatch sequence — `@jarenjs/core/schema`
|
|
155
|
+
* documents that order as contract). */
|
|
156
|
+
const DOLLAR_KEYWORD_ORDER = [
|
|
157
|
+
...NUMERIC_CONSTRAINTS,
|
|
158
|
+
...STRING_CONSTRAINTS,
|
|
159
|
+
...ARRAY_CONSTRAINTS,
|
|
160
|
+
...OBJECT_CONSTRAINTS, 'required',
|
|
161
|
+
'enum', 'const',
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const DOLLAR_ONLY_COMPILERS = {
|
|
165
|
+
__proto__: null,
|
|
166
|
+
uniqueItems: compileDollarDataUniqueItems,
|
|
167
|
+
required: compileDollarDataRequired,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Compile $data keyword validators for a schema object
|
|
172
|
+
* This detects when keyword values are { $data: "..." } objects and creates
|
|
173
|
+
* dynamic validators that resolve the reference at validation time.
|
|
174
|
+
*
|
|
175
|
+
* @param {object} schemaObj - The validation object
|
|
176
|
+
* @param {object} jsonSchema - The JSON schema to compile
|
|
177
|
+
* @returns {function|undefined} The compiled validator function or undefined
|
|
178
|
+
*/
|
|
179
|
+
export function compileDollarDataSchema(schemaObj, jsonSchema) {
|
|
180
|
+
const validators = [];
|
|
181
|
+
|
|
182
|
+
for (const keyword of DOLLAR_KEYWORD_ORDER) {
|
|
183
|
+
const value = jsonSchema[keyword];
|
|
184
|
+
if (!isDollarDataRef(value)) continue;
|
|
185
|
+
|
|
186
|
+
const compileKeyword = KEYWORD_COMPILERS[keyword] ?? DOLLAR_ONLY_COMPILERS[keyword];
|
|
187
|
+
const validator = compileKeyword(schemaObj, value.$data);
|
|
188
|
+
if (validator) validators.push(validator);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (validators.length === 0) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (validators.length === 1) {
|
|
196
|
+
return validators[0];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return function validateDollarDataSchema(data, dataPath, dataRoot) {
|
|
200
|
+
for (let i = 0; i < validators.length; i++) {
|
|
201
|
+
if (!validators[i](data, dataPath, dataRoot)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function isDollarDataReference(jsonSchema) {
|
|
210
|
+
return (jsonSchema !== null && (isObjectType(jsonSchema) && jsonSchema.$data));
|
|
211
|
+
}
|
|
212
|
+
//#endregion
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dynamic Reference Resolution Module
|
|
5
|
+
*
|
|
6
|
+
* This module handles $recursiveRef (draft 2019-09) and $dynamicRef (draft 2020-12)
|
|
7
|
+
* which require runtime resolution based on dynamic scope.
|
|
8
|
+
*
|
|
9
|
+
* Key concepts:
|
|
10
|
+
* - $recursiveRef: References the nearest parent schema with $recursiveAnchor: true
|
|
11
|
+
* - $dynamicRef: References the nearest parent schema with matching $dynamicAnchor
|
|
12
|
+
*
|
|
13
|
+
* Unlike regular $ref, these require runtime resolution because the target depends
|
|
14
|
+
* on the dynamic context where the schema is used.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { isObjectClass, isStringType } from '@jarenjs/core';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if a schema has $recursiveAnchor: true.
|
|
21
|
+
* @param {object} schema - The schema object
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
export function hasRecursiveAnchor(schema) {
|
|
25
|
+
return isObjectClass(schema) && schema.$recursiveAnchor === true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the $dynamicAnchor name from a schema.
|
|
30
|
+
* @param {object} schema - The schema object
|
|
31
|
+
* @returns {string|null}
|
|
32
|
+
*/
|
|
33
|
+
export function getDynamicAnchorName(schema) {
|
|
34
|
+
if (!isObjectClass(schema)) return null;
|
|
35
|
+
return isStringType(schema.$dynamicAnchor) ? schema.$dynamicAnchor : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Collect ALL dynamic anchors of a schema RESOURCE: every $dynamicAnchor
|
|
40
|
+
* reachable from the given schema without crossing into an embedded
|
|
41
|
+
* resource (a subschema that declares its own $id).
|
|
42
|
+
*
|
|
43
|
+
* Per draft 2020-12, entering a schema resource during evaluation brings
|
|
44
|
+
* every $dynamicAnchor of that resource into the dynamic scope - wherever
|
|
45
|
+
* it sits ($defs, allOf branches, properties, ...), not just at the root.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} schema - The resource root schema object
|
|
48
|
+
* @returns {Array<{name: string, schema: object, validator: (function|null)}>}
|
|
49
|
+
*/
|
|
50
|
+
export function collectDynamicAnchorsDeep(schema) {
|
|
51
|
+
const anchors = [];
|
|
52
|
+
if (!isObjectClass(schema)) return anchors;
|
|
53
|
+
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
const queue = [{ node: schema, isRoot: true }];
|
|
56
|
+
while (queue.length > 0) {
|
|
57
|
+
const { node, isRoot } = queue.shift();
|
|
58
|
+
if (!isObjectClass(node) && !Array.isArray(node)) continue;
|
|
59
|
+
if (seen.has(node)) continue;
|
|
60
|
+
seen.add(node);
|
|
61
|
+
|
|
62
|
+
if (Array.isArray(node)) {
|
|
63
|
+
for (let i = 0; i < node.length; ++i) {
|
|
64
|
+
queue.push({ node: node[i], isRoot: false });
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// A nested $id starts a new (embedded) resource - its anchors enter
|
|
70
|
+
// the dynamic scope only when that resource itself is entered.
|
|
71
|
+
if (!isRoot && isStringType(node.$id)) continue;
|
|
72
|
+
|
|
73
|
+
const anchorName = getDynamicAnchorName(node);
|
|
74
|
+
if (anchorName) {
|
|
75
|
+
anchors.push({ name: anchorName, schema: node, validator: null });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const key of Object.keys(node)) {
|
|
79
|
+
queue.push({ node: node[key], isRoot: false });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return anchors;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Collect all dynamic anchors from a schema's immediate definitions ($defs/definitions).
|
|
88
|
+
* This is used to find all $dynamicAnchor definitions that should be in scope
|
|
89
|
+
* when following a $ref from this schema.
|
|
90
|
+
*
|
|
91
|
+
* IMPORTANT: This only collects from the IMMEDIATE $defs of the given schema,
|
|
92
|
+
* not recursively.
|
|
93
|
+
*
|
|
94
|
+
* @param {object} schema - The schema object
|
|
95
|
+
* @returns {Array<{name: string, schema: object}>} Array of {name, schema} objects
|
|
96
|
+
*/
|
|
97
|
+
export function collectDynamicAnchors(schema) {
|
|
98
|
+
const anchors = [];
|
|
99
|
+
if (!isObjectClass(schema)) return anchors;
|
|
100
|
+
|
|
101
|
+
// Check $defs first (draft 2020-12), then definitions (draft 7 and earlier)
|
|
102
|
+
const defs = schema.$defs || schema.definitions;
|
|
103
|
+
if (isObjectClass(defs)) {
|
|
104
|
+
for (const key of Object.keys(defs)) {
|
|
105
|
+
const def = defs[key];
|
|
106
|
+
if (isObjectClass(def)) {
|
|
107
|
+
// Check if this definition has a $dynamicAnchor
|
|
108
|
+
const anchorName = getDynamicAnchorName(def);
|
|
109
|
+
if (anchorName) {
|
|
110
|
+
anchors.push({ name: anchorName, schema: def, key });
|
|
111
|
+
}
|
|
112
|
+
// Note: We do NOT recurse into nested $defs here.
|
|
113
|
+
// Dynamic anchors from nested $defs of a $ref target should NOT be
|
|
114
|
+
// automatically in scope - they should only be registered when that
|
|
115
|
+
// schema is actually evaluated.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return anchors;
|
|
121
|
+
}
|
package/src/enum.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isScalarType,
|
|
5
|
+
} from '@jarenjs/core';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
equalsDeep,
|
|
9
|
+
} from '@jarenjs/core/object';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
getArrayClassMinItems,
|
|
13
|
+
} from './tools.js';
|
|
14
|
+
import { isDollarDataReference } from './dollar-data.js';
|
|
15
|
+
|
|
16
|
+
// export const exampleEnumDataStructure = {
|
|
17
|
+
// allOf: [
|
|
18
|
+
// {
|
|
19
|
+
// enum: {
|
|
20
|
+
// source: false, // jsonpath!!
|
|
21
|
+
// data: [
|
|
22
|
+
// { color: 0xFFFFFF, name: 'white', type: 'greys' },
|
|
23
|
+
// { color: 0x000000, name: 'black', type: 'greys' },
|
|
24
|
+
// null, // this null will not be included in resulting enum array.
|
|
25
|
+
// { color: 0xFF0000, name: 'red', type: 'reds' },
|
|
26
|
+
// { color: 0x00FF00, name: 'green', type: 'greens' },
|
|
27
|
+
// { color: 0x0000FF, name: 'blue', type: 'blues' },
|
|
28
|
+
// ],
|
|
29
|
+
// group: 'type',
|
|
30
|
+
// label: 'name',
|
|
31
|
+
// value: 'color',
|
|
32
|
+
// },
|
|
33
|
+
// },
|
|
34
|
+
// {
|
|
35
|
+
// enum: {
|
|
36
|
+
// source: false, // jsonpath!!
|
|
37
|
+
// data: [
|
|
38
|
+
// [0xFFFFFF, 'white', 'greys'],
|
|
39
|
+
// [0x000000, 'black', 'greys'],
|
|
40
|
+
// null, // null will be ignored
|
|
41
|
+
// [0xFF0000, 'red', 'reds'],
|
|
42
|
+
// [0x00FF00, 'green', 'greens'],
|
|
43
|
+
// [0x0000FF, 'blue', 'blues'],
|
|
44
|
+
// ],
|
|
45
|
+
// group: 2,
|
|
46
|
+
// label: 1,
|
|
47
|
+
// value: 0,
|
|
48
|
+
// },
|
|
49
|
+
// },
|
|
50
|
+
// {
|
|
51
|
+
// enum: {
|
|
52
|
+
// data: [
|
|
53
|
+
// { value: 0xFFFFFF, label: 'white', group: 'greys' },
|
|
54
|
+
// { value: 0x000000, label: 'black', group: 'greys' },
|
|
55
|
+
// null, // null will be ignored
|
|
56
|
+
// { value: 0xFF0000, label: 'red', group: 'reds' },
|
|
57
|
+
// { value: 0x00FF00, label: 'green', group: 'greens' },
|
|
58
|
+
// { value: 0x0000FF, label: 'blue', group: 'blues' },
|
|
59
|
+
// ],
|
|
60
|
+
// },
|
|
61
|
+
// },
|
|
62
|
+
// {
|
|
63
|
+
// enum: {
|
|
64
|
+
// data: [
|
|
65
|
+
// [0xFFFFFF, 'white', 'greys'],
|
|
66
|
+
// [0x000000, 'black', 'greys'],
|
|
67
|
+
// null, // null will be ignored
|
|
68
|
+
// [0xFF0000, 'red', 'reds'],
|
|
69
|
+
// [0x00FF00, 'green', 'greens'],
|
|
70
|
+
// [0x0000FF, 'blue', 'blues'],
|
|
71
|
+
// ],
|
|
72
|
+
// },
|
|
73
|
+
// },
|
|
74
|
+
// ],
|
|
75
|
+
// };
|
|
76
|
+
|
|
77
|
+
function compileConst(schemaObj, jsonSchema) {
|
|
78
|
+
const constant = jsonSchema.const;
|
|
79
|
+
if (constant === undefined)
|
|
80
|
+
return undefined;
|
|
81
|
+
|
|
82
|
+
if (isDollarDataReference(constant))
|
|
83
|
+
return undefined;
|
|
84
|
+
|
|
85
|
+
const addError = schemaObj.createErrorHandler(constant, 'const');
|
|
86
|
+
|
|
87
|
+
if (constant === null || isScalarType(constant)) {
|
|
88
|
+
return function validatePrimitiveConst(data, dataPath) {
|
|
89
|
+
return constant === data
|
|
90
|
+
|| addError(data, dataPath);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
return function validateComplexConst(data, dataPath) {
|
|
95
|
+
return equalsDeep(constant, data)
|
|
96
|
+
|| addError(data, dataPath);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function compileEnum(schemaObj, jsonSchema) {
|
|
102
|
+
const enums = getArrayClassMinItems(jsonSchema.enum, 1);
|
|
103
|
+
if (enums == null) return undefined;
|
|
104
|
+
|
|
105
|
+
let hasObjects = false;
|
|
106
|
+
for (let i = 0; i < enums.length; ++i) {
|
|
107
|
+
const e = enums[i];
|
|
108
|
+
if (e != null && typeof e === 'object') {
|
|
109
|
+
hasObjects = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const addError = schemaObj.createErrorHandler(enums, 'enum');
|
|
115
|
+
|
|
116
|
+
if (hasObjects === false) {
|
|
117
|
+
return function validateEnumSimple(data, dataPath) {
|
|
118
|
+
return data === undefined
|
|
119
|
+
? true
|
|
120
|
+
: enums.includes(data)
|
|
121
|
+
? true
|
|
122
|
+
: addError(data, dataPath);
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
return function validateEnumDeep(data, dataPath) {
|
|
127
|
+
if (data === undefined) return true;
|
|
128
|
+
if (data === null || typeof data !== 'object')
|
|
129
|
+
return enums.includes(data)
|
|
130
|
+
? true
|
|
131
|
+
: addError(data, dataPath);
|
|
132
|
+
|
|
133
|
+
for (let i = 0; i < enums.length; ++i) {
|
|
134
|
+
if (equalsDeep(enums[i], data) === true)
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return addError(data, dataPath);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function compileEnumBasic(schemaObj, jsonSchema) {
|
|
143
|
+
return [
|
|
144
|
+
compileConst(schemaObj, jsonSchema),
|
|
145
|
+
compileEnum(schemaObj, jsonSchema),
|
|
146
|
+
];
|
|
147
|
+
}
|
package/src/format.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isStringType,
|
|
3
|
+
isFn,
|
|
4
|
+
} from '@jarenjs/core';
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('./index.js').FormatCompiler} FormatCompiler */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Registers a single format compiler under a name.
|
|
10
|
+
* Existing registrations are never overwritten.
|
|
11
|
+
* @param {Record<string, FormatCompiler>} registered - The formats registry object
|
|
12
|
+
* @param {string} name - The format name (e.g. 'email', 'uri', 'date-time')
|
|
13
|
+
* @param {FormatCompiler} formatCompiler - The compiler to register
|
|
14
|
+
* @returns {boolean} True when the compiler was registered
|
|
15
|
+
*/
|
|
16
|
+
export function registerFormatCompiler(registered, name, formatCompiler) {
|
|
17
|
+
if (registered[name] == null) {
|
|
18
|
+
if (isFn(formatCompiler)) {
|
|
19
|
+
registered[name] = formatCompiler;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Registers multiple format compilers at once.
|
|
28
|
+
* Existing registrations are never overwritten.
|
|
29
|
+
* @param {Record<string, FormatCompiler>} registered - The formats registry object
|
|
30
|
+
* @param {Record<string, FormatCompiler>} formatCompilers - Object mapping format names to compiler functions
|
|
31
|
+
* @returns {Record<string, FormatCompiler>} The registry object passed in
|
|
32
|
+
*/
|
|
33
|
+
export function registerFormatCompilers(registered, formatCompilers) {
|
|
34
|
+
const keys = Object.keys(formatCompilers);
|
|
35
|
+
for (let i = 0; i < keys.length; ++i) {
|
|
36
|
+
const key = keys[i];
|
|
37
|
+
const item = formatCompilers[key];
|
|
38
|
+
registerFormatCompiler(registered, key, item);
|
|
39
|
+
}
|
|
40
|
+
return registered;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Named once: both wrong-shape paths end at the same mistake. */
|
|
44
|
+
const WRONG_SHAPE_HINT = 'Register the format COMPILERS (stringFormats, dateTimeFormats, '
|
|
45
|
+
+ 'numberFormats, jsonFormats, geoFormats) rather than the raw testers (formatTesters) — '
|
|
46
|
+
+ 'both are objects full of functions, and only the compilers take (schemaObj, jsonSchema).';
|
|
47
|
+
|
|
48
|
+
export function getSchemaFormatCompiler(registered, name) {
|
|
49
|
+
if (isStringType(name))
|
|
50
|
+
return registered[name];
|
|
51
|
+
else
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function compileFormatBasic(schemaObj, jsonSchema) {
|
|
56
|
+
if (!isStringType(jsonSchema.format))
|
|
57
|
+
return undefined;
|
|
58
|
+
|
|
59
|
+
// From draft 2020-12 on, format is annotation-only unless assertion is
|
|
60
|
+
// enabled (formatAssertion option or format-assertion vocabulary).
|
|
61
|
+
// Nothing is lost by an unregistered name here — the keyword would not
|
|
62
|
+
// have asserted even with a compiler — so the unknownFormats check
|
|
63
|
+
// below deliberately does not run in this branch. Complaining here
|
|
64
|
+
// would be complaining about the spec.
|
|
65
|
+
if (schemaObj.options.formatAssertion === false)
|
|
66
|
+
return undefined;
|
|
67
|
+
const compiler = getSchemaFormatCompiler(
|
|
68
|
+
schemaObj.formats,
|
|
69
|
+
jsonSchema.format);
|
|
70
|
+
|
|
71
|
+
// An unregistered name with assertion ON is the silent failure this
|
|
72
|
+
// check exists to end: the author asked for the value to be checked,
|
|
73
|
+
// the registry has nothing to check it with, and without this the
|
|
74
|
+
// schema compiles to a validator that accepts everything. Raising it
|
|
75
|
+
// at COMPILE time keeps instance validation spec-exact — no data is
|
|
76
|
+
// ever invalidated by an unknown format, the schema's AUTHOR is told.
|
|
77
|
+
if (compiler == null) {
|
|
78
|
+
if (schemaObj.options.unknownFormats === 'ignore')
|
|
79
|
+
return undefined;
|
|
80
|
+
throw new Error(`Unknown format '${jsonSchema.format}': no compiler is registered for it, `
|
|
81
|
+
+ 'so this schema would accept every value for that keyword. Register one '
|
|
82
|
+
+ '(addFormats(dateTimeFormats) etc. from @jarenjs/formats, or addFormat(name, compiler) '
|
|
83
|
+
+ "for your own), or pass { unknownFormats: 'ignore' } to accept it as an annotation.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A format COMPILER is called once per schema location and returns the
|
|
87
|
+
// per-value validator. Handing it a bare tester — `formatTesters`
|
|
88
|
+
// instead of `dateTimeFormats`, an easy mistake since both are objects
|
|
89
|
+
// full of functions — either returns a boolean (which the caller
|
|
90
|
+
// silently drops, so the format checks nothing again) or throws deep
|
|
91
|
+
// inside the tester on an argument it never expected. Both become one
|
|
92
|
+
// legible complaint here.
|
|
93
|
+
let validator;
|
|
94
|
+
try {
|
|
95
|
+
validator = compiler(schemaObj, jsonSchema);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
throw new Error(`Format '${jsonSchema.format}' failed to compile: ${err?.message ?? err}. `
|
|
99
|
+
+ `${WRONG_SHAPE_HINT}`, { cause: err });
|
|
100
|
+
}
|
|
101
|
+
// Nullish stays legal: a compiler may decline a schema it cannot serve.
|
|
102
|
+
if (validator != null && !isFn(validator) && !Array.isArray(validator)) {
|
|
103
|
+
throw new Error(`Format '${jsonSchema.format}' is registered with something that is not a `
|
|
104
|
+
+ `format compiler: calling it returned ${typeof validator}, not a validator function. `
|
|
105
|
+
+ `${WRONG_SHAPE_HINT}`);
|
|
106
|
+
}
|
|
107
|
+
return validator;
|
|
108
|
+
}
|