@maroonedog/luq 2.0.0 → 2.1.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 (109) hide show
  1. package/README.md +100 -10
  2. package/dist/chain/create-chain-node.js +22 -1
  3. package/dist/chain/create-chain-node.mjs +22 -1
  4. package/dist/compile/resolve-conditional-presence.js +1 -0
  5. package/dist/compile/resolve-conditional-presence.mjs +1 -0
  6. package/dist/compile/resolve-presence.js +4 -0
  7. package/dist/compile/resolve-presence.mjs +4 -0
  8. package/dist/compile/validation-plan.types.d.ts +10 -0
  9. package/dist/json-schema/build-from-schema.js +2 -3
  10. package/dist/json-schema/build-from-schema.mjs +2 -3
  11. package/dist/json-schema/collect-definitions.d.ts +30 -0
  12. package/dist/json-schema/collect-definitions.js +47 -3
  13. package/dist/json-schema/collect-definitions.mjs +46 -4
  14. package/dist/json-schema/collect-sub-schema-rules.d.ts +12 -1
  15. package/dist/json-schema/collect-sub-schema-rules.js +37 -7
  16. package/dist/json-schema/collect-sub-schema-rules.mjs +38 -8
  17. package/dist/json-schema/create-structural-context.d.ts +13 -3
  18. package/dist/json-schema/create-structural-context.js +72 -6
  19. package/dist/json-schema/create-structural-context.mjs +72 -7
  20. package/dist/json-schema/declare-additional-properties.d.ts +19 -0
  21. package/dist/json-schema/declare-additional-properties.js +42 -0
  22. package/dist/json-schema/declare-additional-properties.mjs +37 -0
  23. package/dist/json-schema/declare-object-keywords.d.ts +0 -2
  24. package/dist/json-schema/declare-object-keywords.js +6 -13
  25. package/dist/json-schema/declare-object-keywords.mjs +7 -13
  26. package/dist/json-schema/declare-presence.d.ts +9 -3
  27. package/dist/json-schema/declare-presence.js +12 -6
  28. package/dist/json-schema/declare-presence.mjs +12 -6
  29. package/dist/json-schema/extensions/json-schema/index.d.ts +1 -0
  30. package/dist/json-schema/extensions/json-schema/json-schema.d.ts +17 -2
  31. package/dist/json-schema/extensions/json-schema/json-schema.js +15 -7
  32. package/dist/json-schema/extensions/json-schema/json-schema.mjs +16 -8
  33. package/dist/json-schema/extensions/json-schema-full-feature/index.d.ts +1 -0
  34. package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.d.ts +2 -1
  35. package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.js +3 -1
  36. package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.mjs +3 -1
  37. package/dist/json-schema/follow-json-pointer.d.ts +23 -0
  38. package/dist/json-schema/follow-json-pointer.js +87 -0
  39. package/dist/json-schema/follow-json-pointer.mjs +82 -0
  40. package/dist/json-schema/index.d.ts +7 -2
  41. package/dist/json-schema/index.js +10 -2
  42. package/dist/json-schema/index.mjs +5 -2
  43. package/dist/json-schema/keyword-map-object.d.ts +1 -1
  44. package/dist/json-schema/ref-resolution-error.d.ts +4 -0
  45. package/dist/json-schema/ref-resolution-error.js +20 -0
  46. package/dist/json-schema/ref-resolution-error.mjs +16 -0
  47. package/dist/json-schema/ref-scope.d.ts +18 -0
  48. package/dist/json-schema/ref-scope.js +37 -0
  49. package/dist/json-schema/ref-scope.mjs +33 -0
  50. package/dist/json-schema/resolve-ref.d.ts +14 -9
  51. package/dist/json-schema/resolve-ref.js +89 -59
  52. package/dist/json-schema/resolve-ref.mjs +86 -55
  53. package/dist/json-schema/schema-registry.d.ts +30 -0
  54. package/dist/json-schema/schema-registry.js +142 -0
  55. package/dist/json-schema/schema-registry.mjs +139 -0
  56. package/dist/json-schema/schema-to-declarations.js +2 -1
  57. package/dist/json-schema/schema-to-declarations.mjs +2 -1
  58. package/dist/json-schema/structural-expansion.types.d.ts +8 -1
  59. package/dist/json-schema/uri-reference.d.ts +29 -0
  60. package/dist/json-schema/uri-reference.js +118 -0
  61. package/dist/json-schema/uri-reference.mjs +111 -0
  62. package/dist/path/create-value-writer.js +27 -2
  63. package/dist/path/create-value-writer.mjs +27 -2
  64. package/dist/path/reserved-segment.d.ts +22 -4
  65. package/dist/path/reserved-segment.js +27 -7
  66. package/dist/path/reserved-segment.mjs +27 -7
  67. package/dist/plugin-kit/compiled-rule.d.ts +13 -0
  68. package/dist/plugin-kit/create-rule.d.ts +11 -0
  69. package/dist/plugin-kit/create-rule.js +1 -0
  70. package/dist/plugin-kit/create-rule.mjs +1 -0
  71. package/dist/plugin-kit/plugin-definition.d.ts +15 -0
  72. package/dist/plugin-kit/plugin-definition.js +1 -0
  73. package/dist/plugin-kit/plugin-definition.mjs +1 -0
  74. package/dist/plugins/object-additional-properties/object-additional-properties-schema.d.ts +1 -1
  75. package/dist/plugins/object-additional-properties/object-additional-properties-schema.js +4 -2
  76. package/dist/plugins/object-additional-properties/object-additional-properties-schema.mjs +4 -2
  77. package/dist/plugins/object-additional-properties/object-additional-properties.d.ts +1 -1
  78. package/dist/plugins/object-additional-properties/object-additional-properties.js +4 -2
  79. package/dist/plugins/object-additional-properties/object-additional-properties.mjs +4 -2
  80. package/dist/plugins/object-additional-properties/select-additional-keys.d.ts +15 -0
  81. package/dist/plugins/object-additional-properties/select-additional-keys.js +51 -0
  82. package/dist/plugins/object-additional-properties/select-additional-keys.mjs +47 -0
  83. package/dist/plugins/string-exact-length/string-exact-length.js +2 -2
  84. package/dist/plugins/string-exact-length/string-exact-length.mjs +3 -3
  85. package/dist/plugins/string-max/string-max.js +2 -2
  86. package/dist/plugins/string-max/string-max.mjs +3 -3
  87. package/dist/plugins/string-min/string-min.js +2 -2
  88. package/dist/plugins/string-min/string-min.mjs +3 -3
  89. package/dist/runtime/decide-presence.js +2 -1
  90. package/dist/runtime/decide-presence.mjs +2 -1
  91. package/dist/standard-schema/index.d.ts +6 -0
  92. package/dist/standard-schema/index.js +7 -0
  93. package/dist/standard-schema/index.mjs +2 -0
  94. package/dist/standard-schema/split-issue-path.d.ts +9 -0
  95. package/dist/standard-schema/split-issue-path.js +66 -0
  96. package/dist/standard-schema/split-issue-path.mjs +63 -0
  97. package/dist/standard-schema/standard-schema.types.d.ts +46 -0
  98. package/dist/standard-schema/standard-schema.types.js +13 -0
  99. package/dist/standard-schema/standard-schema.types.mjs +12 -0
  100. package/dist/standard-schema/to-standard-schema.d.ts +41 -0
  101. package/dist/standard-schema/to-standard-schema.js +60 -0
  102. package/dist/standard-schema/to-standard-schema.mjs +57 -0
  103. package/dist/standard-schema.d.ts +1 -0
  104. package/dist/standard-schema.js +2 -0
  105. package/dist/standard-schema.mjs +1 -0
  106. package/dist/types/index.d.ts +12 -0
  107. package/dist/types/index.js +18 -0
  108. package/dist/types/index.mjs +17 -0
  109. package/package.json +10 -3
