@hyperjump/json-schema 1.5.0 → 1.5.2

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
@@ -18,7 +18,9 @@ A collection of modules for working with JSON Schemas.
18
18
  * Provides utilities for working with annotations
19
19
 
20
20
  ## Install
21
- Includes support for node.js (ES Modules, TypeScript) and browsers.
21
+ Includes support for node.js (ES Modules, TypeScript) and browsers (works with
22
+ CSP
23
+ [`unsafe-eval`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#unsafe_eval_expressions)).
22
24
 
23
25
  ### Node.js
24
26
  ```bash
package/lib/core.js CHANGED
@@ -90,9 +90,15 @@ subscribe("validate.metaValidate", async (message, schema) => {
90
90
  const isDynamicRefEnabled = isExperimentalKeywordEnabled(dyanmicRefKeywordId);
91
91
  setExperimentalKeywordEnabled(dyanmicRefKeywordId, true);
92
92
 
93
+ // itemPattern is experimental, but is necessary for meta-validation
94
+ const itemPatternKeywordId = "https://json-schema.org/keyword/itemPattern";
95
+ const isItemPatternEnabled = isExperimentalKeywordEnabled(itemPatternKeywordId);
96
+ setExperimentalKeywordEnabled(itemPatternKeywordId, true);
97
+
93
98
  const compiledSchema = await compile(schema.dialectId);
94
99
  metaValidators[schema.dialectId] = interpret(compiledSchema);
95
100
 
101
+ setExperimentalKeywordEnabled(itemPatternKeywordId, isItemPatternEnabled);
96
102
  setExperimentalKeywordEnabled(dyanmicRefKeywordId, isDynamicRefEnabled);
97
103
  }
98
104
 
package/lib/index.js CHANGED
@@ -5,6 +5,7 @@ import additionalProperties from "./keywords/additionalProperties.js";
5
5
  import allOf from "./keywords/allOf.js";
6
6
  import anchor from "./keywords/anchor.js";
7
7
  import anyOf from "./keywords/anyOf.js";
8
+ import conditional from "./keywords/conditional.js";
8
9
  import const_ from "./keywords/const.js";
9
10
  import contains from "./keywords/contains.js";
10
11
  import comment from "./keywords/comment.js";
@@ -27,6 +28,7 @@ import exclusiveMinimum from "./keywords/exclusiveMinimum.js";
27
28
  import format from "./keywords/format.js";
28
29
  import id from "./keywords/id.js";
29
30
  import if_ from "./keywords/if.js";
31
+ import itemPattern from "./keywords/itemPattern.js";
30
32
  import items from "./keywords/items.js";
31
33
  import maxContains from "./keywords/maxContains.js";
32
34
  import maxItems from "./keywords/maxItems.js";
@@ -74,6 +76,7 @@ addKeyword(additionalProperties);
74
76
  addKeyword(allOf);
75
77
  addKeyword(anchor);
76
78
  addKeyword(anyOf);
79
+ addKeyword(conditional);
77
80
  addKeyword(const_);
78
81
  addKeyword(contains);
79
82
  addKeyword(comment);
@@ -120,6 +123,7 @@ addKeyword(readOnly);
120
123
  addKeyword(ref);
121
124
  addKeyword(requireAllExcept);
122
125
  addKeyword(required);
126
+ addKeyword(itemPattern);
123
127
  addKeyword(title);
124
128
  addKeyword(then);
125
129
  addKeyword(type);
@@ -1,4 +1,4 @@
1
- import { filter, every, pipe } from "@hyperjump/pact";
1
+ import { concat, join, empty, map, filter, every, pipe, tap } from "@hyperjump/pact";
2
2
  import * as Schema from "../schema.js";
3
3
  import * as Instance from "../instance.js";
4
4
  import { getKeywordName } from "../keywords.js";
@@ -8,26 +8,24 @@ import Validation from "./validation.js";
8
8
  const id = "https://json-schema.org/keyword/additionalProperties";
9
9
 
10
10
  const compile = async (schema, ast, parentSchema) => {
11
- const patterns = [];
12
-
13
11
  const propertiesKeyword = getKeywordName(schema.dialectId, "https://json-schema.org/keyword/properties");
14
12
  const propertiesSchema = await Schema.step(propertiesKeyword, parentSchema);
15
- if (Schema.typeOf(propertiesSchema, "object")) {
16
- for (const name of Schema.keys(propertiesSchema)) {
17
- patterns.push(regexEscape(name));
18
- }
19
- }
13
+ const propertyPatterns = Schema.typeOf(propertiesSchema, "object")
14
+ ? map((propertyName) => "^" + regexEscape(propertyName) + "$", Schema.keys(propertiesSchema))
15
+ : empty();
20
16
 
21
17
  const patternPropertiesKeyword = getKeywordName(schema.dialectId, "https://json-schema.org/keyword/patternProperties");
22
18
  const patternProperties = await Schema.step(patternPropertiesKeyword, parentSchema);
23
- if (Schema.typeOf(patternProperties, "object")) {
24
- patterns.push(...Schema.keys(patternProperties));
25
- }
19
+ const patternPropertyPatterns = Schema.typeOf(patternProperties, "object")
20
+ ? Schema.keys(patternProperties)
21
+ : empty();
22
+
23
+ const pattern = pipe(
24
+ concat(propertyPatterns, patternPropertyPatterns),
25
+ join("|")
26
+ ) || "(?!)";
26
27
 
27
- return [
28
- new RegExp(patterns.length > 0 ? patterns.join("|") : "(?!)", "u"),
29
- await Validation.compile(schema, ast)
30
- ];
28
+ return [new RegExp(pattern, "u"), await Validation.compile(schema, ast)];
31
29
  };
32
30
 
33
31
  const regexEscape = (string) => string
@@ -0,0 +1,65 @@
1
+ import { pipe, asyncMap, asyncCollectArray } from "@hyperjump/pact";
2
+ import * as Schema from "../schema.js";
3
+ import Validation from "./validation.js";
4
+
5
+
6
+ const id = "https://json-schema.org/keyword/conditional";
7
+ const experimental = true;
8
+
9
+ const compile = (schema, ast) => pipe(
10
+ Schema.iter(schema),
11
+ schemaFlatten,
12
+ asyncMap((subSchema) => Validation.compile(subSchema, ast)),
13
+ asyncCollectArray
14
+ );
15
+
16
+ const interpret = (conditional, instance, ast, dynamicAnchors, quiet) => {
17
+ for (let index = 0; index < conditional.length; index += 2) {
18
+ const isValid = Validation.interpret(conditional[index], instance, ast, dynamicAnchors, quiet);
19
+ if (index + 1 === conditional.length) {
20
+ return isValid;
21
+ } else if (isValid) {
22
+ return Validation.interpret(conditional[index + 1], instance, ast, dynamicAnchors, quiet);
23
+ }
24
+ }
25
+
26
+ return true;
27
+ };
28
+
29
+ const collectEvaluatedProperties = (conditional, instance, ast, dynamicAnchors) => {
30
+ for (let index = 0; index < conditional.length; index += 2) {
31
+ const unevaluatedProperties = Validation.collectEvaluatedProperties(conditional[index], instance, ast, dynamicAnchors);
32
+ if (index + 1 === conditional.length) {
33
+ return unevaluatedProperties;
34
+ } else if (unevaluatedProperties !== false) {
35
+ return Validation.collectEvaluatedProperties(conditional[index + 1], instance, ast, dynamicAnchors);
36
+ }
37
+ }
38
+
39
+ return new Set();
40
+ };
41
+
42
+ const collectEvaluatedItems = (conditional, instance, ast, dynamicAnchors) => {
43
+ for (let index = 0; index < conditional.length; index += 2) {
44
+ const unevaluatedItems = Validation.collectEvaluatedItems(conditional[index], instance, ast, dynamicAnchors);
45
+ if (index + 1 === conditional.length) {
46
+ return unevaluatedItems;
47
+ } else if (unevaluatedItems !== false) {
48
+ return Validation.collectEvaluatedItems(conditional[index + 1], instance, ast, dynamicAnchors);
49
+ }
50
+ }
51
+
52
+ return new Set();
53
+ };
54
+
55
+ const schemaFlatten = async function* (iter, depth = 1) {
56
+ for await (const n of iter) {
57
+ if (depth > 0 && Schema.typeOf(n, "array")) {
58
+ yield* schemaFlatten(Schema.iter(n), depth - 1);
59
+ } else {
60
+ yield n;
61
+ }
62
+ }
63
+ };
64
+
65
+ export default { id, experimental, compile, interpret, collectEvaluatedProperties, collectEvaluatedItems };
@@ -1,4 +1,4 @@
1
- import { pipe, map, filter, reduce, collectSet, zip, range } from "@hyperjump/pact";
1
+ import { pipe, map, filter, count, collectSet, zip, range } from "@hyperjump/pact";
2
2
  import * as Schema from "../schema.js";
3
3
  import * as Instance from "../instance.js";
4
4
  import { getKeywordName } from "../keywords.js";
@@ -34,7 +34,7 @@ const interpret = ({ contains, minContains, maxContains }, instance, ast, dynami
34
34
  const matches = pipe(
35
35
  iterator,
36
36
  filter((item) => Validation.interpret(contains, item, ast, dynamicAnchors, quiet)),
37
- reduce((matches) => matches + 1, 0)
37
+ count
38
38
  );
39
39
  return matches >= minContains && matches <= maxContains;
40
40
  };
@@ -12,10 +12,8 @@ const compile = (schema) => pipe(
12
12
  );
13
13
 
14
14
  const interpret = (dependentRequired, instance) => {
15
- const value = Instance.value(instance);
16
-
17
15
  return !Instance.typeOf(instance, "object") || dependentRequired.every(([propertyName, required]) => {
18
- return !(propertyName in value) || required.every((key) => key in value);
16
+ return !Instance.has(propertyName, instance) || required.every((key) => Instance.has(key, instance));
19
17
  });
20
18
  };
21
19
 
@@ -13,22 +13,19 @@ const compile = (schema, ast) => pipe(
13
13
  );
14
14
 
15
15
  const interpret = (dependentSchemas, instance, ast, dynamicAnchors, quiet) => {
16
- const value = Instance.value(instance);
17
-
18
16
  return !Instance.typeOf(instance, "object") || dependentSchemas.every(([propertyName, dependentSchema]) => {
19
- return !(propertyName in value) || Validation.interpret(dependentSchema, instance, ast, dynamicAnchors, quiet);
17
+ return !Instance.has(propertyName, instance) || Validation.interpret(dependentSchema, instance, ast, dynamicAnchors, quiet);
20
18
  });
21
19
  };
22
20
 
23
21
  const collectEvaluatedProperties = (dependentSchemas, instance, ast, dynamicAnchors) => {
24
- const value = Instance.value(instance);
25
22
  if (!Instance.typeOf(instance, "object")) {
26
23
  return false;
27
24
  }
28
25
 
29
26
  const evaluatedPropertyNames = new Set();
30
27
  for (const [propertyName, dependentSchema] of dependentSchemas) {
31
- if (propertyName in value) {
28
+ if (Instance.has(propertyName, instance)) {
32
29
  const propertyNames = Validation.collectEvaluatedProperties(dependentSchema, instance, ast, dynamicAnchors);
33
30
  if (propertyNames === false) {
34
31
  return false;
@@ -0,0 +1,88 @@
1
+ import * as Schema from "../schema.js";
2
+ import * as Instance from "../instance.js";
3
+ import Validation from "./validation.js";
4
+ import { fromEpsilon, fromSchema, closure, zeroOrOne, oneOrMore, concat, union } from "../nfa.js";
5
+
6
+
7
+ const id = "https://json-schema.org/keyword/itemPattern";
8
+ const experimental = true;
9
+
10
+ const compile = async (schema, ast) => {
11
+ const groups = [[]];
12
+ let group = groups[0];
13
+
14
+ for await (const rule of Schema.iter(schema)) {
15
+ if (Schema.typeOf(rule, "string")) {
16
+ const operator = Schema.value(rule);
17
+
18
+ if (operator === "*") {
19
+ group.push(closure(group.pop()));
20
+ } else if (operator === "?") {
21
+ group.push(zeroOrOne(group.pop()));
22
+ } else if (operator === "+") {
23
+ group.push(oneOrMore(group.pop()));
24
+ } else if (operator === "|") {
25
+ group = [];
26
+ groups.push(group);
27
+ } else {
28
+ throw Error(`Unsupported pattern syntax: ${operator}`);
29
+ }
30
+ } else {
31
+ const node = Schema.typeOf(rule, "array")
32
+ ? compile(rule, ast)
33
+ : fromSchema(await Validation.compile(rule, ast));
34
+ group.push(await node);
35
+ }
36
+ }
37
+
38
+ return Schema.length(schema) === 0 ? fromEpsilon() : groups
39
+ .map((group) => group.reduce(concat))
40
+ .reduce(union);
41
+ };
42
+
43
+ const interpret = (nfa, instance, ast, dynamicAnchors, quiet) => {
44
+ if (!Instance.typeOf(instance, "array")) {
45
+ return true;
46
+ }
47
+
48
+ let currentStates = [];
49
+ addNextState(nfa.start, currentStates, []);
50
+
51
+ for (const item of Instance.iter(instance)) {
52
+ const nextStates = [];
53
+
54
+ for (const state of currentStates) {
55
+ const nextState = transition(state.transition, item, ast, dynamicAnchors, quiet);
56
+ if (nextState) {
57
+ addNextState(nextState, nextStates, []);
58
+ }
59
+ }
60
+
61
+ currentStates = nextStates;
62
+ }
63
+
64
+ return Boolean(currentStates.find((s) => s.isEnd));
65
+ };
66
+
67
+ const addNextState = (state, nextStates, visited) => {
68
+ if (state.epsilonTransitions.length) {
69
+ for (const epsilonState of state.epsilonTransitions) {
70
+ if (!visited.find((visited) => visited === epsilonState)) {
71
+ visited.push(epsilonState);
72
+ addNextState(epsilonState, nextStates, visited);
73
+ }
74
+ }
75
+ } else {
76
+ nextStates.push(state);
77
+ }
78
+ };
79
+
80
+ const transition = (transitions, instance, ast, dynamicAnchors, quiet) => {
81
+ for (const schema in transitions) {
82
+ if (Validation.interpret(schema, instance, ast, dynamicAnchors, quiet)) {
83
+ return transitions[schema];
84
+ }
85
+ }
86
+ };
87
+
88
+ export default { id, experimental, compile, interpret };
@@ -13,11 +13,7 @@ const compile = (schema, ast) => pipe(
13
13
  );
14
14
 
15
15
  const interpret = (prefixItems, instance, ast, dynamicAnchors, quiet) => {
16
- if (!Instance.typeOf(instance, "array")) {
17
- return true;
18
- }
19
-
20
- return pipe(
16
+ return !Instance.typeOf(instance, "array") || pipe(
21
17
  zip(prefixItems, Instance.iter(instance)),
22
18
  take(Instance.length(instance)),
23
19
  every(([prefixItem, item]) => Validation.interpret(prefixItem, item, ast, dynamicAnchors, quiet))
package/lib/nfa.js ADDED
@@ -0,0 +1,95 @@
1
+ export const fromEpsilon = () => {
2
+ const start = createState(false);
3
+ const end = createState(true);
4
+ addEpsilonTransition(start, end);
5
+
6
+ return { start, end };
7
+ };
8
+
9
+ export const fromSchema = (schema) => {
10
+ const start = createState(false);
11
+ const end = createState(true);
12
+ addTransition(start, end, schema);
13
+
14
+ return { start, end };
15
+ };
16
+
17
+ export const concat = (first, second) => {
18
+ if (first === undefined) {
19
+ return second;
20
+ }
21
+ addEpsilonTransition(first.end, second.start);
22
+ first.end.isEnd = false;
23
+
24
+ return { start: first.start, end: second.end };
25
+ };
26
+
27
+ export const union = (first, second) => {
28
+ const start = createState(false);
29
+ addEpsilonTransition(start, first.start);
30
+ addEpsilonTransition(start, second.start);
31
+
32
+ const end = createState(true);
33
+
34
+ addEpsilonTransition(first.end, end);
35
+ first.end.isEnd = false;
36
+ addEpsilonTransition(second.end, end);
37
+ second.end.isEnd = false;
38
+
39
+ return { start, end };
40
+ };
41
+
42
+ export const closure = (nfa) => {
43
+ const start = createState(false);
44
+ const end = createState(true);
45
+
46
+ addEpsilonTransition(start, end);
47
+ addEpsilonTransition(start, nfa.start);
48
+
49
+ addEpsilonTransition(nfa.end, end);
50
+ addEpsilonTransition(nfa.end, nfa.start);
51
+ nfa.end.isEnd = false;
52
+
53
+ return { start, end };
54
+ };
55
+
56
+ export const zeroOrOne = (nfa) => {
57
+ const start = createState(false);
58
+ const end = createState(true);
59
+
60
+ addEpsilonTransition(start, end);
61
+ addEpsilonTransition(start, nfa.start);
62
+
63
+ addEpsilonTransition(nfa.end, end);
64
+ nfa.end.isEnd = false;
65
+
66
+ return { start, end };
67
+ };
68
+
69
+ export const oneOrMore = (nfa) => {
70
+ const start = createState(false);
71
+ const end = createState(true);
72
+
73
+ addEpsilonTransition(start, nfa.start);
74
+ addEpsilonTransition(nfa.end, end);
75
+ addEpsilonTransition(nfa.end, nfa.start);
76
+ nfa.end.isEnd = false;
77
+
78
+ return { start, end };
79
+ };
80
+
81
+ const addEpsilonTransition = (from, to) => {
82
+ from.epsilonTransitions.push(to);
83
+ };
84
+
85
+ const addTransition = (from, to, symbol) => {
86
+ from.transition[symbol] = to;
87
+ };
88
+
89
+ const createState = (isEnd) => {
90
+ return {
91
+ isEnd,
92
+ transition: {},
93
+ epsilonTransitions: []
94
+ };
95
+ };
package/lib/schema.js CHANGED
@@ -1,5 +1,3 @@
1
- import curry from "just-curry-it";
2
- import * as Pact from "@hyperjump/pact";
3
1
  import { nil as nilPointer, append as pointerAppend, get as pointerGet } from "@hyperjump/json-pointer";
4
2
  import { toAbsoluteIri } from "@hyperjump/uri";
5
3
  import { jsonTypeOf, resolveUri, uriFragment, pathRelative, jsonStringify } from "./common.js";
@@ -206,7 +204,7 @@ const getAnchorPointer = (schema, fragment) => {
206
204
 
207
205
  // Utility Functions
208
206
  export const uri = (doc) => doc.id ? `${doc.id}#${encodeURI(doc.pointer)}` : undefined;
209
- export const value = (doc) => Reference.isReference(doc.value) ? Reference.value(doc.value) : doc.value;
207
+ export const value = (doc) => doc.value;
210
208
  export const has = (key, doc) => key in value(doc);
211
209
  export const typeOf = (doc, type) => jsonTypeOf(value(doc), type);
212
210
 
@@ -65,6 +65,10 @@ loadDialect(jsonSchemaVersion, {
65
65
  [jsonSchemaVersion]: true
66
66
  });
67
67
 
68
+ loadDialect("https://spec.openapis.org/oas/3.0/schema", {
69
+ [jsonSchemaVersion]: true
70
+ });
71
+
68
72
  addSchema(dialectSchema);
69
73
 
70
74
  addSchema(schema20210928, "https://spec.openapis.org/oas/3.0/schema");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperjump/json-schema",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
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",
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@hyperjump/json-pointer": "^1.0.0",
67
- "@hyperjump/pact": "^1.0.0",
67
+ "@hyperjump/pact": "^1.2.0",
68
68
  "@hyperjump/uri": "^1.2.0",
69
69
  "content-type": "^1.0.4",
70
70
  "fastest-stable-stringify": "^2.0.2",
package/stable/index.js CHANGED
@@ -34,7 +34,9 @@ defineVocabulary("https://json-schema.org/vocab/applicator", {
34
34
  "if": "https://json-schema.org/keyword/if",
35
35
  "then": "https://json-schema.org/keyword/then",
36
36
  "else": "https://json-schema.org/keyword/else",
37
+ "conditional": "https://json-schema.org/keyword/conditional",
37
38
  "items": "https://json-schema.org/keyword/items",
39
+ "itemPattern": "https://json-schema.org/keyword/itemPattern",
38
40
  "not": "https://json-schema.org/keyword/not",
39
41
  "oneOf": "https://json-schema.org/keyword/oneOf",
40
42
  "patternProperties": "https://json-schema.org/keyword/patternProperties",
@@ -8,6 +8,7 @@ export default {
8
8
  "prefixItems": { "$ref": "#/$defs/schemaArray" },
9
9
  "items": { "$dynamicRef": "meta" },
10
10
  "contains": { "$dynamicRef": "meta" },
11
+ "itemPattern": { "$ref": "#/$defs/itemPattern" },
11
12
  "additionalProperties": { "$dynamicRef": "meta" },
12
13
  "properties": {
13
14
  "type": "object",
@@ -33,6 +34,16 @@ export default {
33
34
  "if": { "$dynamicRef": "meta" },
34
35
  "then": { "$dynamicRef": "meta" },
35
36
  "else": { "$dynamicRef": "meta" },
37
+ "conditional": {
38
+ "type": "array",
39
+ "items": {
40
+ "if": { "type": "array" },
41
+ "then": {
42
+ "items": { "$dynamicRef": "meta" }
43
+ },
44
+ "else": { "$dynamicRef": "meta" }
45
+ }
46
+ },
36
47
  "allOf": { "$ref": "#/$defs/schemaArray" },
37
48
  "anyOf": { "$ref": "#/$defs/schemaArray" },
38
49
  "oneOf": { "$ref": "#/$defs/schemaArray" },
@@ -44,6 +55,21 @@ export default {
44
55
  "type": "array",
45
56
  "minItems": 1,
46
57
  "items": { "$dynamicRef": "meta" }
58
+ },
59
+ "itemPattern": {
60
+ "type": "array",
61
+ "itemPattern": [
62
+ [
63
+ {
64
+ "if": { "type": "array" },
65
+ "then": { "$ref": "#/$defs/itemPattern" },
66
+ "else": { "$dynamicRef": "meta" }
67
+ },
68
+ { "enum": ["?", "*", "+"] }, "?",
69
+ "|",
70
+ { "const": "|" }
71
+ ], "*"
72
+ ]
47
73
  }
48
74
  }
49
75
  };