@hyperjump/json-schema 1.14.1 → 1.15.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
@@ -212,11 +212,11 @@ Schema, such as `@hyperjump/json-schema/draft-2020-12`.
212
212
  Load a schema manually rather than fetching it from the filesystem or over
213
213
  the network. Any schema already registered with the same identifier will be
214
214
  replaced with no warning.
215
- * **validate**: (schemaURI: string, instance: any, outputFormat: OutputFormat = FLAG) => Promise\<OutputUnit>
215
+ * **validate**: (schemaURI: string, instance: any, outputFormat: ValidationOptions | OutputFormat = FLAG) => Promise\<OutputUnit>
216
216
 
217
217
  Validate an instance against a schema. This function is curried to allow
218
218
  compiling the schema once and applying it to multiple instances.
219
- * **validate**: (schemaURI: string) => Promise\<(instance: any, outputFormat: OutputFormat = FLAG) => OutputUnit>
219
+ * **validate**: (schemaURI: string) => Promise\<(instance: any, outputFormat: ValidationOptions | OutputFormat = FLAG) => OutputUnit>
220
220
 
221
221
  Compiling a schema to a validation function.
222
222
  * **FLAG**: "FLAG"
@@ -255,6 +255,10 @@ The following types are used in the above definitions
255
255
  Output is an experimental feature of the JSON Schema specification. There
256
256
  may be additional fields present in the OutputUnit, but only the `valid`
257
257
  property should be considered part of the Stable API.
258
+ * **ValidationOptions**:
259
+
260
+ * outputFormat?: OutputFormat
261
+ * plugins?: EvaluationPlugin[]
258
262
 
259
263
  ## Bundling
260
264
 
