schiva 0.1.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.
@@ -0,0 +1,59 @@
1
+ function deepEqual(a, b) {
2
+ if (a === b) return true;
3
+ if (Number.isNaN(a) && Number.isNaN(b)) return true;
4
+ if (a && b && typeof a === 'object' && typeof b === 'object') {
5
+ if (a.constructor !== b.constructor) return false;
6
+ if (Array.isArray(a)) {
7
+ const l = a.length;
8
+ if (l !== b.length) return false;
9
+ for (let i = 0; i < l; i += 1) {
10
+ if (!deepEqual(a[i], b[i])) return false;
11
+ }
12
+ return true;
13
+ }
14
+ if (a instanceof Map && b instanceof Map) {
15
+ if (a.size !== b.size) return false;
16
+ const keys = [...a.keys()];
17
+ for (let i = 0; i < keys.length; i += 1) {
18
+ const key = keys[i];
19
+ if (!b.has(key)) return false;
20
+ }
21
+ for (let i = 0; i < keys.length; i += 1) {
22
+ const key = keys[i];
23
+ if (!deepEqual(a.get(key), b.get(key))) return false;
24
+ }
25
+ return true;
26
+ }
27
+ if (a instanceof Set && b instanceof Set) {
28
+ if (a.size !== b.size) return false;
29
+ const keys = [...a.keys()];
30
+ for (let i = 0; i < keys.length; i += 1) {
31
+ const key = keys[i];
32
+ if (!b.has(key)) return false;
33
+ }
34
+ return true;
35
+ }
36
+ if (ArrayBuffer.isView(a)) {
37
+ const l = a.length;
38
+ if (l !== b.length) return false;
39
+ for (let i = 0; i < l; i += 1) {
40
+ if (a[i] !== b[i]) return false;
41
+ }
42
+ return true;
43
+ }
44
+ if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags;
45
+ if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf();
46
+ if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();
47
+ const keys = Object.keys(a);
48
+ if (keys.length !== Object.keys(b).length) return false;
49
+ for (let i = 0; i < keys.length; i += 1) {
50
+ const key = keys[i];
51
+ if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
52
+ if (!deepEqual(a[key], b[key])) return false;
53
+ }
54
+ return true;
55
+ }
56
+ return false;
57
+ }
58
+
59
+ module.exports = { deepEqual };
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ const closedSchema = require('./closed-schema');
2
+ const compile = require('./compile');
3
+ const jsonSchema = require('./json-schema');
4
+ const schema = require('./schema');
5
+ const types = require('./types');
6
+
7
+ module.exports = {
8
+ ...closedSchema,
9
+ ...compile,
10
+ ...jsonSchema,
11
+ ...schema,
12
+ ...types,
13
+ };
@@ -0,0 +1,170 @@
1
+ // Resolution of JSON Schema (draft-07) references: JSON pointers ("#/definitions/a"), "$id" base URI changes and "$id"
2
+ // anchors ("#foo"), within the schema and within other documents registered by URI. Nothing is loaded from the
3
+ // network: a reference to a document that is not registered does not resolve.
4
+
5
+ // Base URI of a document without "$id".
6
+ const DEFAULT_BASE = 'schiva://schema/root.json';
7
+
8
+ // Keywords whose value is a subschema, a map of subschemas or a list of subschemas, where "$id" can appear.
9
+ const SCHEMA_KEYWORDS = [
10
+ 'additionalItems',
11
+ 'additionalProperties',
12
+ 'contains',
13
+ 'else',
14
+ 'if',
15
+ 'items',
16
+ 'not',
17
+ 'propertyNames',
18
+ 'then',
19
+ ];
20
+ const SCHEMA_MAP_KEYWORDS = ['definitions', 'dependencies', 'patternProperties', 'properties'];
21
+ const SCHEMA_LIST_KEYWORDS = ['allOf', 'anyOf', 'items', 'oneOf'];
22
+
23
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
24
+ const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
25
+
26
+ function resolveUri(ref, base) {
27
+ try {
28
+ return new URL(ref, base).href;
29
+ } catch (e) {
30
+ return undefined;
31
+ }
32
+ }
33
+
34
+ function splitFragment(uri) {
35
+ const index = uri.indexOf('#');
36
+ return index === -1 ? [uri, ''] : [uri.slice(0, index), uri.slice(index + 1)];
37
+ }
38
+
39
+ function decode(text) {
40
+ try {
41
+ return decodeURIComponent(text);
42
+ } catch (e) {
43
+ return undefined;
44
+ }
45
+ }
46
+
47
+ // Follows a JSON pointer ("/a/b~1c/0") from a node; undefined when a token is missing.
48
+ function followPointer(node, pointer) {
49
+ const tokens = pointer
50
+ .split('/')
51
+ .slice(1)
52
+ .map((token) => token.replace(/~1/g, '/').replace(/~0/g, '~'));
53
+ let current = node;
54
+ for (let i = 0; i < tokens.length; i += 1) {
55
+ const token = tokens[i];
56
+ if (Array.isArray(current) && /^(0|[1-9][0-9]*)$/.test(token)) {
57
+ current = current[Number(token)];
58
+ } else if (current !== null && typeof current === 'object' && !Array.isArray(current) && hasOwn(current, token)) {
59
+ current = current[token];
60
+ } else {
61
+ return undefined;
62
+ }
63
+ }
64
+ return current;
65
+ }
66
+
67
+ // Documents to register, from { uri: schema } or [schema with "$id"]. URIs must be absolute, without a fragment other
68
+ // than an empty one ("http://json-schema.org/draft-07/schema#").
69
+ function documentsOf(schemas) {
70
+ if (schemas === undefined) {
71
+ return [];
72
+ }
73
+ let entries;
74
+ if (Array.isArray(schemas)) {
75
+ entries = schemas.map((schema) => [schema && schema.$id, schema]);
76
+ } else if (isObject(schemas)) {
77
+ entries = Object.entries(schemas);
78
+ } else {
79
+ throw new Error('Unsupported JSON Schema option "schemas": expected an object of schemas by URI or an array');
80
+ }
81
+ return entries.map(([uri, schema]) => {
82
+ const absolute = typeof uri === 'string' ? resolveUri(uri, undefined) : undefined;
83
+ const [document, fragment] = absolute === undefined ? [] : splitFragment(absolute);
84
+ if (absolute === undefined || fragment !== '') {
85
+ throw new Error(`Unsupported JSON Schema option "schemas": "${uri}" is not an absolute URI without fragment`);
86
+ }
87
+ return { uri: document, schema };
88
+ });
89
+ }
90
+
91
+ class RefIndex {
92
+ constructor(root, schemas = undefined) {
93
+ // Documents (URIs without fragment) and anchors ("uri#name") to their schema node.
94
+ this.resources = new Map([[DEFAULT_BASE, root]]);
95
+ this.anchors = new Map();
96
+ // Schema node to the base URI its references are resolved against.
97
+ this.bases = new Map();
98
+ this.visit(root, DEFAULT_BASE);
99
+ // Other documents are resolved against the URI they are registered with, unless they change it with "$id".
100
+ documentsOf(schemas).forEach(({ uri, schema }) => {
101
+ this.addResource(uri, schema);
102
+ this.visit(schema, uri);
103
+ });
104
+ }
105
+
106
+ // The first document registered for a URI keeps it.
107
+ addResource(uri, node) {
108
+ if (!this.resources.has(uri)) {
109
+ this.resources.set(uri, node);
110
+ }
111
+ }
112
+
113
+ visit(node, parentBase) {
114
+ if (!isObject(node) || this.bases.has(node)) {
115
+ return;
116
+ }
117
+ let base = parentBase;
118
+ // In draft-07 every keyword next to "$ref" is ignored, "$id" included.
119
+ if (typeof node.$id === 'string' && node.$ref === undefined) {
120
+ const uri = resolveUri(node.$id, parentBase);
121
+ if (uri !== undefined) {
122
+ const [document, fragment] = splitFragment(uri);
123
+ const anchor = `${document}#${decode(fragment)}`;
124
+ if (fragment === '') {
125
+ base = document;
126
+ this.addResource(document, node);
127
+ } else if (!this.anchors.has(anchor)) {
128
+ this.anchors.set(anchor, node);
129
+ }
130
+ }
131
+ }
132
+ this.bases.set(node, base);
133
+ SCHEMA_KEYWORDS.forEach((keyword) => this.visit(node[keyword], base));
134
+ SCHEMA_MAP_KEYWORDS.filter((keyword) => isObject(node[keyword])).forEach((keyword) =>
135
+ Object.values(node[keyword]).forEach((child) => this.visit(child, base))
136
+ );
137
+ SCHEMA_LIST_KEYWORDS.filter((keyword) => Array.isArray(node[keyword])).forEach((keyword) =>
138
+ node[keyword].forEach((child) => this.visit(child, base))
139
+ );
140
+ }
141
+
142
+ // Schema node that the "$ref" of `node` points to, or undefined when it is not in this document.
143
+ resolve(node) {
144
+ const uri = resolveUri(node.$ref, this.bases.get(node) ?? DEFAULT_BASE);
145
+ if (uri === undefined) {
146
+ return undefined;
147
+ }
148
+ const [document, rawFragment] = splitFragment(uri);
149
+ const fragment = decode(rawFragment);
150
+ if (fragment === undefined) {
151
+ return undefined;
152
+ }
153
+ let target;
154
+ if (fragment === '' || fragment.startsWith('/')) {
155
+ const resource = this.resources.get(document);
156
+ target = resource === undefined ? undefined : followPointer(resource, fragment);
157
+ if (target !== undefined) {
158
+ // A pointer can reach a node that was not indexed as a schema; its references resolve against the document.
159
+ this.visit(target, document);
160
+ }
161
+ } else {
162
+ target = this.anchors.get(`${document}#${fragment}`);
163
+ }
164
+ return target;
165
+ }
166
+ }
167
+
168
+ module.exports = {
169
+ RefIndex,
170
+ };
@@ -0,0 +1,420 @@
1
+ const { Schema } = require('./schema');
2
+ const { compileType } = require('./compile');
3
+ const { RefIndex } = require('./json-schema-refs');
4
+
5
+ const {
6
+ AllOfType,
7
+ AnyOfType,
8
+ AnyType,
9
+ ArrayOfType,
10
+ BooleanType,
11
+ ConditionalType,
12
+ FloatType,
13
+ IntegerType,
14
+ NeverType,
15
+ NotType,
16
+ OneOfType,
17
+ RefType,
18
+ StringType,
19
+ ValuesType,
20
+ WhenType,
21
+ } = require('./types');
22
+
23
+ const ANNOTATIONS = [
24
+ '$schema',
25
+ '$id',
26
+ '$comment',
27
+ 'title',
28
+ 'description',
29
+ 'default',
30
+ 'examples',
31
+ 'format',
32
+ 'readOnly',
33
+ 'writeOnly',
34
+ 'deprecated',
35
+ 'nullable',
36
+ // Only used through "$ref".
37
+ 'definitions',
38
+ ];
39
+
40
+ const TYPED_KEYWORDS = {
41
+ properties: 'object',
42
+ patternProperties: 'object',
43
+ dependencies: 'object',
44
+ propertyNames: 'object',
45
+ contains: 'array',
46
+ additionalItems: 'array',
47
+ required: 'object',
48
+ additionalProperties: 'object',
49
+ minProperties: 'object',
50
+ maxProperties: 'object',
51
+ items: 'array',
52
+ minItems: 'array',
53
+ maxItems: 'array',
54
+ uniqueItems: 'array',
55
+ minLength: 'string',
56
+ maxLength: 'string',
57
+ pattern: 'string',
58
+ minimum: 'number',
59
+ maximum: 'number',
60
+ exclusiveMinimum: 'number',
61
+ exclusiveMaximum: 'number',
62
+ multipleOf: 'number',
63
+ };
64
+
65
+ const UNTYPED_KEYWORDS = ['type', 'enum', 'const', 'anyOf', 'oneOf', 'not', 'allOf', 'if', 'then', 'else'];
66
+
67
+ const TYPE_NAMES = ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'];
68
+
69
+ function getTypeNames(json) {
70
+ if (json.type === undefined) {
71
+ return [];
72
+ }
73
+ return Array.isArray(json.type) ? json.type : [json.type];
74
+ }
75
+
76
+ function checkKeywords(json, path) {
77
+ const typeNames = getTypeNames(json);
78
+ typeNames.forEach((typeName) => {
79
+ if (!TYPE_NAMES.includes(typeName)) {
80
+ throw new Error(`Unsupported JSON Schema type "${typeName}" at ${path}`);
81
+ }
82
+ });
83
+ Object.keys(json).forEach((keyword) => {
84
+ if (ANNOTATIONS.includes(keyword) || UNTYPED_KEYWORDS.includes(keyword)) {
85
+ return;
86
+ }
87
+ const requiredType = TYPED_KEYWORDS[keyword];
88
+ if (!requiredType) {
89
+ throw new Error(`Unsupported JSON Schema keyword "${keyword}" at ${path}`);
90
+ }
91
+ // Without "type" a keyword only applies to values of its type. With a "type" that excludes it, the keyword could
92
+ // never apply, which is most likely a mistake.
93
+ const isDeclared = typeNames.includes(requiredType) || (requiredType === 'number' && typeNames.includes('integer'));
94
+ if (typeNames.length > 0 && !isDeclared) {
95
+ throw new Error(`JSON Schema keyword "${keyword}" at ${path} requires "type": "${requiredType}"`);
96
+ }
97
+ });
98
+ }
99
+
100
+ // State of the conversion in progress: the reference index of the document, the types converted for reference
101
+ // targets, and the references still to resolve.
102
+ let context;
103
+
104
+ // null is valid only if every constraint of the node accepts it. `seen` stops at reference cycles, which give no
105
+ // value that accepts null.
106
+ function acceptsNull(json, seen = new Set()) {
107
+ if (json === true) {
108
+ return true;
109
+ }
110
+ if (json === false || json === null || typeof json !== 'object') {
111
+ return false;
112
+ }
113
+ if (json.$ref !== undefined) {
114
+ const target = context.index.resolve(json);
115
+ if (target === undefined || seen.has(json)) {
116
+ return false;
117
+ }
118
+ // `seen` holds the references being followed, so a target reached again through another path is not a cycle.
119
+ seen.add(json);
120
+ const result = acceptsNull(target, seen);
121
+ seen.delete(json);
122
+ return result;
123
+ }
124
+ if (json.nullable === true) {
125
+ return true;
126
+ }
127
+ const checks = [];
128
+ if (json.type !== undefined) {
129
+ checks.push(getTypeNames(json).includes('null'));
130
+ }
131
+ if (json.enum) {
132
+ checks.push(json.enum.includes(null));
133
+ }
134
+ if ('const' in json) {
135
+ checks.push(json.const === null);
136
+ }
137
+ if (json.anyOf) {
138
+ checks.push(json.anyOf.some((item) => acceptsNull(item, seen)));
139
+ }
140
+ if (json.oneOf) {
141
+ checks.push(json.oneOf.filter((item) => acceptsNull(item, seen)).length === 1);
142
+ }
143
+ if (json.not !== undefined) {
144
+ checks.push(!acceptsNull(json.not, seen));
145
+ }
146
+ if (json.allOf) {
147
+ checks.push(json.allOf.every((item) => acceptsNull(item, seen)));
148
+ }
149
+ if (json.if !== undefined) {
150
+ const branch = acceptsNull(json.if, seen) ? json.then : json.else;
151
+ checks.push(branch === undefined || acceptsNull(branch, seen));
152
+ }
153
+ return checks.every(Boolean);
154
+ }
155
+
156
+ // Inner types of a combination only check non-null values: the outer type owns mandatory/nullable.
157
+ function asInner(type) {
158
+ type.isMandatory = false;
159
+ type.isNullable = true;
160
+
161
+ return type;
162
+ }
163
+
164
+ function combine(types, Type) {
165
+ if (types.length === 0) {
166
+ return new AnyType();
167
+ }
168
+ if (types.length === 1) {
169
+ return types[0];
170
+ }
171
+ return new Type({ types: types.map(asInner) });
172
+ }
173
+
174
+ let convert;
175
+
176
+ // A list of required properties, or a schema the whole object must satisfy, for each key.
177
+ function convertDependencies(json, path) {
178
+ const dependencies = json.dependencies || {};
179
+ return Object.keys(dependencies).map((key) => {
180
+ const dependency = dependencies[key];
181
+ if (Array.isArray(dependency)) {
182
+ if (!dependency.every((property) => typeof property === 'string')) {
183
+ throw new Error(`Unsupported JSON Schema at ${path}.dependencies.${key}: expected property names`);
184
+ }
185
+ return { key, required: dependency };
186
+ }
187
+ return { key, type: asInner(convert(dependency, `${path}.dependencies.${key}`)) };
188
+ });
189
+ }
190
+
191
+ function convertObject(json, path) {
192
+ const properties = json.properties || {};
193
+ const required = json.required || [];
194
+ const { additionalProperties } = json;
195
+ const additionalType =
196
+ additionalProperties !== undefined && typeof additionalProperties === 'object'
197
+ ? convert(additionalProperties, `${path}.additionalProperties`)
198
+ : undefined;
199
+ // No prototype: keys such as __proto__ or toString must be plain entries.
200
+ const definition = Object.create(null);
201
+ Object.keys(properties).forEach((key) => {
202
+ definition[key] = convert(properties[key], `${path}.properties.${key}`, required.includes(key));
203
+ });
204
+ required
205
+ .filter((key) => !Object.prototype.hasOwnProperty.call(definition, key))
206
+ .forEach((key) => {
207
+ definition[key] = additionalType
208
+ ? convert(additionalProperties, `${path}.additionalProperties`)
209
+ : new AnyType({ isNullable: true });
210
+ });
211
+ const patternProperties = json.patternProperties || {};
212
+ const patternTypes = Object.keys(patternProperties).map((source) => ({
213
+ pattern: new RegExp(source, 'u'),
214
+ type: convert(patternProperties[source], `${path}.patternProperties.${source}`),
215
+ }));
216
+ return new Schema(definition, {
217
+ isOpen: additionalProperties !== false,
218
+ additionalType,
219
+ patternTypes,
220
+ dependencies: convertDependencies(json, path),
221
+ propertyNameType:
222
+ json.propertyNames === undefined ? undefined : convert(json.propertyNames, `${path}.propertyNames`),
223
+ minProperties: json.minProperties,
224
+ maxProperties: json.maxProperties,
225
+ });
226
+ }
227
+
228
+ function convertArray(json, path) {
229
+ const { items } = json;
230
+ let type;
231
+ if (Array.isArray(items)) {
232
+ type = items.map((item, i) => convert(item, `${path}.items[${i}]`, false));
233
+ } else if (items !== undefined) {
234
+ type = convert(items, `${path}.items`);
235
+ }
236
+ // Elements are values of their own: null is checked, not skipped.
237
+ const contains = json.contains === undefined ? undefined : convert(json.contains, `${path}.contains`);
238
+ // Only used after the positions of an items array.
239
+ const additionalType =
240
+ Array.isArray(items) && json.additionalItems !== undefined
241
+ ? convert(json.additionalItems, `${path}.additionalItems`)
242
+ : undefined;
243
+ return new ArrayOfType({
244
+ type,
245
+ min: json.minItems,
246
+ max: json.maxItems,
247
+ unique: json.uniqueItems,
248
+ contains,
249
+ additionalType,
250
+ });
251
+ }
252
+
253
+ function convertNumber(json, Type, path) {
254
+ if (json.multipleOf !== undefined && !(typeof json.multipleOf === 'number' && json.multipleOf > 0)) {
255
+ throw new Error(`Unsupported JSON Schema at ${path}: "multipleOf" must be a number greater than 0`);
256
+ }
257
+ return new Type({
258
+ min: json.minimum,
259
+ max: json.maximum,
260
+ exclusiveMin: json.exclusiveMinimum,
261
+ exclusiveMax: json.exclusiveMaximum,
262
+ multipleOf: json.multipleOf,
263
+ });
264
+ }
265
+
266
+ function convertTypeName(typeName, json, path) {
267
+ switch (typeName) {
268
+ case 'object':
269
+ return convertObject(json, path);
270
+ case 'array':
271
+ return convertArray(json, path);
272
+ case 'string':
273
+ return new StringType({
274
+ min: json.minLength,
275
+ max: json.maxLength,
276
+ pattern: json.pattern === undefined ? undefined : new RegExp(json.pattern, 'u'),
277
+ allowEmpty: false,
278
+ countCodePoints: true,
279
+ });
280
+ case 'number':
281
+ return convertNumber(json, FloatType, path);
282
+ case 'integer':
283
+ return convertNumber(json, IntegerType, path);
284
+ case 'boolean':
285
+ return new BooleanType();
286
+ default:
287
+ return new ValuesType({ values: [null], isNullable: true });
288
+ }
289
+ }
290
+
291
+ // Keywords of a schema without "type": each group of them checks only the values of its JSON type.
292
+ function convertUntyped(json, path) {
293
+ const jsonTypes = [...new Set(Object.keys(json).map((keyword) => TYPED_KEYWORDS[keyword]))].filter(Boolean);
294
+ return jsonTypes.map((jsonType) => new WhenType({ jsonType, type: asInner(convertTypeName(jsonType, json, path)) }));
295
+ }
296
+
297
+ convert = (json, path, isMandatory = true) => {
298
+ if (json === true) {
299
+ return new AnyType({ isMandatory, isNullable: true });
300
+ }
301
+ if (json === false) {
302
+ return new NeverType({ isMandatory, isNullable: false });
303
+ }
304
+ if (json === null || typeof json !== 'object' || Array.isArray(json)) {
305
+ throw new Error(`Unsupported JSON Schema at ${path}: expected an object or a boolean`);
306
+ }
307
+ if (json.$ref !== undefined) {
308
+ // In draft-07 every keyword next to "$ref" is ignored.
309
+ if (typeof json.$ref !== 'string') {
310
+ throw new Error(`Unsupported JSON Schema at ${path}: "$ref" must be a string`);
311
+ }
312
+ const ref = new RefType({ ref: json.$ref, isMandatory, isNullable: true });
313
+ context.pending.push({ ref, json, path });
314
+ return ref;
315
+ }
316
+ checkKeywords(json, path);
317
+ const typeNames = getTypeNames(json);
318
+ const nonNullNames = typeNames.filter((typeName) => typeName !== 'null');
319
+ const namesToConvert = nonNullNames.length > 0 ? nonNullNames : typeNames;
320
+ const constraints = [];
321
+ if (namesToConvert.length === 0) {
322
+ constraints.push(...convertUntyped(json, path));
323
+ } else {
324
+ constraints.push(
325
+ combine(
326
+ namesToConvert.map((typeName) => convertTypeName(typeName, json, path)),
327
+ AnyOfType
328
+ )
329
+ );
330
+ }
331
+ if (json.enum) {
332
+ constraints.push(new ValuesType({ values: json.enum }));
333
+ }
334
+ if ('const' in json) {
335
+ constraints.push(new ValuesType({ values: [json.const] }));
336
+ }
337
+ if (json.anyOf) {
338
+ constraints.push(
339
+ combine(
340
+ json.anyOf.map((item, i) => convert(item, `${path}.anyOf[${i}]`)),
341
+ AnyOfType
342
+ )
343
+ );
344
+ }
345
+ if (json.oneOf) {
346
+ if (!Array.isArray(json.oneOf) || json.oneOf.length === 0) {
347
+ throw new Error(`Unsupported JSON Schema at ${path}: "oneOf" must be a non-empty array`);
348
+ }
349
+ const types = json.oneOf.map((item, i) => asInner(convert(item, `${path}.oneOf[${i}]`)));
350
+ constraints.push(new OneOfType({ types }));
351
+ }
352
+ if (json.not !== undefined) {
353
+ constraints.push(new NotType({ type: asInner(convert(json.not, `${path}.not`)) }));
354
+ }
355
+ if (json.allOf) {
356
+ json.allOf.forEach((item, i) => constraints.push(convert(item, `${path}.allOf[${i}]`)));
357
+ }
358
+ // "then" and "else" are ignored without "if", and "if" alone checks nothing.
359
+ if (json.if !== undefined && (json.then !== undefined || json.else !== undefined)) {
360
+ const branch = (keyword) =>
361
+ json[keyword] === undefined ? undefined : asInner(convert(json[keyword], `${path}.${keyword}`));
362
+ constraints.push(
363
+ new ConditionalType({
364
+ ifType: asInner(convert(json.if, `${path}.if`)),
365
+ thenType: branch('then'),
366
+ elseType: branch('else'),
367
+ })
368
+ );
369
+ }
370
+ const type = combine(constraints, AllOfType);
371
+ type.isMandatory = isMandatory;
372
+ type.isNullable = acceptsNull(json);
373
+ return type;
374
+ };
375
+
376
+ // Points every reference to the type of its target, converting each target once. Converting a target can add
377
+ // references, which the loop resolves too.
378
+ function resolveReferences() {
379
+ while (context.pending.length > 0) {
380
+ const { ref, json, path } = context.pending.shift();
381
+ const target = context.index.resolve(json);
382
+ if (target === undefined) {
383
+ throw new Error(
384
+ `Unsupported JSON Schema "$ref": "${json.$ref}" at ${path}: only references within the schema or to documents in the "schemas" option are supported`
385
+ );
386
+ }
387
+ if (!context.targets.has(target)) {
388
+ context.targets.set(target, convert(target, json.$ref));
389
+ }
390
+ ref.target = context.targets.get(target);
391
+ }
392
+ }
393
+
394
+ // Builds a validation type from a JSON Schema (draft-07). Throws on unsupported keywords instead of silently ignoring
395
+ // them. "$ref" can point within the schema or to the documents in options.schemas, given as { uri: schema } or as an
396
+ // array of schemas with "$id"; they are only converted where referenced.
397
+ function fromJsonSchema(json, options = {}) {
398
+ context = { index: new RefIndex(json, options.schemas), targets: new Map(), pending: [] };
399
+ try {
400
+ const type = convert(json, '#');
401
+ context.targets.set(json, type);
402
+ resolveReferences();
403
+ return type;
404
+ } finally {
405
+ context = undefined;
406
+ }
407
+ }
408
+
409
+ // Compatibility with ajv compile: returns a function that gives the list of errors for a value (empty when valid).
410
+ // With allErrors: false it stops at the first failing check and gives only that error. With errors: false it gives
411
+ // true or false instead, for when only validity matters. options.schemas registers other documents for "$ref", as in
412
+ // fromJsonSchema().
413
+ function compileJsonSchema(json, options = {}) {
414
+ return compileType(fromJsonSchema(json, options), options);
415
+ }
416
+
417
+ module.exports = {
418
+ fromJsonSchema,
419
+ compileJsonSchema,
420
+ };