@@ -18,20 +18,17 @@
18
18
  // ===========================================================================
19
19
  import { composite, fieldsBranch } from "../plugin-kit/create-rule.mjs";
20
20
  import { PASS } from "../types/index.mjs";
21
- import { resolveSchemaNode } from "./collect-definitions.mjs";
21
+ import { resolveSchemaNodeInScope } from "./collect-definitions.mjs";
22
22
  import { declarePresenceRules } from "./declare-presence.mjs";
23
- import { permitsNull } from "./declare-value-keywords.mjs";
24
23
  import { EACH_STEP } from "./flatten-array-schema.mjs";
25
24
  import { expandSchemaRules, readChildSchemas } from "./schema-to-declarations.mjs";
26
25
  const NO_BRANCH_FIELDS = Object.freeze([]);
27
26
  function toBranchField(child, context) {
28
- const node = resolveSchemaNode(child.schema, context.root);
29
27
  return {
30
28
  path: child.step,
31
29
  rules: Object.freeze([
32
30
  ...declarePresenceRules({
33
31
  isRequired: child.isRequired,
34
- allowsNull: permitsNull(node),
35
32
  severity: context.build.config.defaultSeverity,
36
33
  }),
37
34
  ...context.collectSubSchemaRules(child.schema),
@@ -52,9 +49,31 @@ function composeProperties(children, context) {
52
49
  }),
53
50
  ];
54
51
  }
52
+ /**
53
+ * Every sub-schema subject gets the same one-line statement: null is a VALUE
54
+ * here, so the checks run on it.
55
+ *
56
+ * src/runtime/run-field.ts settles presence BEFORE any check, and a subject
57
+ * with no presence rule carries OPEN_PRESENCE, which ends the field on null.
58
+ * A branch subject and an array element both arrive without one, so every
59
+ * check the sub-schema declared was skipped for null: `[null]` passed
60
+ * `{"items":{"type":"boolean"}}`, and `additionalItems: false` accepted a
61
+ * trailing null. Deciding it from `type` alone is not enough either —
62
+ * `false`, `{"not": {}}` and an `enum` without null forbid null while
63
+ * saying nothing about `type`.
64
+ */
65
+ function declareOwnNullPolicy(context) {
66
+ return declarePresenceRules({
67
+ isRequired: false,
68
+ severity: context.build.config.defaultSeverity,
69
+ });
70
+ }
55
71
  export function collectSubSchemaRules(schema, context) {
56
- const node = resolveSchemaNode(schema, context.root);
57
- const rules = [...expandSchemaRules(node, context)];
72
+ const node = resolveSchemaNodeInScope(schema, context.scope).node;
73
+ const rules = [
74
+ ...declareOwnNullPolicy(context),
75
+ ...expandSchemaRules(node, context),
76
+ ];
58
77
  const children = readChildSchemas(node);
59
78
  const properties = children.filter((child) => child.step !== EACH_STEP);
60
79
  if (properties.length > 0) {
@@ -68,11 +87,22 @@ export function collectSubSchemaRules(schema, context) {
68
87
  }
69
88
  return Object.freeze(rules);
70
89
  }
71
- /** Every branch of every composite is built here, so a branch is one shape. */
90
+ /**
91
+ * Every branch of every composite is built here, so a branch is one shape.
92
+ *
93
+ * The call is to the function above and NOT to `context.collectSubSchemaRules`,
94
+ * and the difference is load-bearing. `context` is already the child context
95
+ * that createStructuralContext produced by DESCENDING through this schema, so
96
+ * a `$ref` here is already recorded in `visitedRefs`. Going through the
97
+ * context would descend the same `$ref` a second time, the recursion guard
98
+ * would see it as a cycle, and the branch would come back with no rules at all
99
+ * — `{"items":[{"$ref":"#/definitions/x"}]}` constrained nothing while the
100
+ * inline form `{"items":[{"type":"integer"}]}` worked.
101
+ */
72
102
  export function toSchemaBranch(label, schema, context) {
73
103
  return {
74
104
  label,
75
- rules: context.collectSubSchemaRules(schema),
105
+ rules: collectSubSchemaRules(schema, context),
76
106
  fields: NO_BRANCH_FIELDS,
77
107
  };
78
108
  }
@@ -1,10 +1,20 @@
1
1
  import type { ChainBuildContext } from "../chain/create-chain-node";
2
- import type { Draft07Schema, Draft07SchemaObject } from "./draft07.types";
2
+ import type { RefScope } from "./ref-scope";
3
+ import type { Draft07SchemaObject } from "./draft07.types";
3
4
  import type { JsonSchemaBag } from "./json-schema-bag.types";
4
5
  import type { StructuralContext } from "./structural-expansion.types";
6
+ /** Shared by every context of ONE conversion, so the budget is the whole. */
7
+ interface ExpansionBudget {
8
+ remaining: number;
9
+ }
5
10
  export interface ConversionSeed {
6
11
  readonly bag: JsonSchemaBag;
7
- readonly root: Draft07Schema;
12
+ readonly scope: RefScope;
8
13
  readonly chain: ChainBuildContext;
14
+ /** Created by the entry point; every nested context shares this one. */
15
+ readonly budget?: ExpansionBudget;
9
16
  }
10
- export declare function createStructuralContext(seed: ConversionSeed, node: Draft07SchemaObject, visitedRefs: readonly string[]): StructuralContext;
17
+ /** The budget a conversion starts with. One per document, not one per node. */
18
+ export declare function createExpansionBudget(): ExpansionBudget;
19
+ export declare function createStructuralContext(seed: ConversionSeed, node: Draft07SchemaObject, visitedRefs: readonly string[], scope?: RefScope): StructuralContext;
20
+ export {};
@@ -1,25 +1,91 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createExpansionBudget = createExpansionBudget;
3
4
  exports.createStructuralContext = createStructuralContext;
4
5
  const collect_definitions_1 = require("./collect-definitions");
5
6
  const collect_sub_schema_rules_1 = require("./collect-sub-schema-rules");
6
7
  const NO_RULES = Object.freeze([]);
8
+ /**
9
+ * How many times one `$ref` may be entered on a single branch of the
10
+ * conversion, and how much expansion the whole conversion may do.
11
+ *
12
+ * A recursive definition has infinitely many declared paths and Luq declares
13
+ * finite ones, so SOMETHING has to stop. It used to stop at the FIRST repeat,
14
+ * which is why a mutually recursive schema (tree -> node -> tree) checked only
15
+ * two levels and silently accepted anything deeper.
16
+ *
17
+ * Unrolling further is exponential in the number of mutually recursive
18
+ * definitions, and the measurement says so plainly. A three-way cycle
19
+ * (a -> b -> c -> a, each also naming the others) builds in:
20
+ *
21
+ * 1 unroll 17 ms 2 unrolls 55 ms
22
+ * 3 unrolls 538 ms 4 unrolls 9257 ms
23
+ *
24
+ * Three is what the corpus needs (the mutually recursive tree in ref.json
25
+ * puts its invalid value three levels down) and is where the cost is still
26
+ * a fraction of a second. It is a BUILD-time cost, paid once per document.
27
+ *
28
+ * The per-ref cap bounds the depth and leaves the WIDTH to the document, so
29
+ * it is not a bound on the work by itself. EXPANSION_BUDGET is: it caps the
30
+ * total number of `$ref` expansions one conversion may do, whatever shape
31
+ * the document has. It is deliberately far above what a real schema reaches
32
+ * — the whole official corpus stays under it — so it changes nothing for
33
+ * ordinary documents and only stops a pathological one from running away.
34
+ *
35
+ * Running out is not an error. The conversion stops adding rules, exactly as
36
+ * it did at the first repeat before, and the document is checked to the depth
37
+ * that was reached — the same silence as before, moved further out.
38
+ */
39
+ const MAX_REF_UNROLL = 3;
40
+ const EXPANSION_BUDGET = 100000;
41
+ function countOf(pointers, pointer) {
42
+ let count = 0;
43
+ for (const one of pointers)
44
+ if (one === pointer)
45
+ count += 1;
46
+ return count;
47
+ }
48
+ /** The budget a conversion starts with. One per document, not one per node. */
49
+ function createExpansionBudget() {
50
+ return { remaining: EXPANSION_BUDGET };
51
+ }
7
52
  function propertyKeysOf(schema) {
8
53
  return Object.freeze(Object.keys(schema.properties ?? {}));
9
54
  }
10
- function createStructuralContext(seed, node, visitedRefs) {
55
+ function createStructuralContext(seed, node, visitedRefs, scope = seed.scope) {
56
+ const budget = seed.budget ?? createExpansionBudget();
11
57
  const build = {
12
58
  fieldPath: seed.chain.fieldPath,
13
59
  declaredSiblingKeys: propertyKeysOf(node),
14
60
  config: seed.chain.config,
15
61
  };
62
+ /**
63
+ * One descent: the node this schema really is, and the context inside it.
64
+ *
65
+ * The RESOLVED node is handed back, and callers must use it rather than the
66
+ * schema they passed in. Resolving the same `$ref` a second time — which
67
+ * is what happens if the original is passed on — moves the base twice, so
68
+ * a relative reference under a relative `$id` doubles its own folder.
69
+ */
16
70
  const descend = (schema) => {
17
71
  const pointer = (0, collect_definitions_1.readRefPointer)(schema);
18
- if (pointer !== undefined && visitedRefs.includes(pointer)) {
72
+ if (pointer !== undefined &&
73
+ countOf(visitedRefs, pointer) >= MAX_REF_UNROLL) {
19
74
  return undefined;
20
75
  }
76
+ if (pointer !== undefined) {
77
+ if (budget.remaining <= 0)
78
+ return undefined;
79
+ budget.remaining -= 1;
80
+ }
21
81
  const seen = pointer === undefined ? visitedRefs : [...visitedRefs, pointer];
22
- return createStructuralContext(seed, (0, collect_definitions_1.resolveSchemaNode)(schema, seed.root), seen);
82
+ // The scope moves with the node: following a `$ref` into another
83
+ // document makes THAT document's base the one its own refs resolve in.
84
+ const resolved = (0, collect_definitions_1.resolveSchemaNodeInScope)(schema, scope);
85
+ return {
86
+ node: resolved.node,
87
+ context: createStructuralContext({ ...seed, budget }, resolved.node, seen, resolved.scope),
88
+ };
23
89
  };
24
90
  const ruleContextFor = (pluginName, code) => ({
25
91
  pluginName,
@@ -32,19 +98,19 @@ function createStructuralContext(seed, node, visitedRefs) {
32
98
  return {
33
99
  bag: seed.bag,
34
100
  build,
35
- root: seed.root,
101
+ scope,
36
102
  visitedRefs,
37
103
  collectSubSchemaRules: (schema) => {
38
104
  const child = descend(schema);
39
105
  return child === undefined
40
106
  ? NO_RULES
41
- : (0, collect_sub_schema_rules_1.collectSubSchemaRules)(schema, child);
107
+ : (0, collect_sub_schema_rules_1.collectSubSchemaRules)(child.node, child.context);
42
108
  },
43
109
  toBranch: (label, schema) => {
44
110
  const child = descend(schema);
45
111
  return child === undefined
46
112
  ? { label, rules: NO_RULES, fields: [] }
47
- : (0, collect_sub_schema_rules_1.toSchemaBranch)(label, schema, child);
113
+ : (0, collect_sub_schema_rules_1.toSchemaBranch)(label, child.node, child.context);
48
114
  },
49
115
  ruleContextFor,
50
116
  };
@@ -1,22 +1,87 @@
1
- import { readRefPointer, resolveSchemaNode } from "./collect-definitions.mjs";
1
+ import { readRefPointer, resolveSchemaNodeInScope, } from "./collect-definitions.mjs";
2
2
  import { collectSubSchemaRules, toSchemaBranch, } from "./collect-sub-schema-rules.mjs";
3
3
  const NO_RULES = Object.freeze([]);
4
+ /**
5
+ * How many times one `$ref` may be entered on a single branch of the
6
+ * conversion, and how much expansion the whole conversion may do.
7
+ *
8
+ * A recursive definition has infinitely many declared paths and Luq declares
9
+ * finite ones, so SOMETHING has to stop. It used to stop at the FIRST repeat,
10
+ * which is why a mutually recursive schema (tree -> node -> tree) checked only
11
+ * two levels and silently accepted anything deeper.
12
+ *
13
+ * Unrolling further is exponential in the number of mutually recursive
14
+ * definitions, and the measurement says so plainly. A three-way cycle
15
+ * (a -> b -> c -> a, each also naming the others) builds in:
16
+ *
17
+ * 1 unroll 17 ms 2 unrolls 55 ms
18
+ * 3 unrolls 538 ms 4 unrolls 9257 ms
19
+ *
20
+ * Three is what the corpus needs (the mutually recursive tree in ref.json
21
+ * puts its invalid value three levels down) and is where the cost is still
22
+ * a fraction of a second. It is a BUILD-time cost, paid once per document.
23
+ *
24
+ * The per-ref cap bounds the depth and leaves the WIDTH to the document, so
25
+ * it is not a bound on the work by itself. EXPANSION_BUDGET is: it caps the
26
+ * total number of `$ref` expansions one conversion may do, whatever shape
27
+ * the document has. It is deliberately far above what a real schema reaches
28
+ * — the whole official corpus stays under it — so it changes nothing for
29
+ * ordinary documents and only stops a pathological one from running away.
30
+ *
31
+ * Running out is not an error. The conversion stops adding rules, exactly as
32
+ * it did at the first repeat before, and the document is checked to the depth
33
+ * that was reached — the same silence as before, moved further out.
34
+ */
35
+ const MAX_REF_UNROLL = 3;
36
+ const EXPANSION_BUDGET = 100000;
37
+ function countOf(pointers, pointer) {
38
+ let count = 0;
39
+ for (const one of pointers)
40
+ if (one === pointer)
41
+ count += 1;
42
+ return count;
43
+ }
44
+ /** The budget a conversion starts with. One per document, not one per node. */
45
+ export function createExpansionBudget() {
46
+ return { remaining: EXPANSION_BUDGET };
47
+ }
4
48
  function propertyKeysOf(schema) {
5
49
  return Object.freeze(Object.keys(schema.properties ?? {}));
6
50
  }
7
- export function createStructuralContext(seed, node, visitedRefs) {
51
+ export function createStructuralContext(seed, node, visitedRefs, scope = seed.scope) {
52
+ const budget = seed.budget ?? createExpansionBudget();
8
53
  const build = {
9
54
  fieldPath: seed.chain.fieldPath,
10
55
  declaredSiblingKeys: propertyKeysOf(node),
11
56
  config: seed.chain.config,
12
57
  };
58
+ /**
59
+ * One descent: the node this schema really is, and the context inside it.
60
+ *
61
+ * The RESOLVED node is handed back, and callers must use it rather than the
62
+ * schema they passed in. Resolving the same `$ref` a second time — which
63
+ * is what happens if the original is passed on — moves the base twice, so
64
+ * a relative reference under a relative `$id` doubles its own folder.
65
+ */
13
66
  const descend = (schema) => {
14
67
  const pointer = readRefPointer(schema);
15
- if (pointer !== undefined && visitedRefs.includes(pointer)) {
68
+ if (pointer !== undefined &&
69
+ countOf(visitedRefs, pointer) >= MAX_REF_UNROLL) {
16
70
  return undefined;
17
71
  }
72
+ if (pointer !== undefined) {
73
+ if (budget.remaining <= 0)
74
+ return undefined;
75
+ budget.remaining -= 1;
76
+ }
18
77
  const seen = pointer === undefined ? visitedRefs : [...visitedRefs, pointer];
19
- return createStructuralContext(seed, resolveSchemaNode(schema, seed.root), seen);
78
+ // The scope moves with the node: following a `$ref` into another
79
+ // document makes THAT document's base the one its own refs resolve in.
80
+ const resolved = resolveSchemaNodeInScope(schema, scope);
81
+ return {
82
+ node: resolved.node,
83
+ context: createStructuralContext({ ...seed, budget }, resolved.node, seen, resolved.scope),
84
+ };
20
85
  };
21
86
  const ruleContextFor = (pluginName, code) => ({
22
87
  pluginName,
@@ -29,19 +94,19 @@ export function createStructuralContext(seed, node, visitedRefs) {
29
94
  return {
30
95
  bag: seed.bag,
31
96
  build,
32
- root: seed.root,
97
+ scope,
33
98
  visitedRefs,
34
99
  collectSubSchemaRules: (schema) => {
35
100
  const child = descend(schema);
36
101
  return child === undefined
37
102
  ? NO_RULES
38
- : collectSubSchemaRules(schema, child);
103
+ : collectSubSchemaRules(child.node, child.context);
39
104
  },
40
105
  toBranch: (label, schema) => {
41
106
  const child = descend(schema);
42
107
  return child === undefined
43
108
  ? { label, rules: NO_RULES, fields: [] }
44
- : toSchemaBranch(label, schema, child);
109
+ : toSchemaBranch(label, child.node, child.context);
45
110
  },
46
111
  ruleContextFor,
47
112
  };
@@ -0,0 +1,19 @@
1
+ import type { Rule } from "../plugin-kit/compiled-rule";
2
+ import type { ConverterChain } from "./apply-keyword-binding";
3
+ import type { Draft07SchemaObject } from "./draft07.types";
4
+ import type { StructuralContext } from "./structural-expansion.types";
5
+ /**
6
+ * `patternProperties` のキー、つまり正規表現の文字列。
7
+ * additionalProperties が「該当しないキー」を選ぶのに要る。
8
+ */
9
+ export declare function patternPropertyKeys(schema: Draft07SchemaObject): readonly string[];
10
+ /**
11
+ * BOOLEAN 形。
12
+ *
13
+ * キーワード束縛は自分の値 (boolean) しか運べないので、パターンがあるときだけ
14
+ * プラグインを直接呼ぶ。束縛のほうは残す: キーワード表と「そのメソッドが
15
+ * 実在する」というコンパイル時のゲートはそちらが持っている。
16
+ */
17
+ export declare function applyAdditionalPropertiesBoolean(chain: ConverterChain<"object">, schema: Draft07SchemaObject, allowed: boolean): ConverterChain<"object">;
18
+ /** SCHEMA 形: 宣言にもパターンにも該当しない値が、そのスキーマに従う。 */
19
+ export declare function declareAdditionalPropertiesSchema(schema: Draft07SchemaObject, context: StructuralContext): readonly Rule[];
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.patternPropertyKeys = patternPropertyKeys;
4
+ exports.applyAdditionalPropertiesBoolean = applyAdditionalPropertiesBoolean;
5
+ exports.declareAdditionalPropertiesSchema = declareAdditionalPropertiesSchema;
6
+ const apply_keyword_binding_1 = require("./apply-keyword-binding");
7
+ const keyword_map_object_1 = require("./keyword-map-object");
8
+ const NO_RULES = Object.freeze([]);
9
+ /**
10
+ * `patternProperties` のキー、つまり正規表現の文字列。
11
+ * additionalProperties が「該当しないキー」を選ぶのに要る。
12
+ */
13
+ function patternPropertyKeys(schema) {
14
+ const patterns = schema.patternProperties;
15
+ if (patterns === undefined || patterns === null)
16
+ return Object.freeze([]);
17
+ return Object.freeze(Object.keys(patterns));
18
+ }
19
+ /**
20
+ * BOOLEAN 形。
21
+ *
22
+ * キーワード束縛は自分の値 (boolean) しか運べないので、パターンがあるときだけ
23
+ * プラグインを直接呼ぶ。束縛のほうは残す: キーワード表と「そのメソッドが
24
+ * 実在する」というコンパイル時のゲートはそちらが持っている。
25
+ */
26
+ function applyAdditionalPropertiesBoolean(chain, schema, allowed) {
27
+ const patterns = patternPropertyKeys(schema);
28
+ return patterns.length === 0
29
+ ? (0, apply_keyword_binding_1.applyKeywordBinding)(chain, keyword_map_object_1.additionalPropertiesBinding, allowed)
30
+ : chain.additionalProperties(allowed, undefined, patterns);
31
+ }
32
+ /** SCHEMA 形: 宣言にもパターンにも該当しない値が、そのスキーマに従う。 */
33
+ function declareAdditionalPropertiesSchema(schema, context) {
34
+ const additional = schema.additionalProperties;
35
+ if (additional === undefined || typeof additional === "boolean") {
36
+ return NO_RULES;
37
+ }
38
+ const plugin = context.bag.objectAdditionalPropertiesSchema;
39
+ return Object.freeze([
40
+ plugin.build(context.ruleContextFor(plugin.name, "additionalProperties"), context.collectSubSchemaRules(additional), undefined, patternPropertyKeys(schema)),
41
+ ]);
42
+ }
@@ -0,0 +1,37 @@
1
+ import { applyKeywordBinding } from "./apply-keyword-binding.mjs";
2
+ import { additionalPropertiesBinding } from "./keyword-map-object.mjs";
3
+ const NO_RULES = Object.freeze([]);
4
+ /**
5
+ * `patternProperties` のキー、つまり正規表現の文字列。
6
+ * additionalProperties が「該当しないキー」を選ぶのに要る。
7
+ */
8
+ export function patternPropertyKeys(schema) {
9
+ const patterns = schema.patternProperties;
10
+ if (patterns === undefined || patterns === null)
11
+ return Object.freeze([]);
12
+ return Object.freeze(Object.keys(patterns));
13
+ }
14
+ /**
15
+ * BOOLEAN 形。
16
+ *
17
+ * キーワード束縛は自分の値 (boolean) しか運べないので、パターンがあるときだけ
18
+ * プラグインを直接呼ぶ。束縛のほうは残す: キーワード表と「そのメソッドが
19
+ * 実在する」というコンパイル時のゲートはそちらが持っている。
20
+ */
21
+ export function applyAdditionalPropertiesBoolean(chain, schema, allowed) {
22
+ const patterns = patternPropertyKeys(schema);
23
+ return patterns.length === 0
24
+ ? applyKeywordBinding(chain, additionalPropertiesBinding, allowed)
25
+ : chain.additionalProperties(allowed, undefined, patterns);
26
+ }
27
+ /** SCHEMA 形: 宣言にもパターンにも該当しない値が、そのスキーマに従う。 */
28
+ export function declareAdditionalPropertiesSchema(schema, context) {
29
+ const additional = schema.additionalProperties;
30
+ if (additional === undefined || typeof additional === "boolean") {
31
+ return NO_RULES;
32
+ }
33
+ const plugin = context.bag.objectAdditionalPropertiesSchema;
34
+ return Object.freeze([
35
+ plugin.build(context.ruleContextFor(plugin.name, "additionalProperties"), context.collectSubSchemaRules(additional), undefined, patternPropertyKeys(schema)),
36
+ ]);
37
+ }
@@ -2,8 +2,6 @@ import type { Rule } from "../plugin-kit/compiled-rule";
2
2
  import type { Draft07Schema, Draft07SchemaObject } from "./draft07.types";
3
3
  import type { StructuralContext } from "./structural-expansion.types";
4
4
  export declare function declareObjectRules(schema: Draft07SchemaObject, context: StructuralContext): readonly Rule[];
5
- /** The SCHEMA form of `additionalProperties`: every undeclared value obeys it. */
6
- export declare function declareAdditionalPropertiesSchema(schema: Draft07SchemaObject, context: StructuralContext): readonly Rule[];
7
5
  /** EVERY matching pattern applies; 1.x broke after the first match. */
8
6
  export declare function declarePatternProperties(schema: Draft07SchemaObject, context: StructuralContext): readonly Rule[];
9
7
  /** A sub-chain over the property NAMES, which are always strings. */
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.declareObjectRules = declareObjectRules;
4
- exports.declareAdditionalPropertiesSchema = declareAdditionalPropertiesSchema;
5
4
  exports.declarePatternProperties = declarePatternProperties;
6
5
  exports.declarePropertyNames = declarePropertyNames;
7
6
  exports.declareDependencies = declareDependencies;
@@ -23,6 +22,7 @@ exports.readPropertyChildren = readPropertyChildren;
23
22
  const create_chain_node_1 = require("../chain/create-chain-node");
24
23
  const create_field_slots_1 = require("../chain/create-field-slots");
25
24
  const types_1 = require("../types");
25
+ const declare_additional_properties_1 = require("./declare-additional-properties");
26
26
  const apply_keyword_binding_1 = require("./apply-keyword-binding");
27
27
  const collect_definitions_1 = require("./collect-definitions");
28
28
  const keyword_map_object_1 = require("./keyword-map-object");
@@ -40,21 +40,14 @@ function declareObjectRules(schema, context) {
40
40
  }
41
41
  const additional = schema.additionalProperties;
42
42
  if (typeof additional === "boolean") {
43
- chain = (0, apply_keyword_binding_1.applyKeywordBinding)(chain, keyword_map_object_1.additionalPropertiesBinding, additional);
43
+ // §6.5.4 の対象は「properties にも patternProperties にも該当しない」キー。
44
+ // キーワード束縛は自分の値 (boolean) しか運べないので、パターンがある
45
+ // ときだけプラグインを直接呼ぶ。束縛のほうは残す: キーワード表と
46
+ // 「そのメソッドが実在する」というコンパイル時のゲートはそちらが持つ。
47
+ chain = (0, declare_additional_properties_1.applyAdditionalPropertiesBoolean)(chain, schema, additional);
44
48
  }
45
49
  return readRules(chain);
46
50
  }
47
- /** The SCHEMA form of `additionalProperties`: every undeclared value obeys it. */
48
- function declareAdditionalPropertiesSchema(schema, context) {
49
- const additional = schema.additionalProperties;
50
- if (additional === undefined || typeof additional === "boolean") {
51
- return NO_RULES;
52
- }
53
- const plugin = context.bag.objectAdditionalPropertiesSchema;
54
- return Object.freeze([
55
- plugin.build(context.ruleContextFor(plugin.name, "additionalProperties"), context.collectSubSchemaRules(additional)),
56
- ]);
57
- }
58
51
  /** EVERY matching pattern applies; 1.x broke after the first match. */
59
52
  function declarePatternProperties(schema, context) {
60
53
  const patterns = schema.patternProperties;
@@ -15,9 +15,10 @@
15
15
  import { readChainRules } from "../chain/create-chain-node.mjs";
16
16
  import { createFieldSlots } from "../chain/create-field-slots.mjs";
17
17
  import { isStringArray } from "../types/index.mjs";
18
+ import { applyAdditionalPropertiesBoolean } from "./declare-additional-properties.mjs";
18
19
  import { applyKeywordBinding } from "./apply-keyword-binding.mjs";
19
20
  import { toSchemaObject } from "./collect-definitions.mjs";
20
- import { additionalPropertiesBinding, maxPropertiesBinding, minPropertiesBinding, } from "./keyword-map-object.mjs";
21
+ import { maxPropertiesBinding, minPropertiesBinding, } from "./keyword-map-object.mjs";
21
22
  const NO_RULES = Object.freeze([]);
22
23
  function readRules(chain) {
23
24
  return readChainRules(chain) ?? NO_RULES;
@@ -32,21 +33,14 @@ export function declareObjectRules(schema, context) {
32
33
  }
33
34
  const additional = schema.additionalProperties;
34
35
  if (typeof additional === "boolean") {
35
- chain = applyKeywordBinding(chain, additionalPropertiesBinding, additional);
36
+ // §6.5.4 の対象は「properties にも patternProperties にも該当しない」キー。
37
+ // キーワード束縛は自分の値 (boolean) しか運べないので、パターンがある
38
+ // ときだけプラグインを直接呼ぶ。束縛のほうは残す: キーワード表と
39
+ // 「そのメソッドが実在する」というコンパイル時のゲートはそちらが持つ。
40
+ chain = applyAdditionalPropertiesBoolean(chain, schema, additional);
36
41
  }
37
42
  return readRules(chain);
38
43
  }
39
- /** The SCHEMA form of `additionalProperties`: every undeclared value obeys it. */
40
- export function declareAdditionalPropertiesSchema(schema, context) {
41
- const additional = schema.additionalProperties;
42
- if (additional === undefined || typeof additional === "boolean") {
43
- return NO_RULES;
44
- }
45
- const plugin = context.bag.objectAdditionalPropertiesSchema;
46
- return Object.freeze([
47
- plugin.build(context.ruleContextFor(plugin.name, "additionalProperties"), context.collectSubSchemaRules(additional)),
48
- ]);
49
- }
50
44
  /** EVERY matching pattern applies; 1.x broke after the first match. */
51
45
  export function declarePatternProperties(schema, context) {
52
46
  const patterns = schema.patternProperties;
@@ -2,11 +2,17 @@ import type { Rule } from "../plugin-kit/compiled-rule";
2
2
  import type { IssueSeverity } from "../types";
3
3
  export interface PresenceDeclaration {
4
4
  readonly isRequired: boolean;
5
- readonly allowsNull: boolean;
6
5
  readonly severity: IssueSeverity;
7
6
  }
8
7
  /**
9
- * A field that may be absent AND may be null needs no rule at all: that is
10
- * OPEN_PRESENCE, the policy a field with no presence rule already carries.
8
+ * The rule is emitted even when it forbids nothing, because it carries
9
+ * `nullIsValue` and that is not a permission it is the statement that null
10
+ * has to be JUDGED here rather than settled by presence. A subject with no
11
+ * presence rule carries OPEN_PRESENCE, which ends the field on null before a
12
+ * single check runs; under Draft-07 that is wrong, because `false`,
13
+ * `{"not": {}}` and an `enum` without null all forbid null while saying
14
+ * nothing about `type`. Null therefore goes to the checks, and `type`
15
+ * (declare-value-keywords.ts) is what rejects it when the document says so —
16
+ * one answer instead of two that have to agree.
11
17
  */
12
18
  export declare function declarePresenceRules(declaration: PresenceDeclaration): readonly Rule[];
@@ -23,20 +23,26 @@ exports.declarePresenceRules = declarePresenceRules;
23
23
  // ===========================================================================
24
24
  const create_rule_1 = require("../plugin-kit/create-rule");
25
25
  /**
26
- * A field that may be absent AND may be null needs no rule at all: that is
27
- * OPEN_PRESENCE, the policy a field with no presence rule already carries.
26
+ * The rule is emitted even when it forbids nothing, because it carries
27
+ * `nullIsValue` and that is not a permission it is the statement that null
28
+ * has to be JUDGED here rather than settled by presence. A subject with no
29
+ * presence rule carries OPEN_PRESENCE, which ends the field on null before a
30
+ * single check runs; under Draft-07 that is wrong, because `false`,
31
+ * `{"not": {}}` and an `enum` without null all forbid null while saying
32
+ * nothing about `type`. Null therefore goes to the checks, and `type`
33
+ * (declare-value-keywords.ts) is what rejects it when the document says so —
34
+ * one answer instead of two that have to agree.
28
35
  */
29
36
  function declarePresenceRules(declaration) {
30
- if (!declaration.isRequired && declaration.allowsNull) {
31
- return Object.freeze([]);
32
- }
33
37
  return Object.freeze([
34
38
  (0, create_rule_1.presence)({
35
39
  code: declaration.isRequired ? "required" : "type",
36
40
  severity: declaration.severity,
37
41
  allowUndefined: !declaration.isRequired,
38
- allowNull: declaration.allowsNull,
42
+ // Unreachable for null while nullIsValue is true; `type` decides.
43
+ allowNull: true,
39
44
  emptyStringIsMissing: false,
45
+ nullIsValue: true,
40
46
  describe: (messageContext) => declaration.isRequired
41
47
  ? `${messageContext.path} is required`
42
48
  : `${messageContext.path} must not be null`,
@@ -20,20 +20,26 @@
20
20
  // ===========================================================================
21
21
  import { presence } from "../plugin-kit/create-rule.mjs";
22
22
  /**
23
- * A field that may be absent AND may be null needs no rule at all: that is
24
- * OPEN_PRESENCE, the policy a field with no presence rule already carries.
23
+ * The rule is emitted even when it forbids nothing, because it carries
24
+ * `nullIsValue` and that is not a permission it is the statement that null
25
+ * has to be JUDGED here rather than settled by presence. A subject with no
26
+ * presence rule carries OPEN_PRESENCE, which ends the field on null before a
27
+ * single check runs; under Draft-07 that is wrong, because `false`,
28
+ * `{"not": {}}` and an `enum` without null all forbid null while saying
29
+ * nothing about `type`. Null therefore goes to the checks, and `type`
30
+ * (declare-value-keywords.ts) is what rejects it when the document says so —
31
+ * one answer instead of two that have to agree.
25
32
  */
26
33
  export function declarePresenceRules(declaration) {
27
- if (!declaration.isRequired && declaration.allowsNull) {
28
- return Object.freeze([]);
29
- }
30
34
  return Object.freeze([
31
35
  presence({
32
36
  code: declaration.isRequired ? "required" : "type",
33
37
  severity: declaration.severity,
34
38
  allowUndefined: !declaration.isRequired,
35
- allowNull: declaration.allowsNull,
39
+ // Unreachable for null while nullIsValue is true; `type` decides.
40
+ allowNull: true,
36
41
  emptyStringIsMissing: false,
42
+ nullIsValue: true,
37
43
  describe: (messageContext) => declaration.isRequired
38
44
  ? `${messageContext.path} is required`
39
45
  : `${messageContext.path} must not be null`,
@@ -1,3 +1,4 @@
1
1
  export { SCHEMA_BRANCH_LABEL, collectDocumentRules, jsonSchemaPlugin, } from "./json-schema";
2
+ export type { JsonSchemaOptions } from "./json-schema";
2
3
  export { NotASchemaError, UnsupportedKeywordError, RefResolutionError, buildFieldEntries, buildFromSchema, fromJsonSchema, isDraft07Schema, listBoundPluginNames, listDraft07Keywords, listFormatNames, } from "../../index";
3
4
  export type { Draft07Schema, Draft07SchemaObject, JsonSchemaBag, } from "../../index";