@hyperjump/json-schema 1.1.1 → 1.2.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/README.md CHANGED
@@ -11,6 +11,9 @@ A collection of modules for working with JSON Schemas.
11
11
  * Schemas can reference other schemas using a different dialect
12
12
  * Work directly with schemas on the filesystem or HTTP
13
13
  * Create custom keywords, vocabularies, and dialects
14
+ * Bundle multiple schemas into one document
15
+ * Uses the process defined in the 2020-12 specification but works with any
16
+ dialect.
14
17
  * Provides utilities for building non-validation JSON Schema tooling
15
18
 
16
19
  ## Install
@@ -41,7 +44,12 @@ The API for this library is divided into two categories: Stable and
41
44
  Experimental. The Stable API strictly follows semantic versioning, but the
42
45
  Experimental API may have backward-incompatible changes between minor versions.
43
46
 
44
- ## Usage
47
+ All experimental features are segregated into exports that include the word
48
+ "experimental" so you never accidentally depend on something that could change
49
+ or be removed in future releases.
50
+
51
+ ## Validation
52
+ ### Usage
45
53
  This library supports many versions of JSON Schema. Use the pattern
46
54
  `@hyperjump/json-schema/*` to import the version you need.
47
55
 
@@ -130,7 +138,7 @@ const isString = await validate(`file://${__dirname}/string.schema.yaml`);
130
138
  const output = isString("foo");
131
139
  ```
132
140
 
133
- **Open API**
141
+ **OpenAPI**
134
142
 
135
143
  The OpenAPI 3.0 and 3.1 meta-schemas are pre-loaded and the OpenAPI JSON Schema
136
144
  dialects for each of those versions is supported. A document with a Content-Type
@@ -155,7 +163,7 @@ const output = await validate("https://spec.openapis.org/oas/3.1/schema-base", o
155
163
  const output = await validate(`file://${__dirname}/example.openapi.json#/components/schemas/foo`, 42);
156
164
  ```
157
165
 
158
- ## API
166
+ ### API
159
167
  These are available from any of the exports that refer to a version of JSON
160
168
  Schema, such as `@hyperjump/json-schema/draft-2020-12`.
161
169
 
@@ -178,12 +186,12 @@ Schema, such as `@hyperjump/json-schema/draft-2020-12`.
178
186
 
179
187
  This error is thrown if the schema being compiled is found to be invalid.
180
188
  The `output` field contains an `OutputUnit` with information about the
181
- error. You can use the `setMetaOutputFormat` configuration to set the output
182
- format that is returned in `output`.
183
- * **setMetaOutputFormat**: (outputFormat: OutputFormat) => void
189
+ error. You can use the `setMetaSchemaOutputFormat` configuration to set the
190
+ output format that is returned in `output`.
191
+ * **setMetaSchemaOutputFormat**: (outputFormat: OutputFormat) => void
184
192
 
185
193
  Set the output format used for validating schemas.
186
- * **getMetaOutputFormat**: () => OutputFormat
194
+ * **getMetaSchemaOutputFormat**: () => OutputFormat
187
195
 
188
196
  Get the output format used for validating schemas.
189
197
  * **setShouldMetaValidate**: (isEnabled: boolean) => void
@@ -221,28 +229,92 @@ The following types are used in the above definitions
221
229
  Given a filesystem path, return whether or not the file should be
222
230
  considered a member of this media type.
223
231
 
224
- ## Experimental
225
- The JSON Schema specification includes several features that are experimental in
226
- nature including the Vocabulary System, Output Formats, and Annotations. This
227
- implementation aims to support only the latest version of experimental features
228
- as they evolve. There will not be a major version bump if there needs to be
229
- backward incompatible changes to the Experimental API.
230
-
232
+ ## Bundling
231
233
  ### Usage
232
- All experimental features are segregated into exports that include the word
233
- "experimental" so you never accidentally depend on something that could change
234
- or be removed in future releases.
234
+ You can bundle schemas with external references into single deliverable using
235
+ the official JSON Schema bundling process introduced in the 2020-12
236
+ specification. Given a schema with external references, any external schemas
237
+ will be embedded in the schema resulting in a Compound Schema Document with all
238
+ the schemas necessary to evaluate the given schema in one document.
239
+
240
+ The bundling process allows schemas to be embedded without needing to modify any
241
+ references which means you get the same output details whether you validate the
242
+ bundle or the original unbundled schemas.
235
243
 
236
244
  ```javascript
