@jarenjs/validate 0.8.4 → 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.
- package/ARCHITECTURE.md +1067 -0
- package/LICENSE +21 -0
- package/README.md +339 -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 +20 -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 +874 -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 +51 -0
- package/dist/types/traverse.d.ts +32 -0
- package/dist/types/unevaluated.d.ts +12 -0
- package/package.json +32 -7
- package/src/array.js +565 -0
- package/src/bigint.js +97 -0
- package/src/combine.js +226 -0
- package/src/condition.js +109 -0
- package/src/content.js +83 -0
- package/src/data.js +477 -0
- package/src/dollar-data.js +629 -0
- package/src/dynamic-ref.js +121 -0
- package/src/enum.js +148 -0
- package/src/format.js +66 -0
- package/src/index.js +1854 -0
- package/src/number.js +159 -0
- package/src/object.js +755 -0
- package/src/query-keyword.js +99 -0
- package/src/query.js +59 -0
- package/src/schema.js +645 -0
- package/src/string.js +152 -0
- package/src/tools.js +205 -0
- package/src/traverse.js +433 -0
- package/src/unevaluated.js +151 -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,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,148 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isObjectType,
|
|
5
|
+
isScalarType,
|
|
6
|
+
} from '@jarenjs/core';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
equalsDeep,
|
|
10
|
+
} from '@jarenjs/core/object';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getArrayClassMinItems,
|
|
14
|
+
} from './tools.js';
|
|
15
|
+
import { isDollarDataReference } from './dollar-data.js';
|
|
16
|
+
|
|
17
|
+
// export const exampleEnumDataStructure = {
|
|
18
|
+
// allOf: [
|
|
19
|
+
// {
|
|
20
|
+
// enum: {
|
|
21
|
+
// source: false, // jsonpath!!
|
|
22
|
+
// data: [
|
|
23
|
+
// { color: 0xFFFFFF, name: 'white', type: 'greys' },
|
|
24
|
+
// { color: 0x000000, name: 'black', type: 'greys' },
|
|
25
|
+
// null, // this null will not be included in resulting enum array.
|
|
26
|
+
// { color: 0xFF0000, name: 'red', type: 'reds' },
|
|
27
|
+
// { color: 0x00FF00, name: 'green', type: 'greens' },
|
|
28
|
+
// { color: 0x0000FF, name: 'blue', type: 'blues' },
|
|
29
|
+
// ],
|
|
30
|
+
// group: 'type',
|
|
31
|
+
// label: 'name',
|
|
32
|
+
// value: 'color',
|
|
33
|
+
// },
|
|
34
|
+
// },
|
|
35
|
+
// {
|
|
36
|
+
// enum: {
|
|
37
|
+
// source: false, // jsonpath!!
|
|
38
|
+
// data: [
|
|
39
|
+
// [0xFFFFFF, 'white', 'greys'],
|
|
40
|
+
// [0x000000, 'black', 'greys'],
|
|
41
|
+
// null, // null will be ignored
|
|
42
|
+
// [0xFF0000, 'red', 'reds'],
|
|
43
|
+
// [0x00FF00, 'green', 'greens'],
|
|
44
|
+
// [0x0000FF, 'blue', 'blues'],
|
|
45
|
+
// ],
|
|
46
|
+
// group: 2,
|
|
47
|
+
// label: 1,
|
|
48
|
+
// value: 0,
|
|
49
|
+
// },
|
|
50
|
+
// },
|
|
51
|
+
// {
|
|
52
|
+
// enum: {
|
|
53
|
+
// data: [
|
|
54
|
+
// { value: 0xFFFFFF, label: 'white', group: 'greys' },
|
|
55
|
+
// { value: 0x000000, label: 'black', group: 'greys' },
|
|
56
|
+
// null, // null will be ignored
|
|
57
|
+
// { value: 0xFF0000, label: 'red', group: 'reds' },
|
|
58
|
+
// { value: 0x00FF00, label: 'green', group: 'greens' },
|
|
59
|
+
// { value: 0x0000FF, label: 'blue', group: 'blues' },
|
|
60
|
+
// ],
|
|
61
|
+
// },
|
|
62
|
+
// },
|
|
63
|
+
// {
|
|
64
|
+
// enum: {
|
|
65
|
+
// data: [
|
|
66
|
+
// [0xFFFFFF, 'white', 'greys'],
|
|
67
|
+
// [0x000000, 'black', 'greys'],
|
|
68
|
+
// null, // null will be ignored
|
|
69
|
+
// [0xFF0000, 'red', 'reds'],
|
|
70
|
+
// [0x00FF00, 'green', 'greens'],
|
|
71
|
+
// [0x0000FF, 'blue', 'blues'],
|
|
72
|
+
// ],
|
|
73
|
+
// },
|
|
74
|
+
// },
|
|
75
|
+
// ],
|
|
76
|
+
// };
|
|
77
|
+
|
|
78
|
+
function compileConst(schemaObj, jsonSchema) {
|
|
79
|
+
const constant = jsonSchema.const;
|
|
80
|
+
if (constant === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
|
|
83
|
+
if (isDollarDataReference(constant))
|
|
84
|
+
return undefined;
|
|
85
|
+
|
|
86
|
+
const addError = schemaObj.createErrorHandler(constant, 'const');
|
|
87
|
+
|
|
88
|
+
if (constant === null || isScalarType(constant)) {
|
|
89
|
+
return function validatePrimitiveConst(data, dataPath) {
|
|
90
|
+
return constant === data
|
|
91
|
+
|| addError(data, dataPath);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
return function validateComplexConst(data, dataPath) {
|
|
96
|
+
return equalsDeep(constant, data)
|
|
97
|
+
|| addError(data, dataPath);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function compileEnum(schemaObj, jsonSchema) {
|
|
103
|
+
const enums = getArrayClassMinItems(jsonSchema.enum, 1);
|
|
104
|
+
if (enums == null) return undefined;
|
|
105
|
+
|
|
106
|
+
let hasObjects = false;
|
|
107
|
+
for (let i = 0; i < enums.length; ++i) {
|
|
108
|
+
const e = enums[i];
|
|
109
|
+
if (e != null && typeof e === 'object') {
|
|
110
|
+
hasObjects = true;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const addError = schemaObj.createErrorHandler(enums, 'enum');
|
|
116
|
+
|
|
117
|
+
if (hasObjects === false) {
|
|
118
|
+
return function validateEnumSimple(data, dataPath) {
|
|
119
|
+
return data === undefined
|
|
120
|
+
? true
|
|
121
|
+
: enums.includes(data)
|
|
122
|
+
? true
|
|
123
|
+
: addError(data, dataPath);
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
return function validateEnumDeep(data, dataPath) {
|
|
128
|
+
if (data === undefined) return true;
|
|
129
|
+
if (data === null || typeof data !== 'object')
|
|
130
|
+
return enums.includes(data)
|
|
131
|
+
? true
|
|
132
|
+
: addError(data, dataPath);
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < enums.length; ++i) {
|
|
135
|
+
if (equalsDeep(enums[i], data) === true)
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return addError(data, dataPath);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function compileEnumBasic(schemaObj, jsonSchema) {
|
|
144
|
+
return [
|
|
145
|
+
compileConst(schemaObj, jsonSchema),
|
|
146
|
+
compileEnum(schemaObj, jsonSchema),
|
|
147
|
+
];
|
|
148
|
+
}
|
package/src/format.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
export function getSchemaFormatCompiler(registered, name) {
|
|
44
|
+
if (isStringType(name))
|
|
45
|
+
return registered[name];
|
|
46
|
+
else
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function compileFormatBasic(schemaObj, jsonSchema) {
|
|
51
|
+
if (!isStringType(jsonSchema.format))
|
|
52
|
+
return undefined;
|
|
53
|
+
|
|
54
|
+
// From draft 2020-12 on, format is annotation-only unless assertion is
|
|
55
|
+
// enabled (formatAssertion option or format-assertion vocabulary).
|
|
56
|
+
if (schemaObj.options.formatAssertion === false)
|
|
57
|
+
return undefined;
|
|
58
|
+
const compiler = getSchemaFormatCompiler(
|
|
59
|
+
schemaObj.formats,
|
|
60
|
+
jsonSchema.format);
|
|
61
|
+
|
|
62
|
+
if (compiler)
|
|
63
|
+
return compiler(schemaObj, jsonSchema);
|
|
64
|
+
else
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|