@hyperjump/json-schema 1.3.0 → 1.4.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.
Files changed (39) hide show
  1. package/README.md +116 -5
  2. package/annotations/annotated-instance.d.ts +83 -0
  3. package/annotations/annotated-instance.js +50 -0
  4. package/annotations/index.d.ts +11 -0
  5. package/annotations/index.js +123 -0
  6. package/annotations/tests/applicators.json +375 -0
  7. package/annotations/tests/content.json +60 -0
  8. package/annotations/tests/core.json +33 -0
  9. package/annotations/tests/format.json +21 -0
  10. package/annotations/tests/meta-data.json +135 -0
  11. package/annotations/tests/unevaluated.json +557 -0
  12. package/annotations/tests/unknown.json +91 -0
  13. package/annotations/tests/validation.json +328 -0
  14. package/annotations/validation-error.d.ts +8 -0
  15. package/annotations/validation-error.js +7 -0
  16. package/bundle/index.js +2 -3
  17. package/lib/keywords/additionalProperties.js +19 -4
  18. package/lib/keywords/allOf.js +2 -2
  19. package/lib/keywords/anyOf.js +2 -2
  20. package/lib/keywords/contains.js +14 -14
  21. package/lib/keywords/contentSchema.js +7 -2
  22. package/lib/keywords/dependentSchemas.js +18 -8
  23. package/lib/keywords/dynamicRef.js +2 -2
  24. package/lib/keywords/else.js +12 -19
  25. package/lib/keywords/if.js +2 -2
  26. package/lib/keywords/items.js +3 -3
  27. package/lib/keywords/not.js +1 -1
  28. package/lib/keywords/oneOf.js +2 -2
  29. package/lib/keywords/patternProperties.js +20 -3
  30. package/lib/keywords/prefixItems.js +3 -3
  31. package/lib/keywords/properties.js +18 -6
  32. package/lib/keywords/propertyDependencies.js +2 -2
  33. package/lib/keywords/propertyNames.js +1 -1
  34. package/lib/keywords/then.js +12 -19
  35. package/lib/keywords/unevaluatedItems.js +2 -2
  36. package/lib/keywords/unevaluatedProperties.js +25 -5
  37. package/lib/keywords/validation.js +43 -53
  38. package/lib/keywords.js +1 -1
  39. package/package.json +4 -2
package/README.md CHANGED
@@ -15,6 +15,7 @@ A collection of modules for working with JSON Schemas.
15
15
  * Uses the process defined in the 2020-12 specification but works with any
16
16
  dialect.
17
17
  * Provides utilities for building non-validation JSON Schema tooling
18
+ * Provides utilities for working with annotations
18
19
 
19
20
  ## Install
20
21
  Includes support for node.js (ES Modules, TypeScript) and browsers.
@@ -238,11 +239,11 @@ The following types are used in the above definitions
238
239
 
239
240
  ## Bundling
240
241
  ### Usage
241
- You can bundle schemas with external references into single deliverable using
242
+ You can bundle schemas with external references into a single deliverable using
242
243
  the official JSON Schema bundling process introduced in the 2020-12
243
244
  specification. Given a schema with external references, any external schemas
244
245
  will be embedded in the schema resulting in a Compound Schema Document with all
245
- the schemas necessary to evaluate the given schema in one document.
246
+ the schemas necessary to evaluate the given schema in a single JSON document.
246
247
 
247
248
  The bundling process allows schemas to be embedded without needing to modify any
248
249
  references which means you get the same output details whether you validate the
@@ -374,9 +375,9 @@ addKeyword({
374
375
  return Schema.map(async (itemSchema) => Validation.compile(await itemSchema, ast), schema);
375
376
  },
376
377
 
377
- interpret: (implies, instance, ast, dynamicAnchors) => {
378
+ interpret: (implies, instance, ast, dynamicAnchors, quiet) => {
378
379
  return implies.reduce((acc, schema) => {
379
- return !acc || Validation.interpret(schema, instance, ast, dynamicAnchors);
380
+ return !acc || Validation.interpret(schema, instance, ast, dynamicAnchors, quiet);
380
381
  }, true);
381
382
  }
382
383
  });
@@ -518,7 +519,7 @@ These are available from the `@hyperjump/json-schema/experimental` export.
518
519
  needed for compiling sub-schemas. The `parentSchema` parameter is
519
520
  primarily useful for looking up the value of an adjacent keyword that
520
521
  might effect this one.