237
- import { BASIC } from "@hyperjump/json-schema/experimental";
245
+ import { addSchema } from "@hyperjump/json-schema/draft-2020-12";
246
+ import { bundle } from "@hyperjump/json-schema/bundle";
247
+
248
+ addSchema({
249
+ "$id": "https://example.com/main"
250
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
251
+
252
+ "type": "object",
253
+ "properties": {
254
+ "foo": { "$ref": "/string" }
255
+ }
256
+ });
257
+
258
+ addSchema({
259
+ "$id": "https://example.com/string",
260
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
261
+
262
+ "type": "string"
263
+ });
264
+
265
+ const bundledSchema = await bundle("https://example.com/main"); // {
266
+ // "$id": "https://example.com/main",
267
+ // "$schema": "https://json-schema.org/draft/2020-12/schema",
268
+ //
269
+ // "type": "object",
270
+ // "properties": {
271
+ // "foo": { "$ref": "/string" }
272
+ // },
273
+ //
274
+ // "$defs": {
275
+ // "https://example.com/main": {
276
+ // "$id": "https://example.com/main",
277
+ // "type": "string"
278
+ // }
279
+ // }
280
+ // }
238
281
  ```
239
282
 
283
+ ### API
284
+ These are available from the `@hyperjump/json-schema/bundle` export.
285
+
286
+ * **bundle**: (uri: string, options: Options) => Promise<SchemaObject>
287
+
288
+ Create a bundled schema starting with the given schema. External schemas
289
+ will be fetched from the filesystem, the network, or internally as needed.
290
+
291
+ Options:
292
+ * alwaysIncludeDialect: boolean (default: false) -- Include dialect even
293
+ when it isn't strictly needed
294
+ * bundleMode: "flat" | "full" (default: "flat") -- When bundling schemas
295
+ that already contain bundled schemas, "flat" mode with remove nested
296
+ embedded schemas and put them all in the top level `$defs`. When using
297
+ "full" mode, it will keep the already embedded schemas around, which will
298
+ result in some embedded schema duplication.
299
+ * definitionNamingStrategy: "uri" | "uuid" (default: "uri") -- By default
300
+ the name used in definitions for embedded schemas will match the
301
+ identifier of the embedded schema. This naming is unlikely to collide
302
+ with actual definitions, but if you want to be sure, you can use the
303
+ "uuid" strategy instead to be sure you get a unique name.
304
+ * externalSchemas: string[] (default: []) -- A list of schemas URIs that
305
+ are available externally and should not be included in the bundle.
306
+
307
+ ## Output Formats (Experimental)
308
+ ### Usage
309
+
240
310
  **Change the validation output format**
241
311
 
242
312
  The `FLAG` output format isn't very informative. You can change the output
243
- format used for validation to get more information.
313
+ format used for validation to get more information about failures.
244
314
 
245
315
  ```javascript
316
+ import { BASIC } from "@hyperjump/json-schema/experimental";
317
+
246
318
  const output = await validate("https://example.com/schema1", 42, BASIC);
247
319
  ```
248
320
 
@@ -251,7 +323,10 @@ const output = await validate("https://example.com/schema1", 42, BASIC);
251
323
  The output format used for validating schemas can be changed as well.
252
324
 
253
325
  ```javascript
