@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
@@ -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
+ }