Skip to content

Commit e200e30

Browse files
committed
Replace instance with JsonNode AST
1 parent 470908a commit e200e30

File tree

471 files changed

+6111
-26115
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

471 files changed

+6111
-26115
lines changed

README.md

+57-56
Original file line numberDiff line numberDiff line change
@@ -518,28 +518,23 @@ These are available from the `@hyperjump/json-schema/experimental` export.
518518
is needed for compiling sub-schemas. The `parentSchema` parameter is
519519
primarily useful for looking up the value of an adjacent keyword that
520520
might effect this one.
521-
* interpret: (compiledKeywordValue: any, instance: InstanceDocument, ast: AST, dynamicAnchors: object, quiet: boolean) => boolean
521+
* interpret: (compiledKeywordValue: any, instance: JsonNode, ast: AST, dynamicAnchors: object, quiet: boolean, schemaLocation: string) => boolean
522522
523523
This function takes the value returned by the `compile` function and
524524
the instance value that is being validated and returns whether the
525525
value is valid or not. The other parameters are only needed for
526526
validating sub-schemas.
527-
* collectEvaluatedProperties?: (compiledKeywordValue: any, instance: InstanceDocument, ast: AST, dynamicAnchors: object) => Set\<string> | false
527+
* collectEvaluatedProperties?: (compiledKeywordValue: any, instance: JsonNode, ast: AST, dynamicAnchors: object) => Set\<string> | false
528528
529529
If the keyword is an applicator, it will need to implement this
530530
function for `unevaluatedProperties` to work as expected.
531-
* collectEvaluatedItems?: (compiledKeywordValue: A, instance: InstanceDocument, ast: AST, dynamicAnchors: object) => Set\<number> | false
531+
* collectEvaluatedItems?: (compiledKeywordValue: A, instance: JsonNode, ast: AST, dynamicAnchors: object) => Set\<number> | false
532532
533533
If the keyword is an applicator, it will need to implement this
534534
function for `unevaluatedItems` to work as expected.
535535
* collectExternalIds?: (visited: Set\<string>, parentSchema: Browser, schema: Browser) => Set\<string>
536536
If the keyword is an applicator, it will need to implement this
537537
function to work properly with the [bundle](#bundling) feature.
538-
* annotation?: (compiledKeywordValue: any) => any
539-
540-
If the keyword is an annotation, it will need to implement this
541-
function to work with the [annotation](#annotations-experimental)
542-
functions.
543538
* **defineVocabulary**: (id: string, keywords: { [keyword: string]: string }) => void
544539
545540
Define a vocabulary that maps keyword name to keyword URIs defined using
@@ -607,68 +602,69 @@ These are available from the `@hyperjump/json-schema/experimental` export.
607602
608603
Return a compiled schema. This is useful if you're creating tooling for
609604
something other than validation.
610-
* **interpret**: (schema: CompiledSchema, instance: Instance, outputFormat: OutputFormat = BASIC) => OutputUnit
605+
* **interpret**: (schema: CompiledSchema, instance: JsonNode, outputFormat: OutputFormat = BASIC) => OutputUnit
611606
612607
A curried function for validating an instance against a compiled schema.
613608
This can be useful for creating custom output formats.
614609
615-
* **OutputFormat**: **FLAG** | **BASIC** | **DETAILED** | **VERBOSE**
610+
* **OutputFormat**: **FLAG** | **BASIC**
616611
617612
In addition to the `FLAG` output format in the Stable API, the Experimental
618-
API includes support for the `BASIC`, `DETAILED`, and `VERBOSE` formats as
619-
specified in the 2019-09 specification (with some minor customizations).
620-
This implementation doesn't include annotations or human readable error
621-
messages. The output can be processed to create human readable error
622-
messages as needed.
613+
API includes support for the `BASIC` format as specified in the 2019-09
614+
specification (with some minor customizations). This implementation doesn't
615+
include annotations or human readable error messages. The output can be
616+
processed to create human readable error messages as needed.
623617
624618
## Instance API (experimental)
625619
626620
These functions are available from the
627621
`@hyperjump/json-schema/instance/experimental` export.
628622
629-
This library uses InstanceDocument objects to represent a value in an instance.
630-
You'll work with these objects if you create a custom keyword. This module is a
631-
set of functions for working with InstanceDocuments.
623+
This library uses JsonNode objects to represent instances. You'll work with
624+
these objects if you create a custom keyword.
632625
633626
This API uses generators to iterate over arrays and objects. If you like using
634627
higher order functions like `map`/`filter`/`reduce`, see
635628
[`@hyperjump/pact`](https://github.com/hyperjump-io/pact) for utilities for
636629
working with generators and async generators.
637630
638-
* **cons**: (instance: any, uri?: string) => InstanceDocument
631+
* **fromJs**: (value: any, uri?: string) => JsonNode
639632
640-
Construct an InstanceDocument from a value.
641-
* **get**: (url: string, contextDoc: InstanceDocument) => InstanceDocument
633+
Construct a JsonNode from a JavaScript value.
634+
* **get**: (url: string, instance: JsonNode) => JsonNode
642635
643-
Apply a same-resource reference to a InstanceDocument.
644-
* **uri**: (doc: InstanceDocument) => string
636+
Apply a same-resource reference to a JsonNode.
637+
* **uri**: (instance: JsonNode) => string
645638
646-
Returns a URI for the value the InstanceDocument represents.
647-
* **value**: (doc: InstanceDocument) => any
639+
Returns a URI for the value the JsonNode represents.
640+
* **value**: (instance: JsonNode) => any
648641
649-
Returns the value the InstanceDocument represents.
650-
* **has**: (key: string, doc: InstanceDocument) => any
642+
Returns the value the JsonNode represents.
643+
* **has**: (key: string, instance: JsonNode) => boolean
651644
652-
Similar to `key in instance`.
653-
* **typeOf**: (doc: InstanceDocument) => string
645+
Returns whether or not "key" is a property name in a JsonNode that
646+
represents an object.
647+
* **typeOf**: (instance: JsonNode) => string
654648
655-
Determines if the JSON type of the given doc matches the given type.
656-
* **step**: (key: string, doc: InstanceDocument) => InstanceDocument
649+
The JSON type of the JsonNode. In addition to the standard JSON types,
650+
there's also the `property` type that indicates a property name/value pair
651+
in an object.
652+
* **step**: (key: string, instance: JsonNode) => JsonType
657653
658-
Similar to `schema[key]`, but returns a InstanceDocument.
659-
* **iter**: (doc: InstanceDocument) => Generator\<InstanceDocument>
654+
Similar to indexing into a object or array using the `[]` operator.
655+
* **iter**: (instance: JsonNode) => Generator\<JsonNode>
660656
661-
Iterate over the items in the array that the SchemaDocument represents.
662-
* **entries**: (doc: InstanceDocument) => Generator\<[string, InstanceDocument]>
657+
Iterate over the items in the array that the JsonNode represents.
658+
* **entries**: (instance: JsonNode) => Generator\<[JsonNode, JsonNode]>
663659
664-
Similar to `Object.entries`, but yields InstanceDocuments for values.
665-
* **values**: (doc: InstanceDocument) => Generator\<InstanceDocument>
660+
Similar to `Object.entries`, but yields JsonNodes for keys and values.
661+
* **values**: (instance: JsonNode) => Generator\<JsonNode>
666662
667-
Similar to `Object.values`, but yields InstanceDocuments for values.
668-
* **keys**: (doc: InstanceDocument) => Generator\<string>
663+
Similar to `Object.values`, but yields JsonNodes for values.
664+
* **keys**: (instance: JsonNode) => Generator\<JsonNode>
669665
670-
Similar to `Object.keys`.
671-
* **length**: (doc: InstanceDocument) => number
666+
Similar to `Object.keys`, but yields JsonNodes for keys.
667+
* **length**: (instance: JsonNode) => number
672668
673669
Similar to `Array.prototype.length`.
674670
@@ -678,12 +674,13 @@ module provides utilities for working with JSON documents annotated with JSON
678674
Schema.
679675
680676
### Usage
681-
An annotated JSON document is represented as an AnnotatedInstance object. This
682-
object is a wrapper around your JSON document with functions that allow you to
683-
traverse the data structure and get annotations for the values within.
677+
An annotated JSON document is represented as a
678+
(JsonNode)[#/instance-api-experimental] AST. You can use this AST to traverse
679+
the data structure and get annotations for the values it represents.
684680
685681
```javascript
686-
import { annotate, annotatedWith, registerSchema } from "@hyperjump/json-schema/annotations/experimental";
682+
import { registerSchema } from "@hyperjump/json-schema/draft/2020-12";
683+
import { annotate } from "@hyperjump/json-schema/annotations/experimental";
687684
import * as AnnotatedInstance from "@hyperjump/json-schema/annotated-instance/experimental";
688685

689686

@@ -736,13 +733,14 @@ const unknowns = AnnotatedInstance.annotation(instance, "unknown", dialectId); /
736733
const types = AnnotatedInstance.annotation(instance, "type", dialectId); // => []
737734

738735
// Get the title of each of the properties in the object
739-
for (const [propertyName, propertyInstance] of AnnotatedInstance.entries(instance)) {
740-
console.log(propertyName, Instance.annotation(propertyInstance, "title", dialectId));
736+
for (const [propertyNameNode, propertyInstance] of AnnotatedInstance.entries(instance)) {
737+
const propertyName = AnnotatedInstance.value(propertyName);
738+
console.log(propertyName, AnnotatedInstance.annotation(propertyInstance, "title", dialectId));
741739
}
742740

743741
// List all locations in the instance that are deprecated
744742
for (const deprecated of AnnotatedInstance.annotatedWith(instance, "deprecated", dialectId)) {
745-
if (AnnotatedInstance.annotation(instance, "deprecated", dialectId)[0]) {
743+
if (AnnotatedInstance.annotation(deprecated, "deprecated", dialectId)[0]) {
746744
logger.warn(`The value at '${deprecated.pointer}' has been deprecated.`); // => (Example) "WARN: The value at '/name' has been deprecated."
747745
}
748746
}
@@ -752,13 +750,18 @@ for (const deprecated of AnnotatedInstance.annotatedWith(instance, "deprecated",
752750
These are available from the `@hyperjump/json-schema/annotations/experimental`
753751
export.
754752
755-
* **annotate**: (schemaUri: string, instance: any, outputFormat: OutputFormat = FLAG) => Promise\<AnnotatedInstance>
753+
* **annotate**: (schemaUri: string, instance: any, outputFormat: OutputFormat = BASIC) => Promise\<JsonNode>
756754
757755
Annotate an instance using the given schema. The function is curried to
758756
allow compiling the schema once and applying it to multiple instances. This
759757
may throw an [InvalidSchemaError](#api) if there is a problem with the
760758
schema or a ValidationError if the instance doesn't validate against the
761759
schema.
760+
* **interpret**: (compiledSchema: CompiledSchema, instance: JsonNode, outputFormat: OutputFormat = BASIC) => JsonNode
761+
762+
Annotate a JsonNode object rather than a plain JavaScript value. This might
763+
be useful when building tools on top of the annotation functionality, but
764+
you probably don't need it.
762765
* **ValidationError**: Error & { output: OutputUnit }
763766
The `output` field contains an `OutputUnit` with information about the
764767
error.
@@ -769,15 +772,13 @@ These are available from the
769772
following functions are available in addition to the functions available in the
770773
[Instance API](#instance-api-experimental).
771774
772-
* **annotation**: (instance: AnnotatedInstance, keyword: string, dialectId?: string) => any[]
775+
* **annotation**: (instance: JsonNode, keyword: string, dialect?: string): any[];
773776
774-
Get the annotations for a given keyword at the location represented by the
775-
instance object.
776-
* **annotatedWith**: (instance: AnnotatedInstance, keyword: string, dialectId?: string) => AnnotatedInstance[]
777+
Get the annotations for a keyword for the value represented by the JsonNode.
778+
* **annotatedWith**: (instance: JsonNode, keyword: string, dialect?: string): JsonNode[];
777779
778-
Get an array of instances for all the locations that are annotated with the
779-
given keyword.
780-
* **annotate**: (instance: AnnotatedInstance, keywordId: string, value: any) => AnnotatedInstance
780+
Get all JsonNodes that are annotated with the given keyword.
781+
* **setAnnotation**: (instance: JsonNode, keywordId: string, value: any) => JsonNode
781782
782783
Add an annotation to an instance. This is used internally, you probably
783784
don't need it.

annotations/annotated-instance.d.ts

+4-82
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,5 @@
1-
import type { JsonType } from "../lib/common.js";
2-
import type { Json, JsonObject } from "@hyperjump/json-pointer";
1+
export const setAnnotation: (keywordUri: string, schemaLocation: string, value: string) => void;
2+
export const annotation: <A>(instance: JsonNode, keyword: string, dialectUri?: string) => A[];
3+
export const annotatedWith: (instance: JsonNode, keyword: string, dialectUri?: string) => JsonNode[];
34

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-
};
5+
export * from "../lib/instance.js";

annotations/annotated-instance.js

+31-33
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,48 @@
1-
import { toAbsoluteIri } from "@hyperjump/uri";
2-
import { nil as nilInstance, get } from "../lib/instance.js";
1+
import * as JsonPointer from "@hyperjump/json-pointer";
2+
import * as Instance from "../lib/instance.js";
33
import { getKeywordId } from "../lib/keywords.js";
44

55

66
const defaultDialectId = "https://json-schema.org/validation";
77

8-
export const nil = { ...nilInstance, annotations: {} };
9-
export const cons = (instance, id = undefined) => ({
10-
...nil,
11-
id: id ? toAbsoluteIri(id) : "",
12-
instance,
13-
value: instance
14-
});
15-
16-
export const annotation = (instance, keyword, dialectId = defaultDialectId) => {
17-
const keywordId = getKeywordId(keyword, dialectId);
18-
return instance.annotations[instance.pointer]?.[keywordId] || [];
8+
export const setAnnotation = (node, keywordUri, schemaLocation, value) => {
9+
if (!(keywordUri in node.annotations)) {
10+
node.annotations[keywordUri] = {};
11+
}
12+
node.annotations[keywordUri][schemaLocation] = value;
1913
};
2014

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-
}
15+
export const annotation = (node, keyword, dialect = defaultDialectId) => {
16+
const keywordUri = getKeywordId(keyword, dialect);
17+
18+
let currentNode = node.root;
19+
const errors = Object.keys(node.root.errors);
20+
for (let segment of JsonPointer.pointerSegments(node.pointer)) {
21+
segment = segment === "-" && currentNode.typeOf() === "array" ? currentNode.length() : segment;
22+
currentNode = Instance.step(segment, currentNode);
23+
errors.push(...Object.keys(currentNode.errors));
24+
}
25+
26+
const annotations = [];
27+
for (const schemaLocation in node.annotations[keywordUri]) {
28+
if (!errors.some((error) => schemaLocation.startsWith(error))) {
29+
annotations.unshift(node.annotations[keywordUri][schemaLocation]);
3330
}
34-
});
31+
}
32+
33+
return annotations;
3534
};
3635

3736
export const annotatedWith = (instance, keyword, dialectId = defaultDialectId) => {
38-
const instances = [];
37+
const nodes = [];
3938

40-
const keywordId = getKeywordId(keyword, dialectId);
41-
for (const instancePointer in instance.annotations) {
42-
if (keywordId in instance.annotations[instancePointer]) {
43-
instances.push(get(`#${instancePointer}`, instance));
39+
for (const node of Instance.allNodes(instance)) {
40+
if (annotation(node, keyword, dialectId).length > 0) {
41+
nodes.push(node);
4442
}
4543
}
4644

47-
return instances;
45+
return nodes;
4846
};
4947

50-
export { get, uri, value, has, typeOf, step, iter, keys, values, entries, length } from "../lib/instance.js";
48+
export * from "../lib/instance.js";

0 commit comments

Comments
 (0)