254
- setMetaOutputFormat(BASIC);
326
+ import { validate, setMetaSchemaOutputFormat } from "@hyperjump/json-schema/draft-2020-12";
327
+ import { BASIC } from "@hyperjump/json-schema/experimental";
328
+
329
+ setMetaSchemaOutputFormat(BASIC);
255
330
  try {
256
331
  const output = await validate("https://example.com/invalid-schema");
257
332
  } catch (error) {
@@ -259,8 +334,20 @@ try {
259
334
  }
260
335
  ```
261
336
 
262
- **Keywords, Vocabularies, and Dialects**
337
+ ### API
338
+ **Type Definitions**
339
+
340
+ * **OutputFormat**: **FLAG** | **BASIC** | **DETAILED** | **VERBOSE**
341
+
342
+ In addition to the `FLAG` output format in the Stable API, the Experimental
343
+ API includes support for the `BASIC`, `DETAILED`, and `VERBOSE` formats as
344
+ specified in the 2019-09 specification (with some minor customizations).
345
+ This implementation doesn't include annotations or human readable error
346
+ messages. The output can be processed to create human readable error
347
+ messages as needed.
263
348
 
349
+ ## Meta-Schemas, Keywords, Vocabularies, and Dialects (Experimental)
350
+ ### Usage
264
351
  In order to create and use a custom keyword, you need to define your keyword's
265
352
  behavior, create a vocabulary that includes that keyword, and then create a
266
353
  dialect that includes your vocabulary.
@@ -384,14 +471,6 @@ const output = await validate("https://example.com/schema1", 42); // Expect Inva
384
471
  ### API
385
472
  These are available from the `@hyperjump/json-schema/experimental` export.
386
473
 
387
- * **compile**: (schema: SchemaDocument) => Promise<CompiledSchema>
388
-
389
- Return a compiled schema. This is useful if you're creating tooling for
390
- something other than validation.
391
- * **interpret**: (schema: CompiledSchema, instance: Instance, outputFormat: OutputFormat = BASIC) => OutputUnit
392
-
393
- A curried function for validating an instance against a compiled schema.
394
- This can be useful for creating custom output formats.
395
474
  * **addKeyword**: (keywordHandler: Keyword) => void
396
475
 
397
476
  Define a keyword for use in a vocabulary.
@@ -419,18 +498,6 @@ These are available from the `@hyperjump/json-schema/experimental` export.
419
498
  A Keyword object that represents a "validate" operation. You would use this
420
499
  for compiling and evaluating sub-schemas when defining a custom keyword.
421
500
 
422
- **Type Definitions**
423
-
424
- The following types are used in the above definitions
425
-
426
- * **OutputFormat**: **FLAG** | **BASIC** | **DETAILED** | **VERBOSE**
427
-
428
- In addition to the `FLAG` output format in the Stable API, the Experimental
429
- API includes support for the `BASIC`, `DETAILED`, and `VERBOSE` formats as
430
- specified in the 2019-09 specification (with some minor customizations).
431
- This implementation doesn't include annotations or human readable error
432
- messages. The output can be processed to create human readable error
433
- messages as needed.
434
501
  * **Keyword**: object
435
502
  * id: string
436
503
 
@@ -459,7 +526,7 @@ The following types are used in the above definitions
459
526
  If the keyword is an applicator, it will need to implements this
460
527
  function for `unevaluatedItems` to work as expected.
461
528
 
462
- ### Schema
529
+ ### Schema API
463
530
  These functions are available from the
464
531
  `@hyperjump/json-schema/schema/experimental` export.
465
532
 
@@ -518,7 +585,7 @@ The following types are used in the above definitions
518
585
  * includeEmbedded: boolean (default: true) -- If false, embedded schemas
519
586
  will be unbundled from the schema.
520
587
 
521
- ### Instance
588
+ ### Instance API
522
589
  These functions are available from the
523
590
  `@hyperjump/json-schema/instance/experimental` export.
524
591
 
@@ -569,6 +636,19 @@ set of functions for working with InstanceDocuments.
569
636
 
570
637
  Similar to `Array.prototype.length`.
571
638
 
639
+ ## Low-level Utilities (Experimental)
640
+ ### API
641
+ These are available from the `@hyperjump/json-schema/experimental` export.
642
+
643
+ * **compile**: (schema: SchemaDocument) => Promise<CompiledSchema>
644
+
645
+ Return a compiled schema. This is useful if you're creating tooling for
646
+ something other than validation.
647
+ * **interpret**: (schema: CompiledSchema, instance: Instance, outputFormat: OutputFormat = BASIC) => OutputUnit
648
+
649
+ A curried function for validating an instance against a compiled schema.
650
+ This can be useful for creating custom output formats.
651
+
572
652
  ## Contributing
573
653
 
574
654
  ### Tests
@@ -0,0 +1,18 @@
1
+ import type { SchemaObject } from "../lib/schema.js";
2
+
3
+
4
+ export const bundle: <A = SchemaObject>(uri: string, options?: BundleOptions) => Promise<A>;
5
+ export const FULL: "full";
6
+ export const FLAT: "flat";
7
+ export const URI: "uri";
8
+ export const UUID: "uuid";
9
+
10
+ export type BundleOptions = {
11
+ alwaysIncludeDialect?: boolean;
12
+ bundleMode?: BundleMode;
13
+ definitionNamingStrategy?: DefinitionNamingStrategy;
14
+ externalSchemas?: string[];
15
+ };
16
+
17
+ export type BundleMode = "full" | "flat";
18
+ export type DefinitionNamingStrategy = "uri" | "uuid";
@@ -0,0 +1,269 @@
1
+ import { v4 as uuid } from "uuid";
2
+ import * as JsonPointer from "@hyperjump/json-pointer";
3
+ import { toAbsoluteUri } from "../lib/common.js";
4
+ import { compile } from "../lib/core.js";
5
+ import { getKeywordName, getKeyword } from "../lib/keywords.js";
6
+ import Validation from "../lib/keywords/validation.js";
7
+ import * as Schema from "../lib/schema.js";
8
+
9
+
10
+ export const FULL = "full", FLAT = "flat";
11
+ export const URI = "uri", UUID = "uuid";
12
+
13
+ const defaultOptions = {
14
+ alwaysIncludeDialect: false,
15
+ bundleMode: FLAT,
16
+ definitionNamingStrategy: URI,
17
+ externalSchemas: []
18
+ };
19
+
20
+ export const bundle = async (url, options = {}) => {
21
+ loadKeywordSupport();
22
+ const fullOptions = { ...defaultOptions, ...options };
23
+
24
+ const schemaDoc = await Schema.get(url);
25
+ const externalIds = await collectExternalIds(url, fullOptions);
26
+
27
+ const bundled = Schema.toSchema(schemaDoc, {
28
+ includeEmbedded: fullOptions.bundleMode === FULL
29
+ });
30
+
31
+ const bundlingLocation = "/" + getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/definitions");
32
+ if (JsonPointer.get(bundlingLocation, bundled) === undefined && externalIds.size > 0) {
33
+ JsonPointer.assign(bundlingLocation, bundled, {});
34
+ }
35
+
36
+ for (const uri of externalIds.values()) {
37
+ const externalSchema = await Schema.get(uri);
38
+ const embeddedSchema = Schema.toSchema(externalSchema, {
39
+ parentId: schemaDoc.id,
40
+ parentDialect: fullOptions.alwaysIncludeDialect ? "" : schemaDoc.dialectId,
41
+ includeEmbedded: fullOptions.bundleMode === FULL
42
+ });
43
+ let id;
44
+ if (fullOptions.definitionNamingStrategy === URI) {
45
+ const idToken = getKeywordName(externalSchema.dialectId, "https://json-schema.org/keyword/id")
46
+ || getKeywordName(externalSchema.dialectId, "https://json-schema.org/keyword/draft-04/id");
47
+ id = embeddedSchema[idToken];
48
+ } else if (fullOptions.definitionNamingStrategy === UUID) {
49
+ id = uuid();
50
+ } else {
51
+ throw Error(`Unknown definition naming stragety: ${fullOptions.definitionNamingStrategy}`);
52
+ }
53
+ const pointer = JsonPointer.append(id, bundlingLocation);
54
+ JsonPointer.assign(pointer, bundled, embeddedSchema);
55
+ }
56
+
57
+ return bundled;
58
+ };
59
+
60
+ const collectExternalIds = async (uri, options) => {
61
+ const { ast, schemaUri } = await compile(uri);
62
+ const subSchemaUris = new Set();
63
+ Validation.collectExternalIds(schemaUri, subSchemaUris, ast, {});
64
+ const externalIds = new Set([...subSchemaUris]
65
+ .map(toAbsoluteUri)
66
+ .filter((uri) => !options.externalSchemas.includes(uri)));
67
+ externalIds.delete(toAbsoluteUri(schemaUri));
68
+
69
+ return externalIds;
70
+ };
71
+
72
+ Validation.collectExternalIds = (schemaUri, externalIds, ast, dynamicAnchors) => {
73
+ const nodes = ast[schemaUri][2];
74
+ if (externalIds.has(schemaUri) || typeof nodes === "boolean") {
75
+ return;
76
+ }
77
+ externalIds.add(schemaUri);
78
+
79
+ const id = toAbsoluteUri(schemaUri);
80
+ for (const [keywordId, , keywordValue] of nodes) {
81
+ const keyword = getKeyword(keywordId);
82
+
83
+ if (keyword.collectExternalIds) {
84
+ keyword.collectExternalIds(keywordValue, externalIds, ast, {
85
+ ...ast.metaData[id].dynamicAnchors, ...dynamicAnchors
86
+ });
87
+ }
88
+ }
89
+ };
90
+
91
+ const loadKeywordSupport = () => {
92
+ // Stable
93
+
94
+ const additionalProperties = getKeyword("https://json-schema.org/keyword/additionalProperties");
95
+ if (additionalProperties) {
96
+ additionalProperties.collectExternalIds = ([, , additionalProperties], externalIds, ast, dynamicAnchors) => {
97
+ if (typeof additionalProperties === "string") {
98
+ Validation.collectExternalIds(additionalProperties, externalIds, ast, dynamicAnchors);
99
+ }
100
+ };
101
+ }
102
+
103
+ const allOf = getKeyword("https://json-schema.org/keyword/allOf");
104
+ if (allOf) {
105
+ allOf.collectExternalIds = (allOf, externalIds, ast, dynamicAnchors) => {
106
+ allOf.forEach((schemaUri) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
107
+ };
108
+ }
109
+
110
+ const anyOf = getKeyword("https://json-schema.org/keyword/anyOf");
111
+ if (anyOf) {
112
+ anyOf.collectExternalIds = (anyOf, externalIds, ast, dynamicAnchors) => {
113
+ anyOf.forEach((schemaUri) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
114
+ };
115
+ }
116
+
117
+ const contains = getKeyword("https://json-schema.org/keyword/contains");
118
+ if (contains) {
119
+ contains.collectExternalIds = ({ contains }, externalIds, ast, dynamicAnchors) => {
120
+ Validation.collectExternalIds(contains, externalIds, ast, dynamicAnchors);
121
+ };
122
+ }
123
+
124
+ const dependentSchemas = getKeyword("https://json-schema.org/keyword/dependentSchemas");
125
+ if (dependentSchemas) {
126
+ dependentSchemas.collectExternalIds = (dependentSchemas, externalIds, ast, dynamicAnchors) => {
127
+ Object.values(dependentSchemas).forEach(([, schemaUri]) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
128
+ };
129
+ }
130
+
131
+ const if_ = getKeyword("https://json-schema.org/keyword/if");
132
+ if (if_) {
133
+ if_.collectExternalIds = Validation.collectExternalIds;
134
+ }
135
+
136
+ const then = getKeyword("https://json-schema.org/keyword/then");
137
+ if (then) {
138
+ then.collectExternalIds = ([, then], externalIds, ast, dynamicAnchors) => {
139
+ Validation.collectExternalIds(then, externalIds, ast, dynamicAnchors);
140
+ };
141
+ }
142
+
143
+ const else_ = getKeyword("https://json-schema.org/keyword/else");
144
+ if (else_) {
145
+ else_.collectExternalIds = ([, elseSchema], externalIds, ast, dynamicAnchors) => {
146
+ Validation.collectExternalIds(elseSchema, externalIds, ast, dynamicAnchors);
147
+ };
148
+ }
149
+
150
+ const items = getKeyword("https://json-schema.org/keyword/items");
151
+ if (items) {
152
+ items.collectExternalIds = ([, items], externalIds, ast, dynamicAnchors) => {
153
+ Validation.collectExternalIds(items, externalIds, ast, dynamicAnchors);
154
+ };
155
+ }
156
+
157
+ const not = getKeyword("https://json-schema.org/keyword/not");
158
+ if (not) {
159
+ not.collectExternalIds = Validation.collectExternalIds;
160
+ }
161
+
162
+ const oneOf = getKeyword("https://json-schema.org/keyword/oneOf");
163
+ if (oneOf) {
164
+ oneOf.collectExternalIds = (oneOf, externalIds, ast, dynamicAnchors) => {
165
+ oneOf.forEach((schemaUri) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
166
+ };
167
+ }
168
+
169
+ const patternProperties = getKeyword("https://json-schema.org/keyword/patternProperties");
170
+ if (patternProperties) {
171
+ patternProperties.collectExternalIds = (patternProperties, externalIds, ast, dynamicAnchors) => {
172
+ patternProperties.forEach(([, schemaUri]) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
173
+ };
174
+ }
175
+
176
+ const prefixItems = getKeyword("https://json-schema.org/keyword/prefixItems");
177
+ if (prefixItems) {
178
+ prefixItems.collectExternalIds = (tupleItems, externalIds, ast, dynamicAnchors) => {
179
+ tupleItems.forEach((schemaUri) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
180
+ };
181
+ }
182
+
183
+ const properties = getKeyword("https://json-schema.org/keyword/properties");
184
+ if (properties) {
185
+ properties.collectExternalIds = (properties, externalIds, ast, dynamicAnchors) => {
186
+ Object.values(properties).forEach((schemaUri) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
187
+ };
188
+ }
189
+
190
+ const propertyNames = getKeyword("https://json-schema.org/keyword/propertyNames");
191
+ if (propertyNames) {
192
+ propertyNames.collectExternalIds = Validation.collectExternalIds;
193
+ }
194
+
195
+ const ref = getKeyword("https://json-schema.org/keyword/ref");
196
+ if (ref) {
197
+ ref.collectExternalIds = Validation.collectExternalIds;
198
+ }
199
+
200
+ const unevaluatedItems = getKeyword("https://json-schema.org/keyword/unevaluatedItems");
201
+ if (unevaluatedItems) {
202
+ unevaluatedItems.collectExternalIds = ([, unevaluatedItems], externalIds, ast, dynamicAnchors) => {
203
+ Validation.collectExternalIds(unevaluatedItems, externalIds, ast, dynamicAnchors);
204
+ };
205
+ }
206
+
207
+ const unevaluatedProperties = getKeyword("https://json-schema.org/keyword/unevaluatedProperties");
208
+ if (unevaluatedProperties) {
209
+ unevaluatedProperties.collectExternalIds = ([, unevaluatedProperties], externalIds, ast, dynamicAnchors) => {
210
+ Validation.collectExternalIds(unevaluatedProperties, externalIds, ast, dynamicAnchors);
211
+ };
212
+ }
213
+
214
+ // Draft-04
215
+
216
+ const additionalItems4 = getKeyword("https://json-schema.org/keyword/draft-04/additionalItems");
217
+ if (additionalItems4) {
218
+ additionalItems4.collectExternalIds = ([, additionalItems], externalIds, ast, dynamicAnchors) => {
219
+ if (typeof additionalItems === "string") {
220
+ Validation.collectExternalIds(additionalItems, externalIds, ast, dynamicAnchors);
221
+ }
222
+ };
223
+ }
224
+
225
+ const dependencies = getKeyword("https://json-schema.org/keyword/draft-04/dependencies");
226
+ if (dependencies) {
227
+ dependencies.collectExternalIds = (dependentSchemas, externalIds, ast, dynamicAnchors) => {
228
+ Object.values(dependentSchemas).forEach(([, dependency]) => {
229
+ if (typeof dependency === "string") {
230
+ Validation.collectExternalIds(dependency, externalIds, ast, dynamicAnchors);
231
+ }
232
+ });
233
+ };
234
+ }
235
+
236
+ const items4 = getKeyword("https://json-schema.org/keyword/draft-04/items");
237
+ if (items4) {
238
+ items4.collectExternalIds = (items, externalIds, ast, dynamicAnchors) => {
239
+ if (typeof items === "string") {
240
+ Validation.collectExternalIds(items, externalIds, ast, dynamicAnchors);
241
+ } else {
242
+ items.forEach((schemaUri) => Validation.collectExternalIds(schemaUri, externalIds, ast, dynamicAnchors));
243
+ }
244
+ };
245
+ }
246
+
247
+ const ref4 = getKeyword("https://json-schema.org/keyword/draft-04/ref");
248
+ if (ref4) {
249
+ ref4.collectExternalIds = Validation.collectExternalIds;
250
+ }
251
+
252
+ // Draft-06
253
+
254
+ const contains6 = getKeyword("https://json-schema.org/keyword/draft-06/contains");
255
+ if (contains6) {
256
+ contains6.collectExternalIds = (contains, externalIds, ast, dynamicAnchors) => {
257
+ Validation.collectExternalIds(contains, externalIds, ast, dynamicAnchors);
258
+ };
259
+ }
260
+
261
+ // Draft-2019-09
262
+
263
+ const contains19 = getKeyword("https://json-schema.org/keyword/draft-2019-09/contains");
264
+ if (contains19) {
265
+ contains19.collectExternalIds = ({ contains }, externalIds, ast, dynamicAnchors) => {
266
+ Validation.collectExternalIds(contains, externalIds, ast, dynamicAnchors);
267
+ };
268
+ }
269
+ };
@@ -7,6 +7,7 @@ const compile = (schema) => schema.id;
7
7
 
8
8
  const interpret = (id, instance, ast, dynamicAnchors) => {
9
9
  if ("" in ast.metaData[id].dynamicAnchors) {
10
+ dynamicAnchors = { ...ast.metaData[id].dynamicAnchors, ...dynamicAnchors };
10
11
  return Validation.interpret(dynamicAnchors[""], instance, ast, dynamicAnchors);
11
12
  } else {
12
13
  return Validation.interpret(`${id}#`, instance, ast, dynamicAnchors);
@@ -14,6 +14,7 @@ const compile = async (dynamicRef, ast) => {
14
14
 
15
15
  const interpret = ([id, fragment, ref], instance, ast, dynamicAnchors) => {
16
16
  if (fragment in ast.metaData[id].dynamicAnchors) {
17
+ dynamicAnchors = { ...ast.metaData[id].dynamicAnchors, ...dynamicAnchors };
17
18
  return Validation.interpret(dynamicAnchors[fragment], instance, ast, dynamicAnchors);
18
19
  } else {
19
20
  return Validation.interpret(ref, instance, ast, dynamicAnchors);
@@ -7,7 +7,7 @@ export const addMediaTypePlugin = (contentType, plugin) => {
7
7
  mediaTypePlugins[contentType] = plugin;
8
8
  };
9
9
 
10
- export const parse = (response) => {
10
+ export const parseResponse = (response) => {
11
11
  const contentType = contentTypeParser.parse(response.headers.get("content-type"));
12
12
  if (!(contentType.type in mediaTypePlugins)) {
13
13
  throw Error(`${response.url} is not a schema. Found a document with media type: ${contentType.type}`);
package/lib/schema.js CHANGED
@@ -4,8 +4,8 @@ import * as Json from "@hyperjump/json";
4
4
  import * as JsonPointer from "@hyperjump/json-pointer";
5
5
  import { jsonTypeOf, resolveUri, toAbsoluteUri, uriFragment, pathRelative } from "./common.js";
6
6
  import fetch from "./fetch.js";
7
- import * as Keywords from "./keywords.js";
8
- import * as MediaTypes from "./media-types.js";
7
+ import { hasDialect, loadDialect, getKeywordName } from "./keywords.js";
8
+ import { parseResponse } from "./media-types.js";
9
9
  import * as Reference from "./reference.js";
10
10
 
11
11
 
@@ -22,13 +22,13 @@ export const add = (schema, retrievalUri = undefined, contextDialectId = undefin
22
22
  const dialectId = toAbsoluteUri(schema.$schema || contextDialectId || defaultDialectId);
23
23
  delete schema.$schema;
24
24
 
25
- if (!Keywords.hasDialect(dialectId)) {
25
+ if (!hasDialect(dialectId)) {
26
26
  throw Error(`Encountered unknown dialect '${dialectId}'`);
27
27
  }
28
28
 
29
29
  // Identifiers
30
- const idToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/id")
31
- || Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/draft-04/id");
30
+ const idToken = getKeywordName(dialectId, "https://json-schema.org/keyword/id")
31
+ || getKeywordName(dialectId, "https://json-schema.org/keyword/draft-04/id");
32
32
  if (retrievalUri === undefined && !(idToken in schema)) {
33
33
  throw Error(`Unable to determine an identifier for the schema. Use the '${idToken}' keyword or pass a retrievalUri when loading the schema.`);
34
34
  }
@@ -41,19 +41,19 @@ export const add = (schema, retrievalUri = undefined, contextDialectId = undefin
41
41
  }
42
42
 
43
43
  // Vocabulary
44
- const vocabularyToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/vocabulary");
44
+ const vocabularyToken = getKeywordName(dialectId, "https://json-schema.org/keyword/vocabulary");
45
45
  if (jsonTypeOf(schema[vocabularyToken], "object")) {
46
46
  const allowUnknownKeywords = schema[vocabularyToken]["https://json-schema.org/draft/2019-09/vocab/core"]
47
47
  || schema[vocabularyToken]["https://json-schema.org/draft/2020-12/vocab/core"];
48
48
 
49
- Keywords.loadDialect(id, schema[vocabularyToken], allowUnknownKeywords);
49
+ loadDialect(id, schema[vocabularyToken], allowUnknownKeywords);
50
50
  delete schema[vocabularyToken];
51
51
  }
52
52
 
53
53
  const dynamicAnchors = {};
54
54
 
55
55
  // Recursive anchor
56
- const recursiveAnchorToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/draft-2019-09/recursiveAnchor");
56
+ const recursiveAnchorToken = getKeywordName(dialectId, "https://json-schema.org/keyword/draft-2019-09/recursiveAnchor");
57
57
  if (schema[recursiveAnchorToken] === true) {
58
58
  dynamicAnchors[""] = `${id}#`;
59
59
  }
@@ -76,7 +76,7 @@ export const add = (schema, retrievalUri = undefined, contextDialectId = undefin
76
76
  const processSchema = (subject, id, dialectId, pointer, anchors, dynamicAnchors) => {
77
77
  if (jsonTypeOf(subject, "object")) {
78
78
  // Legacy id
79
- const legacyIdToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/draft-04/id");
79
+ const legacyIdToken = getKeywordName(dialectId, "https://json-schema.org/keyword/draft-04/id");
80
80
  if (typeof subject[legacyIdToken] === "string") {
81
81
  if (subject[legacyIdToken][0] === "#") {
82
82
  const anchor = decodeURIComponent(subject[legacyIdToken].slice(1));
@@ -92,7 +92,7 @@ const processSchema = (subject, id, dialectId, pointer, anchors, dynamicAnchors)
92
92
 
93
93
  // Embedded Schema
94
94
  const embeddedDialectId = typeof subject.$schema === "string" ? toAbsoluteUri(subject.$schema) : dialectId;
95
- const idToken = Keywords.getKeywordName(embeddedDialectId, "https://json-schema.org/keyword/id");
95
+ const idToken = getKeywordName(embeddedDialectId, "https://json-schema.org/keyword/id");
96
96
  if (typeof subject[idToken] === "string") {
97
97
  subject[idToken] = resolveUri(subject[idToken], id);
98
98
  add(subject, undefined, dialectId);
@@ -100,27 +100,27 @@ const processSchema = (subject, id, dialectId, pointer, anchors, dynamicAnchors)
100
100
  }
101
101
 
102
102
  // Legacy dynamic anchor
103
- const legacyDynamicAnchorToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/draft-2020-12/dynamicAnchor");
103
+ const legacyDynamicAnchorToken = getKeywordName(dialectId, "https://json-schema.org/keyword/draft-2020-12/dynamicAnchor");
104
104
  if (typeof subject[legacyDynamicAnchorToken] === "string") {
105
105
  dynamicAnchors[subject[legacyDynamicAnchorToken]] = `${id}#${encodeURI(pointer)}`;
106
106
  anchors[subject[legacyDynamicAnchorToken]] = pointer;
107
107
  delete subject[legacyDynamicAnchorToken];
108
108
  }
109
109
 
110
- const dynamicAnchorToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/dynamicAnchor");
110
+ const dynamicAnchorToken = getKeywordName(dialectId, "https://json-schema.org/keyword/dynamicAnchor");
111
111
  if (typeof subject[dynamicAnchorToken] === "string") {
112
112
  dynamicAnchors[subject[dynamicAnchorToken]] = `${id}#${encodeURI(pointer)}`;
113
113
  delete subject[dynamicAnchorToken];
114
114
  }
115
115
 
116
- const anchorToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/anchor");
116
+ const anchorToken = getKeywordName(dialectId, "https://json-schema.org/keyword/anchor");
117
117
  if (typeof subject[anchorToken] === "string") {
118
118
  anchors[subject[anchorToken]] = pointer;
119
119
  delete subject[anchorToken];
120
120
  }
121
121
 
122
122
  // Legacy $ref
123
- const jrefToken = Keywords.getKeywordName(dialectId, "https://json-schema.org/keyword/draft-04/ref");
123
+ const jrefToken = getKeywordName(dialectId, "https://json-schema.org/keyword/draft-04/ref");
124
124
  if (typeof subject[jrefToken] === "string") {
125
125
  return Reference.cons(subject[jrefToken], subject);
126
126
  }
@@ -168,11 +168,11 @@ export const get = async (url, contextDoc = nil) => {
168
168
  throw Error(`Failed to retrieve schema with id: ${id}`);
169
169
  }
170
170
 
171
- const [schema, contextDialectId] = await MediaTypes.parse(response);
171
+ const [schema, contextDialectId] = await parseResponse(response);
172
172
 
173
173
  // Try to determine the dialect from the meta-schema if it isn't already known
174
174
  const dialectId = toAbsoluteUri(schema.$schema || contextDialectId || defaultDialectId);
175
- if (!Keywords.hasDialect(dialectId) && !hasStoredSchema(dialectId)) {
175
+ if (!hasDialect(dialectId) && !hasStoredSchema(dialectId)) {
176
176
  await get(dialectId);
177
177
  }
178
178
 
@@ -241,11 +241,12 @@ const toSchemaDefaultOptions = {
241
241
  export const toSchema = (schemaDoc, options = {}) => {
242
242
  const fullOptions = { ...toSchemaDefaultOptions, ...options };
243
243
 
244
- const idToken = Keywords.getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/id");
245
- const anchorToken = Keywords.getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/anchor");
246
- const dynamicAnchorToken = Keywords.getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/dynamicAnchor");
247
- const legacyDynamicAnchorToken = Keywords.getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/draft-2020-12/dynamicAnchor");
248
- const recursiveAnchorToken = Keywords.getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/recursiveAnchor");
244
+ const idToken = getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/id")
245
+ || getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/draft-04/id");
246
+ const anchorToken = getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/anchor");
247
+ const dynamicAnchorToken = getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/dynamicAnchor");
248
+ const legacyDynamicAnchorToken = getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/draft-2020-12/dynamicAnchor");
249
+ const recursiveAnchorToken = getKeywordName(schemaDoc.dialectId, "https://json-schema.org/keyword/recursiveAnchor");
249
250
 
250
251
  const anchors = {};
251
252
  for (const anchor in schemaDoc.anchors) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperjump/json-schema",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "A JSON Schema validator with support for custom keywords, vocabularies, and dialects",
5
5
  "type": "module",
6
6
  "main": "./stable/index.js",
@@ -15,7 +15,8 @@
15
15
  "./openapi-3-1": "./openapi-3-1/index.js",
16
16
  "./experimental": "./lib/experimental.js",
17
17
  "./schema/experimental": "./lib/schema.js",
18
- "./instance/experimental": "./lib/instance.js"
18
+ "./instance/experimental": "./lib/instance.js",
19
+ "./bundle": "./bundle/index.js"
19
20
  },
20
21
  "browser": {
21
22
  "./lib/fetch.js": "./lib/fetch.browser.js"
@@ -23,7 +24,7 @@
23
24
  "scripts": {
24
25
  "clean": "xargs -a .gitignore rm -rf",
25
26
  "lint": "eslint lib stable draft-* openapi-*",
26
- "test": "mocha 'lib/**/*.spec.ts' 'stable/**/*.spec.ts' 'draft-*/**/*.spec.ts' 'openapi-*/**/*.spec.ts'"
27
+ "test": "mocha 'lib/**/*.spec.ts' 'stable/**/*.spec.ts' 'draft-*/**/*.spec.ts' 'openapi-*/**/*.spec.ts' 'bundle/**/*.spec.ts'"
27
28
  },
28
29
  "repository": "github:hyperjump-io/json-schema",
29
30
  "keywords": [
@@ -67,6 +68,7 @@
67
68
  "@hyperjump/uri": "^1.0.0",
68
69
  "content-type": "^1.0.4",
69
70
  "fastest-stable-stringify": "^2.0.2",
70
- "node-fetch": "^3.3.0"
71
+ "node-fetch": "^3.3.0",
72
+ "uuid": "^9.0.0"
71
73
  }
72
74
  }