@@ -504,6 +508,57 @@ registerSchema({
504
508
  const output = await validate("https://example.com/schema1", 42); // Expect InvalidSchemaError
505
509
  ```
506
510
 
511
+ ### EvaluationPlugins
512
+
513
+ EvaluationPlugins allow you to hook into the validation process for various
514
+ purposes. There are hooks for before an after schema evaluation and before and
515
+ after keyword evaluation. (See the API section for the full interface) The
516
+ following is a simple example to record all the schema locations that were
517
+ evaluated. This could be used as part of a solution for determining test
518
+ coverage for a schema.
519
+
520
+ ```JavaScript
521
+ import { registerSchema, validate } from "@hyperjump/json-schema/draft-2020-12";
522
+ import { BASIC } from "@hyperjump/json-schema/experimental.js";
523
+
524
+ class EvaluatedKeywordsPlugin {
525
+ constructor() {
526
+ this.schemaLocations = new Set();
527
+ }
528
+
529
+ beforeKeyword([, schemaUri]) {
530
+ this.schemaLocations.add(schemaUri);
531
+ }
532
+ }
533
+
534
+ registerSchema({
535
+ $schema: "https://json-schema.org/draft/2020-12/schema",
536
+ type: "object",
537
+ properties: {
538
+ foo: { type: "number" },
539
+ bar: { type: "boolean" }
540
+ },
541
+ required: ["foo"]
542
+ }, "https://schemas.hyperjump.io/main");
543
+
544
+ const evaluatedKeywordPlugin = new EvaluatedKeywordsPlugin();
545
+
546
+ await validate("https://schemas.hyperjump.io/main", { foo: 42 }, {
547
+ outputFormat: BASIC,
548
+ plugins: [evaluatedKeywordPlugin]
549
+ });
550
+
551
+ console.log(evaluatedKeywordPlugin.schemaLocations);
552
+ // Set(4) {
553
+ // 'https://schemas.hyperjump.io/main#/type',
554
+ // 'https://schemas.hyperjump.io/main#/properties',
555
+ // 'https://schemas.hyperjump.io/main#/properties/foo/type',
556
+ // 'https://schemas.hyperjump.io/main#/required'
557
+ // }
558
+
559
+ // NOTE: #/properties/bar is not in the list because the instance doesn't include that property.
560
+ ```
561
+
507
562
  ### API
508
563
 
509
564
  These are available from the `@hyperjump/json-schema/experimental` export.
@@ -543,15 +598,13 @@ These are available from the `@hyperjump/json-schema/experimental` export.
543
598
  function to return the annotation.
544
599
  * plugin?: EvaluationPlugin
545
600
 
601
+ If the keyword needs to track state during the evaluation process, you
602
+ can include an EvaluationPlugin that will get added only when this
603
+ keyword is present in the schema.
604
+
546
605
  * **ValidationContext**: object
547
606
  * ast: AST
548
607
  * plugins: EvaluationPlugins[]
549
-
550
- * **EvaluationPlugin**: object
551
- * beforeSchema(url: string, instance: JsonNode, context: Context): void
552
- * beforeKeyword(keywordNode: Node<any>, instance: JsonNode, context: Context, schemaContext: Context, keyword: Keyword): void
553
- * afterKeyword(keywordNode: Node<any>, instance: JsonNode, context: Context, valid: boolean, schemaContext: Context, keyword: Keyword): void
554
- * afterSchema(url: string, instance: JsonNode, context: Context, valid: boolean): void
555
608
  * **defineVocabulary**: (id: string, keywords: { [keyword: string]: string }) => void
556
609
 
557
610
  Define a vocabulary that maps keyword name to keyword URIs defined using
@@ -630,6 +683,12 @@ These are available from the `@hyperjump/json-schema/experimental` export.
630
683
  include annotations or human readable error messages. The output can be
631
684
  processed to create human readable error messages as needed.
632
685
 
686
+ * **EvaluationPlugin**: object
687
+ * beforeSchema?(url: string, instance: JsonNode, context: Context): void
688
+ * beforeKeyword?(keywordNode: CompiledSchemaNode, instance: JsonNode, context: Context, schemaContext: Context, keyword: Keyword): void
689
+ * afterKeyword?(keywordNode: CompiledSchemaNode, instance: JsonNode, context: Context, valid: boolean, schemaContext: Context, keyword: Keyword): void
690
+ * afterSchema?(url: string, instance: JsonNode, context: Context, valid: boolean): void
691
+
633
692
  ## Instance API (experimental)
634
693
 
635
694
  These functions are available from the
@@ -1,18 +1,18 @@
1
- import type { OutputFormat, OutputUnit } from "../lib/index.js";
1
+ import type { OutputFormat, OutputUnit, ValidationOptions } from "../lib/index.js";
2
2
  import type { CompiledSchema } from "../lib/experimental.js";
3
3
  import type { JsonNode } from "../lib/json-node.js";
4
4
  import type { Json } from "@hyperjump/json-pointer";
5
5
 
6
6
 
7
7
  export const annotate: (
8
- (schemaUrl: string, value: Json, outputFormat?: OutputFormat) => Promise<JsonNode>
8
+ (schemaUrl: string, value: Json, options?: OutputFormat | ValidationOptions) => Promise<JsonNode>
9
9
  ) & (
10
10
  (schemaUrl: string) => Promise<Annotator>
11
11
  );
12
12
 
13
- export type Annotator = (value: Json, outputFormat?: OutputFormat) => JsonNode;
13
+ export type Annotator = (value: Json, options?: OutputFormat | ValidationOptions) => JsonNode;
14
14
 
15
- export const interpret: (compiledSchema: CompiledSchema, value: JsonNode, outputFormat?: OutputFormat) => JsonNode;
15
+ export const interpret: (compiledSchema: CompiledSchema, value: JsonNode, options?: OutputFormat | ValidationOptions) => JsonNode;
16
16
 
17
17
  export class ValidationError extends Error {
18
18
  public output: OutputUnit;
@@ -1,50 +1,36 @@
1
- import { FLAG } from "../lib/index.js";
2
1
  import { ValidationError } from "./validation-error.js";
3
2
  import {
4
3
  getSchema,
5
4
  compile,
5
+ interpret as validate,
6
6
  BASIC,
7
- DETAILED,
8
- annotationsPlugin,
9
- basicOutputPlugin,
10
- detailedOutputPlugin
7
+ AnnotationsPlugin
11
8
  } from "../lib/experimental.js";
12
- import Validation from "../lib/keywords/validation.js";
13
9
  import * as Instance from "../lib/instance.js";
14
10
 
15
11
 
16
- export const annotate = async (schemaUri, json = undefined, outputFormat = undefined) => {
12
+ export const annotate = async (schemaUri, json = undefined, options = undefined) => {
17
13
  const schema = await getSchema(schemaUri);
18
14
  const compiled = await compile(schema);
19
- const interpretAst = (json, outputFormat) => interpret(compiled, Instance.fromJs(json), outputFormat);
15
+ const interpretAst = (json, options) => interpret(compiled, Instance.fromJs(json), options);
20
16
 
21
- return json === undefined ? interpretAst : interpretAst(json, outputFormat);
17
+ return json === undefined ? interpretAst : interpretAst(json, options);
22
18
  };
23
19
 
24
- export const interpret = ({ ast, schemaUri }, instance, outputFormat = BASIC) => {
25
- const context = { ast, plugins: [annotationsPlugin, ...ast.plugins] };
26
-
27
- switch (outputFormat) {
28
- case FLAG:
29
- break;
30
- case BASIC:
31
- context.plugins.push(basicOutputPlugin);
32
- break;
33
- case DETAILED:
34
- context.plugins.push(detailedOutputPlugin);
35
- break;
36
- default:
37
- throw Error(`Unsupported output format '${outputFormat}'`);
38
- }
20
+ export const interpret = (compiledSchema, instance, options = BASIC) => {
21
+ const annotationsPlugin = new AnnotationsPlugin();
22
+ const plugins = options.plugins ?? [];
39
23
 
40
- const valid = Validation.interpret(schemaUri, instance, context);
24
+ const output = validate(compiledSchema, instance, {
25
+ outputFormat: typeof options === "string" ? options : options.outputFormat ?? BASIC,
26
+ plugins: [annotationsPlugin, ...plugins]
27
+ });
41
28
 
42
- if (!valid) {
43
- const result = !valid && "errors" in context ? { valid, errors: context.errors } : { valid };
44
- throw new ValidationError(result);
29
+ if (!output.valid) {
30
+ throw new ValidationError(output);
45
31
  }
46
32
 
47
- for (const annotation of context.annotations) {
33
+ for (const annotation of annotationsPlugin.annotations) {
48
34
  const node = Instance.get(annotation.instanceLocation, instance);
49
35
  const keyword = annotation.keyword;
50
36
  if (!node.annotations[keyword]) {
@@ -32,10 +32,6 @@ const plugin = {
32
32
  },
33
33
  beforeKeyword(_url, _instance, context, schemaContext) {
34
34
  context.dynamicAnchors = schemaContext.dynamicAnchors;
35
- },
36
- afterKeyword() {
37
- },
38
- afterSchema() {
39
35
  }
40
36
  };
41
37
 
package/lib/core.js CHANGED
@@ -11,19 +11,19 @@ import { InvalidSchemaError } from "./invalid-schema-error.js";
11
11
  import { getSchema, registerSchema, unregisterSchema as schemaUnregister } from "./schema.js";
12
12
  import { getKeywordName } from "./keywords.js";
13
13
  import Validation from "./keywords/validation.js";
14
- import { basicOutputPlugin } from "./evaluation-plugins/basic-output.js";
15
- import { detailedOutputPlugin } from "./evaluation-plugins/detailed-output.js";
14
+ import { BasicOutputPlugin } from "./evaluation-plugins/basic-output.js";
15
+ import { DetailedOutputPlugin } from "./evaluation-plugins/detailed-output.js";
16
16
 
17
17
 
18
18
  export const FLAG = "FLAG", BASIC = "BASIC", DETAILED = "DETAILED";
19
19
  setMetaSchemaOutputFormat(FLAG);
20
20
 
21
- export const validate = async (url, value = undefined, outputFormat = undefined) => {
21
+ export const validate = async (url, value = undefined, options = undefined) => {
22
22
  const schema = await getSchema(url);
23
23
  const compiled = await compile(schema);
24
- const interpretAst = (value, outputFormat) => interpret(compiled, Instance.fromJs(value), outputFormat);
24
+ const interpretAst = (value, options) => interpret(compiled, Instance.fromJs(value), options);
25
25
 
26
- return value === undefined ? interpretAst : interpretAst(value, outputFormat);
26
+ return value === undefined ? interpretAst : interpretAst(value, options);
27
27
  };
28
28
 
29
29
  export const compile = async (schema) => {
@@ -32,24 +32,30 @@ export const compile = async (schema) => {
32
32
  return { ast, schemaUri };
33
33
  };
34
34
 
35
- export const interpret = curry(({ ast, schemaUri }, instance, outputFormat = FLAG) => {
36
- const context = { ast, plugins: [...ast.plugins] };
35
+ export const interpret = curry(({ ast, schemaUri }, instance, options = FLAG) => {
36
+ const outputFormat = typeof options === "string" ? options : options.outputFormat ?? FLAG;
37
+ const plugins = options.plugins ?? [];
37
38
 
39
+ const context = { ast, plugins: [...ast.plugins, ...plugins] };
40
+
41
+ let outputPlugin;
38
42
  switch (outputFormat) {
39
43
  case FLAG:
40
44
  break;
41
45
  case BASIC:
42
- context.plugins.push(basicOutputPlugin);
46
+ outputPlugin = new BasicOutputPlugin();
47
+ context.plugins.push(outputPlugin);
43
48
  break;
44
49
  case DETAILED:
45
- context.plugins.push(detailedOutputPlugin);
50
+ outputPlugin = new DetailedOutputPlugin();
51
+ context.plugins.push(outputPlugin);
46
52
  break;
47
53
  default:
48
54
  throw Error(`Unsupported output format '${outputFormat}'`);
49
55
  }
50
56
 
51
57
  const valid = Validation.interpret(schemaUri, instance, context);
52
- return !valid && "errors" in context ? { valid, errors: context.errors } : { valid };
58
+ return !valid && outputPlugin ? { valid, errors: outputPlugin.errors } : { valid };
53
59
  });
54
60
 
55
61
  const metaValidators = {};
@@ -1,14 +1,16 @@
1
1
  import * as Instance from "../instance.js";
2
2
 
3
3
 
4
- export const annotationsPlugin = {
4
+ export class AnnotationsPlugin {
5
5
  beforeSchema(_url, _instance, context) {
6
6
  context.annotations ??= [];
7
7
  context.schemaAnnotations = [];
8
- },
8
+ }
9
+
9
10
  beforeKeyword(_node, _instance, context) {
10
11
  context.annotations = [];
11
- },
12
+ }
13
+
12
14
  afterKeyword(node, instance, context, valid, schemaContext, keyword) {
13
15
  if (valid) {
14
16
  const [keywordId, schemaUri, keywordValue] = node;
@@ -23,10 +25,13 @@ export const annotationsPlugin = {
23
25
  }
24
26
  schemaContext.schemaAnnotations.push(...context.annotations);
25
27
  }
26
- },
28
+ }
29
+
27
30
  afterSchema(_schemaNode, _instanceNode, context, valid) {
28
31
  if (valid) {
29
32
  context.annotations.push(...context.schemaAnnotations);
30
33
  }
34
+
35
+ this.annotations = context.annotations;
31
36
  }
32
- };
37
+ }
@@ -2,13 +2,15 @@ import { Validation } from "../experimental.js";
2
2
  import * as Instance from "../instance.js";
3
3
 
4
4
 
5
- export const basicOutputPlugin = {
5
+ export class BasicOutputPlugin {
6
6
  beforeSchema(_url, _intance, context) {
7
7
  context.errors ??= [];
8
- },
8
+ }
9
+
9
10
  beforeKeyword(_node, _instance, context) {
10
11
  context.errors = [];
11
- },
12
+ }
13
+
12
14
  afterKeyword(node, instance, context, valid, schemaContext, keyword) {
13
15
  if (!valid) {
14
16
  if (!keyword.simpleApplicator) {
@@ -21,7 +23,8 @@ export const basicOutputPlugin = {
21
23
  }
22
24
  schemaContext.errors.push(...context.errors);
23
25
  }
24
- },
26
+ }
27
+
25
28
  afterSchema(url, instance, context, valid) {
26
29
  if (typeof context.ast[url] === "boolean" && !valid) {
27
30
  context.errors.push({
@@ -30,5 +33,7 @@ export const basicOutputPlugin = {
30
33
  instanceLocation: Instance.uri(instance)
31
34
  });
32
35
  }
36
+
37
+ this.errors = context.errors;
33
38
  }
34
- };
39
+ }
@@ -2,13 +2,15 @@ import { Validation } from "../experimental.js";
2
2
  import * as Instance from "../instance.js";
3
3
 
4
4
 
5
- export const detailedOutputPlugin = {
5
+ export class DetailedOutputPlugin {
6
6
  beforeSchema(_url, _instance, context) {
7
7
  context.errors ??= [];
8
- },
8
+ }
9
+
9
10
  beforeKeyword(_node, _instance, context) {
10
11
  context.errors = [];
11
- },
12
+ }
13
+
12
14
  afterKeyword(node, instance, context, valid, schemaContext) {
13
15
  if (!valid) {
14
16
  const [keywordId, schemaUri] = node;
@@ -23,7 +25,8 @@ export const detailedOutputPlugin = {
23
25
  outputUnit.errors = context.errors;
24
26
  }
25
27
  }
26
- },
28
+ }
29
+
27
30
  afterSchema(url, instance, context, valid) {
28
31
  if (typeof context.ast[url] === "boolean" && !valid) {
29
32
  context.errors.push({
@@ -32,5 +35,7 @@ export const detailedOutputPlugin = {
32
35
  instanceLocation: Instance.uri(instance)
33
36
  });
34
37
  }
38
+
39
+ this.errors = context.errors;
35
40
  }
36
- };
41
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Browser, Document } from "@hyperjump/browser";
2
- import type { Validator, OutputUnit, OutputFormat, SchemaObject } from "./index.js";
2
+ import type { Validator, OutputUnit, OutputFormat, SchemaObject, EvaluationPlugin } from "./index.js";
3
3
  import type { JsonNode } from "./instance.js";
4
4
 
5
5
 
@@ -31,6 +31,14 @@ type MetaData = {
31
31
 
32
32
  type Anchors = Record<string, string>;
33
33
 
34
+ // Evaluation Plugins
35
+ export type EvaluationPlugin<Context extends ValidationOptions = ValidationOptions> = {
36
+ beforeSchema?(url: string, instance: JsonNode, context: Context): void;
37
+ beforeKeyword?(keywordNode: Node<unknown>, instance: JsonNode, context: Context, schemaContext: Context, keyword: Keyword): void;
38
+ afterKeyword?(keywordNode: Node<unknown>, instance: JsonNode, context: Context, valid: boolean, schemaContext: Context, keyword: Keyword): void;
39
+ afterSchema?(url: string, instance: JsonNode, context: Context, valid: boolean): void;
40
+ };
41
+
34
42
  // Output Formats
35
43
  export const BASIC: "BASIC";
36
44
  export const DETAILED: "DETAILED";
@@ -66,12 +74,13 @@ export const loadDialect: (dialectId: string, dialect: Record<string, boolean>,
66
74
  export const unloadDialect: (dialectId: string) => void;
67
75
  export const hasDialect: (dialectId: string) => boolean;
68
76
 
69
- export type Keyword<A> = {
77
+ export type Keyword<A, Context extends ValidationContext = ValidationContext> = {
70
78
  id: string;
71
79
  compile: (schema: Browser<SchemaDocument>, ast: AST, parentSchema: Browser<SchemaDocument>) => Promise<A>;
72
- interpret: (compiledKeywordValue: A, instance: JsonNode, context: ValidationContext) => boolean;
73
- simpleApplicator: boolean;
80
+ interpret: (compiledKeywordValue: A, instance: JsonNode, context: Context) => boolean;
81
+ simpleApplicator?: boolean;
74
82
  annotation?: <B>(compiledKeywordValue: A, instance: JsonNode) => B | undefined;
83
+ plugin?: EvaluationPlugin<Context>;
75
84
  };
76
85
 
77
86
  export type ValidationContext = {
@@ -79,20 +88,22 @@ export type ValidationContext = {
79
88
  plugins: EvaluationPlugin<unknown>[];
80
89
  };
81
90
 
82
- export type EvaluationPlugin<Context> = {
83
- beforeSchema(url: string, instance: JsonNode, context: Context): void;
84
- beforeKeyword(keywordNode: Node<unknown>, instance: JsonNode, context: Context, schemaContext: Context, keyword: Keyword): void;
85
- afterKeyword(keywordNode: Node<unknown>, instance: JsonNode, context: Context, valid: boolean, schemaContext: Context, keyword: Keyword): void;
86
- afterSchema(url: string, instance: JsonNode, context: Context, valid: boolean): void;
87
- };
91
+ export class BasicOutputPlugin implements EvaluationPlugin<ErrorsContext> {
92
+ errors: OutputUnit[];
93
+ }
94
+
95
+ export class DetailedOutputPlugin implements EvaluationPlugin<ErrorsContext> {
96
+ errors: OutputUnit[];
97
+ }
88
98
 
89
- export const basicOutputPlugin: EvaluationPlugin<ErrorsContext>;
90
- export const detailedOutputPlugin: EvaluationPlugin<ErrorsContext>;
91
99
  export type ErrorsContext = {
92
100
  errors: OutputUnit[];
93
101
  };
94
102
 
95
- export const annotationsPlugin: EvaluationPlugin<AnnotationsContext>;
103
+ export class AnnotationsPlugin implements EvaluationPlugin<AnnotationsContext> {
104
+ annotations: OutputUnit[];
105
+ }
106
+
96
107
  export type AnnotationsContext = {
97
108
  annotations: OutputUnit[];
98
109
  };
@@ -6,6 +6,6 @@ export {
6
6
  } from "./keywords.js";
7
7
  export { getSchema, toSchema, canonicalUri, buildSchemaDocument } from "./schema.js";
8
8
  export { default as Validation } from "./keywords/validation.js";
9
- export { basicOutputPlugin } from "./evaluation-plugins/basic-output.js";
10
- export { detailedOutputPlugin } from "./evaluation-plugins/detailed-output.js";
11
- export { annotationsPlugin } from "./evaluation-plugins/annotations.js";
9
+ export * from "./evaluation-plugins/basic-output.js";
10
+ export * from "./evaluation-plugins/detailed-output.js";
11
+ export * from "./evaluation-plugins/annotations.js";
package/lib/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Json } from "@hyperjump/json-pointer";
2
+ import type { EvaluationPlugin } from "./experimental.js";
2
3
 
3
4
 
4
5
  export type SchemaFragment = string | number | boolean | null | SchemaObject | SchemaFragment[];
@@ -16,13 +17,18 @@ export const getAllRegisteredSchemaUris: () => string[];
16
17
  */
17
18
  export const addSchema: typeof registerSchema;
18
19
 
20
+ export type ValidationOptions = {
21
+ outputFormat?: OutputFormat;
22
+ plugins?: EvaluationPlugin[];
23
+ };
24
+
19
25
  export const validate: (
20
- (url: string, value: Json, outputFormat?: OutputFormat) => Promise<OutputUnit>
26
+ (url: string, value: Json, options?: OutputFormat | ValidationOptions) => Promise<OutputUnit>
21
27
  ) & (
22
28
  (url: string) => Promise<Validator>
23
29
  );
24
30
 
25
- export type Validator = (value: Json, outputFormat?: OutputFormat) => OutputUnit;
31
+ export type Validator = (value: Json, options?: OutputFormat | ValidationOptions) => OutputUnit;
26
32
 
27
33
  export type OutputUnit = {
28
34
  keyword: string;
@@ -31,10 +31,6 @@ const plugin = {
31
31
  },
32
32
  beforeKeyword(_url, _instance, context, schemaContext) {
33
33
  context.dynamicAnchors = schemaContext.dynamicAnchors;
34
- },
35
- afterKeyword() {
36
- },
37
- afterSchema() {
38
34
  }
39
35
  };
40
36
 
@@ -13,25 +13,22 @@ const interpret = ([schemaUrl, unevaluatedItems], instance, context) => {
13
13
  return true;
14
14
  }
15
15
 
16
- if (context.rootSchema === schemaUrl) {
17
- return true;
18
- }
19
-
20
16
  // Because order matters, we re-evaluate this schema skipping this keyword
21
17
  // just to collect all the evalauted items.
22
- const keywordContext = {
23
- ...context,
24
- plugins: [...context.ast.plugins, unevaluatedPlugin],
25
- rootSchema: schemaUrl
26
- };
27
- if (!Validation.interpret(schemaUrl, instance, keywordContext)) {
18
+ if (context.rootSchema === schemaUrl) {
28
19
  return true;
29
20
  }
21
+ const evaluatedItemsPlugin = new EvaluatedItemsPlugin(schemaUrl);
22
+ Validation.interpret(schemaUrl, instance, {
23
+ ...context,
24
+ plugins: [...context.ast.plugins, evaluatedItemsPlugin]
25
+ });
26
+ const evaluatedItems = evaluatedItemsPlugin.evaluatedItems;
30
27
 
31
28
  let isValid = true;
32
29
  let index = 0;
33
30
  for (const item of Instance.iter(instance)) {
34
- if (!keywordContext.evaluatedItems.has(index)) {
31
+ if (!evaluatedItems.has(index)) {
35
32
  if (!Validation.interpret(unevaluatedItems, item, context)) {
36
33
  isValid = false;
37
34
  }
@@ -47,29 +44,38 @@ const interpret = ([schemaUrl, unevaluatedItems], instance, context) => {
47
44
 
48
45
  const simpleApplicator = true;
49
46
 
50
- const unevaluatedPlugin = {
47
+ class EvaluatedItemsPlugin {
48
+ constructor(rootSchema) {
49
+ this.rootSchema = rootSchema;
50
+ }
51
+
51
52
  beforeSchema(_url, _instance, context) {
52
53
  context.evaluatedItems ??= new Set();
53
54
  context.schemaEvaluatedItems ??= new Set();
54
- },
55
- beforeKeyword(_node, _instance, context, schemaContext) {
56
- context.rootSchema = schemaContext.rootSchema;
55
+ }
56
+
57
+ beforeKeyword(_node, _instance, context) {
58
+ context.rootSchema = this.rootSchema;
57
59
  context.evaluatedItems = new Set();
58
- },
60
+ }
61
+
59
62
  afterKeyword(_node, _instance, context, valid, schemaContext) {
60
63
  if (valid) {
61
64
  for (const index of context.evaluatedItems) {
62
65
  schemaContext.schemaEvaluatedItems.add(index);
63
66
  }
64
67
  }
65
- },
68
+ }
69
+
66
70
  afterSchema(_url, _instance, context, valid) {
67
71
  if (valid) {
68
72
  for (const index of context.schemaEvaluatedItems) {
69
73
  context.evaluatedItems.add(index);
70
74
  }
71
75
  }
76
+
77
+ this.evaluatedItems = context.evaluatedItems;
72
78
  }
73
- };
79
+ }
74
80
 
75
81
  export default { id, compile, interpret, simpleApplicator };
@@ -13,25 +13,22 @@ const interpret = ([schemaUrl, unevaluatedProperties], instance, context) => {
13
13
  return true;
14
14
  }
15
15
 
16
- if (context.rootSchema === schemaUrl) {
17
- return true;
18
- }
19
-
20
16
  // Because order matters, we re-evaluate this schema skipping this keyword
21
17
  // just to collect all the evalauted properties.
22
- const keywordContext = {
23
- ...context,
24
- plugins: [...context.ast.plugins, unevaluatedPlugin],
25
- rootSchema: schemaUrl
26
- };
27
- if (!Validation.interpret(schemaUrl, instance, keywordContext)) {
18
+ if (context.rootSchema === schemaUrl) {
28
19
  return true;
29
20
  }
21
+ const evaluatedPropertiesPlugin = new EvaluatedPropertiesPlugin(schemaUrl);
22
+ Validation.interpret(schemaUrl, instance, {
23
+ ...context,
24
+ plugins: [...context.ast.plugins, evaluatedPropertiesPlugin]
25
+ });
26
+ const evaluatedProperties = evaluatedPropertiesPlugin.evaluatedProperties;
30
27
 
31
28
  let isValid = true;
32
29
  for (const [propertyNameNode, property] of Instance.entries(instance)) {
33
30
  const propertyName = Instance.value(propertyNameNode);
34
- if (keywordContext.evaluatedProperties.has(propertyName)) {
31
+ if (evaluatedProperties.has(propertyName)) {
35
32
  continue;
36
33
  }
37
34
 
@@ -47,29 +44,38 @@ const interpret = ([schemaUrl, unevaluatedProperties], instance, context) => {
47
44
 
48
45
  const simpleApplicator = true;
49
46
 
50
- const unevaluatedPlugin = {
47
+ class EvaluatedPropertiesPlugin {
48
+ constructor(rootSchema) {
49
+ this.rootSchema = rootSchema;
50
+ }
51
+
51
52
  beforeSchema(_url, _instance, context) {
52
53
  context.evaluatedProperties ??= new Set();
53
54
  context.schemaEvaluatedProperties ??= new Set();
54
- },
55
- beforeKeyword(_node, _instance, context, schemaContext) {
56
- context.rootSchema = schemaContext.rootSchema;
55
+ }
56
+
57
+ beforeKeyword(_node, _instance, context) {
58
+ context.rootSchema = this.rootSchema;
57
59
  context.evaluatedProperties = new Set();
58
- },
60
+ }
61
+
59
62
  afterKeyword(_node, _instance, context, valid, schemaContext) {
60
63
  if (valid) {
61
64
  for (const property of context.evaluatedProperties) {
62
65
  schemaContext.schemaEvaluatedProperties.add(property);
63
66
  }
64
67
  }
65
- },
68
+ }
69
+
66
70
  afterSchema(_url, _instance, context, valid) {
67
71
  if (valid) {
68
72
  for (const property of context.schemaEvaluatedProperties) {
69
73
  context.evaluatedProperties.add(property);
70
74
  }
71
75
  }
76
+
77
+ this.evaluatedProperties = context.evaluatedProperties;
72
78
  }
73
- };
79
+ }
74
80
 
75
81
  export default { id, compile, interpret, simpleApplicator };
@@ -49,7 +49,7 @@ const interpret = (url, instance, context) => {
49
49
  let valid = true;
50
50
 
51
51
  for (const plugin of context.plugins) {
52
- plugin.beforeSchema(url, instance, context);
52
+ plugin.beforeSchema?.(url, instance, context);
53
53
  }
54
54
 
55
55
  if (typeof context.ast[url] === "boolean") {
@@ -64,7 +64,7 @@ const interpret = (url, instance, context) => {
64
64
  plugins: context.plugins
65
65
  };
66
66
  for (const plugin of context.plugins) {
67
- plugin.beforeKeyword(node, instance, keywordContext, context, keyword);
67
+ plugin.beforeKeyword?.(node, instance, keywordContext, context, keyword);
68
68
  }
69
69
  const isKeywordValid = keyword.interpret(keywordValue, instance, keywordContext);
70
70
  if (!isKeywordValid) {
@@ -72,13 +72,13 @@ const interpret = (url, instance, context) => {
72
72
  }
73
73
 
74
74
  for (const plugin of context.plugins) {
75
- plugin.afterKeyword(node, instance, keywordContext, isKeywordValid, context, keyword);
75
+ plugin.afterKeyword?.(node, instance, keywordContext, isKeywordValid, context, keyword);
76
76
  }
77
77
  }
78
78
  }
79
79
 
80
80
  for (const plugin of context.plugins) {
81
- plugin.afterSchema(url, instance, context, valid);
81
+ plugin.afterSchema?.(url, instance, context, valid);
82
82
  }
83
83
  return valid;
84
84
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperjump/json-schema",
3
- "version": "1.14.1",
3
+ "version": "1.15.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",