521
- * interpret: (compiledKeywordValue: A, instance: JsonDocument, ast: AST, dynamicAnchors: Anchors) => boolean
522
+ * interpret: (compiledKeywordValue: A, instance: JsonDocument, ast: AST, dynamicAnchors: Anchors, quiet: boolean) => boolean
522
523
 
523
524
  This function takes the value returned by the `compile` function and the
524
525
  instance value that is being validated and returns whether the value is
@@ -643,6 +644,116 @@ set of functions for working with InstanceDocuments.
643
644
 
644
645
  Similar to `Array.prototype.length`.
645
646
 
647
+ ## Annotations (Experimental)
648
+ JSON Schema is for annotating JSON instances as well as validating them. This
649
+ module provides utilities for working with JSON documents annotated with JSON
650
+ Schema.
651
+
652
+ ### Usage
653
+ An annotated JSON document is represented as an AnnotatedInstance object. This
654
+ object is a wrapper around your JSON document with functions that allow you to
655
+ traverse the data structure and get annotations for the values within.
656
+
657
+ ```javascript
658
+ import { annotate, annotatedWith, addSchema } from "@hyperjump/json-schema/annotations/experimental";
659
+ import * as AnnotatedInstance from "@hyperjump/json-schema/annotated-instance/experimental";
660
+
661
+
662
+ const schemaId = "https://example.com/foo";
663
+ const dialectId = "https://json-schema.org/draft/2020-12/schema";
664
+
665
+ addSchema({
666
+ "$schema": dialectId,
667
+
668
+ "title": "Person",
669
+ "unknown": "foo",
670
+
671
+ "type": "object",
672
+ "properties": {
673
+ "name": {
674
+ "$ref": "#/$defs/name",
675
+ "deprecated": true
676
+ },
677
+ "givenName": {
678
+ "$ref": "#/$defs/name",
679
+ "title": "Given Name"
680
+ },
681
+ "familyName": {
682
+ "$ref": "#/$defs/name",
683
+ "title": "Family Name"
684
+ }
685
+ },
686
+
687
+ "$defs": {
688
+ "name": {
689
+ "type": "string",
690
+ "title": "Name"
691
+ }
692
+ }
693
+ }, schemaId);
694
+
695
+ const instance = await annotate(schemaId, {
696
+ name: "Jason Desrosiers",
697
+ givenName: "Jason",
698
+ familyName: "Desrosiers"
699
+ });
700
+
701
+ // Get the title of the instance
702
+ const titles = AnnotatedInstance.annotation(instance, "title", dialectId); // => ["Person"]
703
+
704
+ // Unknown keywords are collected as annotations
705
+ const unknowns = AnnotatedInstance.annotation(instance, "unknown", dialectId); // => ["foo"]
706
+
707
+ // The type keyword doesn't produce annotations
708
+ const types = AnnotatedInstance.annotation(instance, "type", dialectId); // => []
709
+
710
+ // Get the title of each of the properties in the object
711
+ AnnotatedInstance.entries(instance)
712
+ .map(([propertyName, propertyInstance]) => {
713
+ return { propertyName, titles: Instance.annotation(propertyInstance, "title", dialectId) }; // => (Example) { propertyName: "givenName", titles: ["Given Name", "Name"] });
714
+
715
+ // List all locations in the instance that are deprecated
716
+ for (const deprecated of AnnotatedInstance.annotatedWith(instance, "deprecated", dialectId)) {
717
+ if (AnnotatedInstance.annotation(instance, "deprecated", dialectId)[0]) {
718
+ logger.warn(`The value at '${deprecated.pointer}' has been deprecated.`); // => (Example) "WARN: The value at '/name' has been deprecated."
719
+ }
720
+ }
721
+ ```
722
+
723
+ ### API
724
+ These are available from the `@hyperjump/json-schema/annotations/experimental`
725
+ export.
726
+
727
+ * **annotate**: (schemaUri: string, instance: any, outputFormat: OutputFormat = FLAG) => Promise<AnnotatedInstance>
728
+
729
+ Annotate an instance using the given schema. The function is curried to
730
+ allow compiling the schema once and applying it to multiple instances. This
731
+ may throw an [InvalidSchemaError](#api) if there is a problem with the
732
+ schema or a ValidationError if the instance doesn't validate against the
733
+ schema.
734
+ * **ValidationError**:
735
+ output: OutputUnit -- The errors that were found while validating the
736
+ instance.
737
+
738
+ ### AnnotatedInstance API
739
+ These are available from the
740
+ `@hyperjump/json-schema/annotated-instance/experimental` export. The
741
+ following functions are available in addition to the functions available in the
742
+ [Instance API](#instance-api).
743
+
744
+ * **annotation**: (instance: AnnotatedInstance, keyword: string, dialectId?: string) => [any]
745
+
746
+ Get the annotations for a given keyword at the location represented by the
747
+ instance object.
748
+ * **annotatedWith**: (instance: AnnotatedInstance, keyword: string, dialectId?: string) => [AnnotatedInstance]
749
+
750
+ Get an array of instances for all the locations that are annotated with the
751
+ given keyword.
752
+ * **annotate**: (instance: AnnotatedInstance, keywordId: string, value: any) => AnnotatedInstance
753
+
754
+ Add an annotation to an instance. This is used internally, you probably
755
+ don't need it.
756
+
646
757
  ## Low-level Utilities (Experimental)
647
758
  ### API
648
759
  These are available from the `@hyperjump/json-schema/experimental` export.
@@ -0,0 +1,83 @@
1
+ import type { JsonType } from "../lib/common.js";
2
+ import type { Json, JsonObject } from "@hyperjump/json-pointer";
3
+
4
+
5
+ export const annotate: (instance: AnnotatedJsonDocument, keyword: string, value: string) => AnnotatedJsonDocument;
6
+ export const annotation: <A>(instance: AnnotatedJsonDocument, keyword: string, dialectId?: string) => A[];
7
+ export const annotatedWith: (instance: AnnotatedJsonDocument, keyword: string, dialectId?: string) => AnnotatedJsonDocument[];
8
+ export const nil: AnnotatedJsonDocument<undefined>;
9
+ export const cons: (instance: Json, id?: string) => AnnotatedJsonDocument;
10
+ export const get: (uri: string, context?: AnnotatedJsonDocument) => AnnotatedJsonDocument;
11
+ export const uri: (doc: AnnotatedJsonDocument) => string;
12
+ export const value: <A extends Json>(doc: AnnotatedJsonDocument<A>) => A;
13
+ export const has: (key: string, doc: AnnotatedJsonDocument<JsonObject>) => boolean;
14
+ export const typeOf: (
15
+ (doc: AnnotatedJsonDocument, type: "null") => doc is AnnotatedJsonDocument<null>
16
+ ) & (
17
+ (doc: AnnotatedJsonDocument, type: "boolean") => doc is AnnotatedJsonDocument<boolean>
18
+ ) & (
19
+ (doc: AnnotatedJsonDocument, type: "object") => doc is AnnotatedJsonDocument<JsonObject>
20
+ ) & (
21
+ (doc: AnnotatedJsonDocument, type: "array") => doc is AnnotatedJsonDocument<Json[]>
22
+ ) & (
23
+ (doc: AnnotatedJsonDocument, type: "number" | "integer") => doc is AnnotatedJsonDocument<number>
24
+ ) & (
25
+ (doc: AnnotatedJsonDocument, type: "string") => doc is AnnotatedJsonDocument<string>
26
+ ) & (
27
+ (doc: AnnotatedJsonDocument, type: JsonType) => boolean
28
+ ) & (
29
+ (doc: AnnotatedJsonDocument) => (type: JsonType) => boolean
30
+ );
31
+ export const step: (key: string, doc: AnnotatedJsonDocument<JsonObject | Json[]>) => AnnotatedJsonDocument<typeof doc.value>;
32
+ export const entries: (doc: AnnotatedJsonDocument<JsonObject>) => [string, AnnotatedJsonDocument][];
33
+ export const keys: (doc: AnnotatedJsonDocument<JsonObject>) => string[];
34
+ export const map: (
35
+ <A>(fn: MapFn<A>, doc: AnnotatedJsonDocument<Json[]>) => A[]
36
+ ) & (
37
+ <A>(fn: MapFn<A>) => (doc: AnnotatedJsonDocument<Json[]>) => A[]
38
+ );
39
+ export const forEach: (
40
+ (fn: ForEachFn, doc: AnnotatedJsonDocument<Json[]>) => void
41
+ ) & (
42
+ (fn: ForEachFn) => (doc: AnnotatedJsonDocument<Json[]>) => void
43
+ );
44
+ export const filter: (
45
+ (fn: FilterFn, doc: AnnotatedJsonDocument<Json[]>) => AnnotatedJsonDocument[]
46
+ ) & (
47
+ (fn: FilterFn) => (doc: AnnotatedJsonDocument<Json[]>) => AnnotatedJsonDocument[]
48
+ );
49
+ export const reduce: (
50
+ <A>(fn: ReduceFn<A>, acc: A, doc: AnnotatedJsonDocument<Json[]>) => A
51
+ ) & (
52
+ <A>(fn: ReduceFn<A>) => (acc: A, doc: AnnotatedJsonDocument<Json[]>) => A
53
+ ) & (
54
+ <A>(fn: ReduceFn<A>) => (acc: A) => (doc: AnnotatedJsonDocument<Json[]>) => A
55
+ );
56
+ export const every: (
57
+ (fn: FilterFn, doc: AnnotatedJsonDocument<Json[]>) => boolean
58
+ ) & (
59
+ (fn: FilterFn) => (doc: AnnotatedJsonDocument<Json[]>) => boolean
60
+ );
61
+ export const some: (
62
+ (fn: FilterFn, doc: AnnotatedJsonDocument<Json[]>) => boolean
63
+ ) & (
64
+ (fn: FilterFn) => (doc: AnnotatedJsonDocument<Json[]>) => boolean
65
+ );
66
+ export const length: (doc: AnnotatedJsonDocument<Json[] | string>) => number;
67
+
68
+ type MapFn<A> = (element: AnnotatedJsonDocument, index: number) => A;
69
+ type ForEachFn = (element: AnnotatedJsonDocument, index: number) => void;
70
+ type FilterFn = (element: AnnotatedJsonDocument, index: number) => boolean;
71
+ type ReduceFn<A> = (accumulator: A, currentValue: AnnotatedJsonDocument, index: number) => A;
72
+
73
+ export type AnnotatedJsonDocument<A extends Json | undefined = Json> = {
74
+ id: string;
75
+ pointer: string;
76
+ instance: Json;
77
+ value: A;
78
+ annotations: {
79
+ [pointer: string]: {
80
+ [keyword: string]: unknown[]
81
+ }
82
+ }
83
+ };
@@ -0,0 +1,50 @@
1
+ import { toAbsoluteUri } from "../lib/common.js";
2
+ import { nil as nilInstance, get } from "../lib/instance.js";
3
+ import { getKeywordId } from "../lib/keywords.js";
4
+
5
+
6
+ const defaultDialectId = "https://json-schema.org/validation";
7
+
8
+ export const nil = { ...nilInstance, annotations: {} };
9
+ export const cons = (instance, id = undefined) => ({
10
+ ...nil,
11
+ id: id ? toAbsoluteUri(id) : "",
12
+ instance,
13
+ value: instance
14
+ });
15
+
16
+ export const annotation = (instance, keyword, dialectId = defaultDialectId) => {
17
+ const keywordId = getKeywordId(dialectId, keyword);
18
+ return instance.annotations?.[instance.pointer]?.[keywordId] || [];
19
+ };
20
+
21
+ export const annotate = (instance, keyword, value) => {
22
+ return Object.freeze({
23
+ ...instance,
24
+ annotations: {
25
+ ...instance.annotations,
26
+ [instance.pointer]: {
27
+ ...instance.annotations[instance.pointer],
28
+ [keyword]: [
29
+ value,
30
+ ...instance.annotations[instance.pointer]?.[keyword] || []
31
+ ]
32
+ }
33
+ }
34
+ });
35
+ };
36
+
37
+ export const annotatedWith = (instance, keyword, dialectId = defaultDialectId) => {
38
+ const instances = [];
39
+
40
+ const keywordId = getKeywordId(dialectId, keyword);
41
+ for (const instancePointer in instance.annotations) {
42
+ if (keywordId in instance.annotations[instancePointer]) {
43
+ instances.push(get(`#${instancePointer}`, instance));
44
+ }
45
+ }
46
+
47
+ return instances;
48
+ };
49
+
50
+ export * from "../lib/instance.js";
@@ -0,0 +1,11 @@
1
+ import type { OutputFormat } from "../lib/core.js";
2
+ import type { AnnotatedJsonDocument } from "./annotated-instance.js";
3
+
4
+
5
+ export const annotate: (
6
+ (schemaUrl: string, value: unknown, outputFormat?: OutputFormat) => Promise<Annotator>
7
+ ) & (
8
+ (schemaUrl: string) => Promise<Annotator>
9
+ );
10
+
11
+ export type Annotator = (value: unknown, outputFormat?: OutputFormat) => AnnotatedJsonDocument;
@@ -0,0 +1,123 @@
1
+ import { subscribe, unsubscribe } from "../lib/pubsub.js";
2
+ import { compile, interpret as validate, BASIC } from "../lib/core.js";
3
+ import { getKeyword } from "../lib/keywords.js";
4
+ import * as Instance from "./annotated-instance.js";
5
+ import { ValidationError } from "./validation-error.js";
6
+
7
+
8
+ export const annotate = async (schemaUri, json = undefined, outputFormat = undefined) => {
9
+ loadKeywordSupport();
10
+ const compiled = await compile(schemaUri);
11
+ const interpretAst = (json, outputFormat) => interpret(compiled, Instance.cons(json), outputFormat);
12
+
13
+ return json === undefined ? interpretAst : interpretAst(json, outputFormat);
14
+ };
15
+
16
+ const interpret = ({ ast, schemaUri }, instance, outputFormat = BASIC) => {
17
+ const output = [instance];
18
+ const subscriptionToken = subscribe("result", outputHandler(output));
19
+
20
+ try {
21
+ const result = validate({ ast, schemaUri }, instance, outputFormat);
22
+ if (!result.valid) {
23
+ throw new ValidationError(result);
24
+ }
25
+ } finally {
26
+ unsubscribe("result", subscriptionToken);
27
+ }
28
+
29
+ return output[0];
30
+ };
31
+
32
+ const outputHandler = (output) => {
33
+ let isPassing = true;
34
+ const instanceStack = [];
35
+
36
+ return (message, resultNode) => {
37
+ if (message === "result.start") {
38
+ instanceStack.push(output[0]);
39
+ isPassing = true;
40
+ } else if (message === "result" && isPassing) {
41
+ output[0] = Instance.get(resultNode.instanceLocation, output[0]);
42
+
43
+ if (resultNode.valid) {
44
+ const keywordHandler = getKeyword(resultNode.keyword);
45
+ if (keywordHandler?.annotation) {
46
+ const annotation = keywordHandler.annotation(resultNode.ast);
47
+ output[0] = Instance.annotate(output[0], resultNode.keyword, annotation);
48
+ }
49
+ } else {
50
+ output[0] = instanceStack[instanceStack.length - 1];
51
+ isPassing = false;
52
+ }
53
+ } else if (message === "result.end") {
54
+ instanceStack.pop();
55
+ }
56
+ };
57
+ };
58
+
59
+ const identity = (a) => a;
60
+
61
+ const loadKeywordSupport = () => {
62
+ const title = getKeyword("https://json-schema.org/keyword/title");
63
+ if (title) {
64
+ title.annotation = identity;
65
+ }
66
+
67
+ const description = getKeyword("https://json-schema.org/keyword/description");
68
+ if (description) {
69
+ description.annotation = identity;
70
+ }
71
+
72
+ const _default = getKeyword("https://json-schema.org/keyword/default");
73
+ if (_default) {
74
+ _default.annotation = identity;
75
+ }
76
+
77
+ const deprecated = getKeyword("https://json-schema.org/keyword/deprecated");
78
+ if (deprecated) {
79
+ deprecated.annotation = identity;
80
+ }
81
+
82
+ const readOnly = getKeyword("https://json-schema.org/keyword/readOnly");
83
+ if (readOnly) {
84
+ readOnly.annotation = identity;
85
+ }
86
+
87
+ const writeOnly = getKeyword("https://json-schema.org/keyword/writeOnly");
88
+ if (writeOnly) {
89
+ writeOnly.annotation = identity;
90
+ }
91
+
92
+ const examples = getKeyword("https://json-schema.org/keyword/examples");
93
+ if (examples) {
94
+ examples.annotation = identity;
95
+ }
96
+
97
+ const format = getKeyword("https://json-schema.org/keyword/format");
98
+ if (format) {
99
+ format.annotation = identity;
100
+ }
101
+
102
+ const contentMediaType = getKeyword("https://json-schema.org/keyword/contentMediaType");
103
+ if (contentMediaType) {
104
+ contentMediaType.annotation = identity;
105
+ }
106
+
107
+ const contentEncoding = getKeyword("https://json-schema.org/keyword/contentEncoding");
108
+ if (contentEncoding) {
109
+ contentEncoding.annotation = identity;
110
+ }
111
+
112
+ const contentSchema = getKeyword("https://json-schema.org/keyword/contentSchema");
113
+ if (contentSchema) {
114
+ contentSchema.annotation = identity;
115
+ }
116
+
117
+ const unknown = getKeyword("https://json-schema.org/keyword/unknown");
118
+ if (unknown) {
119
+ unknown.annotation = identity;
120
+ }
121
+ };
122
+
123
+ export { ValidationError } from "./validation-error.js";