@hyperjump/json-schema 1.14.0 → 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 +67 -8
- package/annotations/index.d.ts +4 -4
- package/annotations/index.js +15 -29
- package/annotations/test-utils.d.ts +1 -0
- package/annotations/test-utils.js +38 -0
- package/draft-04/dependencies.js +7 -7
- package/draft-2020-12/dynamicRef.js +0 -4
- package/lib/core.js +16 -10
- package/lib/evaluation-plugins/annotations.js +10 -5
- package/lib/evaluation-plugins/basic-output.js +10 -5
- package/lib/evaluation-plugins/detailed-output.js +10 -5
- package/lib/experimental.d.ts +24 -13
- package/lib/experimental.js +3 -3
- package/lib/index.d.ts +8 -2
- package/lib/keywords/contentSchema.js +6 -3
- package/lib/keywords/dynamicRef.js +0 -4
- package/lib/keywords/unevaluatedItems.js +24 -18
- package/lib/keywords/unevaluatedProperties.js +24 -18
- package/lib/keywords/validation.js +4 -4
- package/package.json +1 -1
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
|
package/annotations/index.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
13
|
+
export type Annotator = (value: Json, options?: OutputFormat | ValidationOptions) => JsonNode;
|
|
14
14
|
|
|
15
|
-
export const interpret: (compiledSchema: CompiledSchema, value: 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;
|
package/annotations/index.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
15
|
+
const interpretAst = (json, options) => interpret(compiled, Instance.fromJs(json), options);
|
|
20
16
|
|
|
21
|
-
return json === undefined ? interpretAst : interpretAst(json,
|
|
17
|
+
return json === undefined ? interpretAst : interpretAst(json, options);
|
|
22
18
|
};
|
|
23
19
|
|
|
24
|
-
export const interpret = (
|
|
25
|
-
const
|
|
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
|
|
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
|
-
|
|
44
|
-
throw new ValidationError(result);
|
|
29
|
+
if (!output.valid) {
|
|
30
|
+
throw new ValidationError(output);
|
|
45
31
|
}
|
|
46
32
|
|
|
47
|
-
for (const annotation of
|
|
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]) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const isCompatible: (compatibility: string | undefined, versionUnderTest: number) => boolean;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const isCompatible = (compatibility, versionUnderTest) => {
|
|
2
|
+
if (compatibility === undefined) {
|
|
3
|
+
return true;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const constraints = compatibility.split(",");
|
|
7
|
+
for (const constraint of constraints) {
|
|
8
|
+
const matches = /(?<operator><=|>=|=)?(?<version>\d+)/.exec(constraint);
|
|
9
|
+
if (!matches) {
|
|
10
|
+
throw Error(`Invalid compatibility string: ${compatibility}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const operator = matches[1] ?? ">=";
|
|
14
|
+
const version = parseInt(matches[2], 10);
|
|
15
|
+
|
|
16
|
+
switch (operator) {
|
|
17
|
+
case ">=":
|
|
18
|
+
if (versionUnderTest < version) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
break;
|
|
22
|
+
case "<=":
|
|
23
|
+
if (versionUnderTest > version) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
break;
|
|
27
|
+
case "=":
|
|
28
|
+
if (versionUnderTest !== version) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
default:
|
|
33
|
+
throw Error(`Unsupported contraint operator: ${operator}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return true;
|
|
38
|
+
};
|
package/draft-04/dependencies.js
CHANGED
|
@@ -16,21 +16,21 @@ const compile = (schema, ast) => pipe(
|
|
|
16
16
|
);
|
|
17
17
|
|
|
18
18
|
const interpret = (dependencies, instance, context) => {
|
|
19
|
-
|
|
19
|
+
if (Instance.typeOf(instance) !== "object") {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
20
22
|
|
|
21
|
-
return
|
|
22
|
-
if (!(propertyName
|
|
23
|
+
return dependencies.every(([propertyName, dependency]) => {
|
|
24
|
+
if (!Instance.has(propertyName, instance)) {
|
|
23
25
|
return true;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
if (Array.isArray(dependency)) {
|
|
27
|
-
return dependency.every((key) => key
|
|
29
|
+
return dependency.every((key) => Instance.has(key, instance));
|
|
28
30
|
} else {
|
|
29
31
|
return Validation.interpret(dependency, instance, context);
|
|
30
32
|
}
|
|
31
33
|
});
|
|
32
34
|
};
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export default { id, compile, interpret, simpleApplicator };
|
|
36
|
+
export default { id, compile, interpret };
|
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 {
|
|
15
|
-
import {
|
|
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,
|
|
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,
|
|
24
|
+
const interpretAst = (value, options) => interpret(compiled, Instance.fromJs(value), options);
|
|
25
25
|
|
|
26
|
-
return value === undefined ? interpretAst : interpretAst(value,
|
|
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,
|
|
36
|
-
const
|
|
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
|
-
|
|
46
|
+
outputPlugin = new BasicOutputPlugin();
|
|
47
|
+
context.plugins.push(outputPlugin);
|
|
43
48
|
break;
|
|
44
49
|
case DETAILED:
|
|
45
|
-
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|
package/lib/experimental.d.ts
CHANGED
|
@@ -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:
|
|
73
|
-
simpleApplicator
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
103
|
+
export class AnnotationsPlugin implements EvaluationPlugin<AnnotationsContext> {
|
|
104
|
+
annotations: OutputUnit[];
|
|
105
|
+
}
|
|
106
|
+
|
|
96
107
|
export type AnnotationsContext = {
|
|
97
108
|
annotations: OutputUnit[];
|
|
98
109
|
};
|
package/lib/experimental.js
CHANGED
|
@@ -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
|
|
10
|
-
export
|
|
11
|
-
export
|
|
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,
|
|
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,
|
|
31
|
+
export type Validator = (value: Json, options?: OutputFormat | ValidationOptions) => OutputUnit;
|
|
26
32
|
|
|
27
33
|
export type OutputUnit = {
|
|
28
34
|
keyword: string;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { canonicalUri } from "../schema.js";
|
|
2
1
|
import * as Browser from "@hyperjump/browser";
|
|
3
2
|
import * as Instance from "../instance.js";
|
|
4
3
|
import { getKeywordName } from "../keywords.js";
|
|
@@ -9,13 +8,17 @@ const id = "https://json-schema.org/keyword/contentSchema";
|
|
|
9
8
|
const compile = async (contentSchema, _ast, parentSchema) => {
|
|
10
9
|
const contentMediaTypeKeyword = getKeywordName(contentSchema.document.dialectId, "https://json-schema.org/keyword/contentMediaType");
|
|
11
10
|
const contentMediaType = await Browser.step(contentMediaTypeKeyword, parentSchema);
|
|
12
|
-
|
|
11
|
+
if (Browser.value(contentMediaType) === undefined) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return Browser.value(contentSchema);
|
|
13
16
|
};
|
|
14
17
|
|
|
15
18
|
const interpret = () => true;
|
|
16
19
|
|
|
17
20
|
const annotation = (contentSchema, instance) => {
|
|
18
|
-
if (Instance.typeOf(instance) !== "string") {
|
|
21
|
+
if (!contentSchema || Instance.typeOf(instance) !== "string") {
|
|
19
22
|
return;
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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