@maroonedog/luq 2.0.0 → 2.2.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 (165) hide show
  1. package/README.md +364 -57
  2. package/dist/builder/builder-surface.types.d.ts +1 -0
  3. package/dist/builder/create-builder.js +9 -0
  4. package/dist/builder/create-builder.mjs +9 -0
  5. package/dist/builder/field-builder.types.d.ts +13 -0
  6. package/dist/chain/bundle-paths.types.d.ts +26 -0
  7. package/dist/chain/bundle-paths.types.js +2 -0
  8. package/dist/chain/bundle-paths.types.mjs +1 -0
  9. package/dist/chain/chain-method.types.d.ts +8 -3
  10. package/dist/chain/create-chain-node.js +22 -1
  11. package/dist/chain/create-chain-node.mjs +22 -1
  12. package/dist/chain/resolve-args.types.d.ts +4 -2
  13. package/dist/compile/compile-array-node.d.ts +0 -7
  14. package/dist/compile/compile-array-node.js +4 -0
  15. package/dist/compile/compile-array-node.mjs +4 -0
  16. package/dist/compile/compile-field.js +10 -1
  17. package/dist/compile/compile-field.mjs +10 -1
  18. package/dist/compile/resolve-conditional-presence.js +1 -0
  19. package/dist/compile/resolve-conditional-presence.mjs +1 -0
  20. package/dist/compile/resolve-presence.js +4 -0
  21. package/dist/compile/resolve-presence.mjs +4 -0
  22. package/dist/compile/split-rules-by-kind.js +31 -7
  23. package/dist/compile/split-rules-by-kind.mjs +31 -7
  24. package/dist/compile/validation-plan.types.d.ts +33 -0
  25. package/dist/json-schema/build-from-schema.js +2 -3
  26. package/dist/json-schema/build-from-schema.mjs +2 -3
  27. package/dist/json-schema/collect-definitions.d.ts +30 -0
  28. package/dist/json-schema/collect-definitions.js +47 -3
  29. package/dist/json-schema/collect-definitions.mjs +46 -4
  30. package/dist/json-schema/collect-sub-schema-rules.d.ts +12 -1
  31. package/dist/json-schema/collect-sub-schema-rules.js +37 -7
  32. package/dist/json-schema/collect-sub-schema-rules.mjs +38 -8
  33. package/dist/json-schema/create-structural-context.d.ts +13 -3
  34. package/dist/json-schema/create-structural-context.js +72 -6
  35. package/dist/json-schema/create-structural-context.mjs +72 -7
  36. package/dist/json-schema/declare-additional-properties.d.ts +19 -0
  37. package/dist/json-schema/declare-additional-properties.js +42 -0
  38. package/dist/json-schema/declare-additional-properties.mjs +37 -0
  39. package/dist/json-schema/declare-object-keywords.d.ts +0 -2
  40. package/dist/json-schema/declare-object-keywords.js +6 -13
  41. package/dist/json-schema/declare-object-keywords.mjs +7 -13
  42. package/dist/json-schema/declare-presence.d.ts +9 -3
  43. package/dist/json-schema/declare-presence.js +12 -6
  44. package/dist/json-schema/declare-presence.mjs +12 -6
  45. package/dist/json-schema/extensions/json-schema/index.d.ts +1 -0
  46. package/dist/json-schema/extensions/json-schema/json-schema.d.ts +17 -2
  47. package/dist/json-schema/extensions/json-schema/json-schema.js +15 -7
  48. package/dist/json-schema/extensions/json-schema/json-schema.mjs +16 -8
  49. package/dist/json-schema/extensions/json-schema-full-feature/index.d.ts +1 -0
  50. package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.d.ts +2 -1
  51. package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.js +3 -1
  52. package/dist/json-schema/extensions/json-schema-full-feature/json-schema-full-feature.mjs +3 -1
  53. package/dist/json-schema/flatten-schema.js +1 -1
  54. package/dist/json-schema/flatten-schema.mjs +1 -1
  55. package/dist/json-schema/follow-json-pointer.d.ts +23 -0
  56. package/dist/json-schema/follow-json-pointer.js +87 -0
  57. package/dist/json-schema/follow-json-pointer.mjs +82 -0
  58. package/dist/json-schema/index.d.ts +7 -2
  59. package/dist/json-schema/index.js +10 -2
  60. package/dist/json-schema/index.mjs +5 -2
  61. package/dist/json-schema/keyword-map-object.d.ts +1 -1
  62. package/dist/json-schema/ref-resolution-error.d.ts +4 -0
  63. package/dist/json-schema/ref-resolution-error.js +20 -0
  64. package/dist/json-schema/ref-resolution-error.mjs +16 -0
  65. package/dist/json-schema/ref-scope.d.ts +18 -0
  66. package/dist/json-schema/ref-scope.js +37 -0
  67. package/dist/json-schema/ref-scope.mjs +33 -0
  68. package/dist/json-schema/resolve-ref.d.ts +14 -9
  69. package/dist/json-schema/resolve-ref.js +89 -59
  70. package/dist/json-schema/resolve-ref.mjs +86 -55
  71. package/dist/json-schema/schema-registry.d.ts +30 -0
  72. package/dist/json-schema/schema-registry.js +142 -0
  73. package/dist/json-schema/schema-registry.mjs +139 -0
  74. package/dist/json-schema/schema-to-declarations.js +2 -1
  75. package/dist/json-schema/schema-to-declarations.mjs +2 -1
  76. package/dist/json-schema/structural-expansion.types.d.ts +8 -1
  77. package/dist/json-schema/uri-reference.d.ts +29 -0
  78. package/dist/json-schema/uri-reference.js +118 -0
  79. package/dist/json-schema/uri-reference.mjs +111 -0
  80. package/dist/path/create-value-writer.js +27 -2
  81. package/dist/path/create-value-writer.mjs +27 -2
  82. package/dist/path/reserved-segment.d.ts +22 -4
  83. package/dist/path/reserved-segment.js +27 -7
  84. package/dist/path/reserved-segment.mjs +27 -7
  85. package/dist/plugin-kit/compiled-rule.d.ts +13 -0
  86. package/dist/plugin-kit/create-rule.d.ts +11 -0
  87. package/dist/plugin-kit/create-rule.js +1 -0
  88. package/dist/plugin-kit/create-rule.mjs +1 -0
  89. package/dist/plugin-kit/marker.types.d.ts +18 -0
  90. package/dist/plugin-kit/plugin-definition.d.ts +15 -0
  91. package/dist/plugin-kit/plugin-definition.js +1 -0
  92. package/dist/plugin-kit/plugin-definition.mjs +1 -0
  93. package/dist/plugins/index.generated.d.ts +1 -0
  94. package/dist/plugins/index.generated.js +4 -2
  95. package/dist/plugins/index.generated.mjs +1 -0
  96. package/dist/plugins/manifest.generated.js +1 -0
  97. package/dist/plugins/manifest.generated.mjs +1 -0
  98. package/dist/plugins/object-additional-properties/object-additional-properties-schema.d.ts +1 -1
  99. package/dist/plugins/object-additional-properties/object-additional-properties-schema.js +4 -2
  100. package/dist/plugins/object-additional-properties/object-additional-properties-schema.mjs +4 -2
  101. package/dist/plugins/object-additional-properties/object-additional-properties.d.ts +1 -1
  102. package/dist/plugins/object-additional-properties/object-additional-properties.js +4 -2
  103. package/dist/plugins/object-additional-properties/object-additional-properties.mjs +4 -2
  104. package/dist/plugins/object-additional-properties/select-additional-keys.d.ts +15 -0
  105. package/dist/plugins/object-additional-properties/select-additional-keys.js +51 -0
  106. package/dist/plugins/object-additional-properties/select-additional-keys.mjs +47 -0
  107. package/dist/plugins/stitch/stitch.d.ts +28 -8
  108. package/dist/plugins/stitch-with/index.d.ts +2 -0
  109. package/dist/plugins/stitch-with/index.js +5 -0
  110. package/dist/plugins/stitch-with/index.mjs +1 -0
  111. package/dist/plugins/stitch-with/stitch-with.d.ts +12 -0
  112. package/dist/plugins/stitch-with/stitch-with.js +94 -0
  113. package/dist/plugins/stitch-with/stitch-with.mjs +91 -0
  114. package/dist/plugins/stitchWith.d.ts +1 -0
  115. package/dist/plugins/stitchWith.js +2 -0
  116. package/dist/plugins/stitchWith.mjs +1 -0
  117. package/dist/plugins/string-exact-length/string-exact-length.js +2 -2
  118. package/dist/plugins/string-exact-length/string-exact-length.mjs +3 -3
  119. package/dist/plugins/string-max/string-max.js +2 -2
  120. package/dist/plugins/string-max/string-max.mjs +3 -3
  121. package/dist/plugins/string-min/string-min.js +21 -3
  122. package/dist/plugins/string-min/string-min.mjs +21 -3
  123. package/dist/presets/index.d.ts +1 -0
  124. package/dist/presets/index.js +9 -0
  125. package/dist/presets/index.mjs +1 -0
  126. package/dist/presets/presets.d.ts +165 -0
  127. package/dist/presets/presets.js +80 -0
  128. package/dist/presets/presets.mjs +77 -0
  129. package/dist/runtime/create-field-validator.js +24 -8
  130. package/dist/runtime/create-field-validator.mjs +24 -8
  131. package/dist/runtime/create-validator.js +38 -8
  132. package/dist/runtime/create-validator.mjs +38 -8
  133. package/dist/runtime/decide-presence.js +2 -1
  134. package/dist/runtime/decide-presence.mjs +2 -1
  135. package/dist/runtime/field-rule-context.d.ts +32 -0
  136. package/dist/runtime/field-rule-context.js +48 -0
  137. package/dist/runtime/field-rule-context.mjs +44 -0
  138. package/dist/runtime/index-stack.d.ts +29 -4
  139. package/dist/runtime/index-stack.js +76 -15
  140. package/dist/runtime/index-stack.mjs +76 -15
  141. package/dist/runtime/run-array-node.js +22 -10
  142. package/dist/runtime/run-array-node.mjs +22 -10
  143. package/dist/runtime/run-field.js +47 -17
  144. package/dist/runtime/run-field.mjs +47 -17
  145. package/dist/runtime/run-plan.js +5 -1
  146. package/dist/runtime/run-plan.mjs +5 -1
  147. package/dist/standard-schema/index.d.ts +6 -0
  148. package/dist/standard-schema/index.js +7 -0
  149. package/dist/standard-schema/index.mjs +2 -0
  150. package/dist/standard-schema/split-issue-path.d.ts +9 -0
  151. package/dist/standard-schema/split-issue-path.js +66 -0
  152. package/dist/standard-schema/split-issue-path.mjs +63 -0
  153. package/dist/standard-schema/standard-schema.types.d.ts +46 -0
  154. package/dist/standard-schema/standard-schema.types.js +13 -0
  155. package/dist/standard-schema/standard-schema.types.mjs +12 -0
  156. package/dist/standard-schema/to-standard-schema.d.ts +41 -0
  157. package/dist/standard-schema/to-standard-schema.js +60 -0
  158. package/dist/standard-schema/to-standard-schema.mjs +57 -0
  159. package/dist/standard-schema.d.ts +1 -0
  160. package/dist/standard-schema.js +2 -0
  161. package/dist/standard-schema.mjs +1 -0
  162. package/dist/types/index.d.ts +24 -0
  163. package/dist/types/index.js +18 -0
  164. package/dist/types/index.mjs +17 -0
  165. package/package.json +42 -19
@@ -1,5 +1,7 @@
1
1
  import { isSchemaObject } from "./draft07.types.mjs";
2
- import { resolveRef } from "./resolve-ref.mjs";
2
+ import { createLocalScope } from "./ref-scope.mjs";
3
+ import { resolveRefInScope } from "./resolve-ref.mjs";
4
+ import { nextBaseUri } from "./uri-reference.mjs";
3
5
  /** `true`: matches every instance. */
4
6
  const ALWAYS_SCHEMA = Object.freeze({});
5
7
  /** `false`: matches nothing. `not` of the empty schema, so no new rule kind. */
@@ -28,10 +30,50 @@ export function toSchemaObject(schema) {
28
30
  * this draft `$ref` replaces the object it appears in.
29
31
  */
30
32
  export function resolveSchemaNode(schema, root) {
33
+ return resolveSchemaNodeInScope(schema, createLocalScope(root)).node;
34
+ }
35
+ /**
36
+ * The scope-aware form, and the one the converter uses.
37
+ *
38
+ * The returned scope is not the one passed in: following a `$ref` can cross
39
+ * into another document, and a `$ref` written inside THAT document resolves
40
+ * against ITS base. Returning only the node — which is what the root-taking
41
+ * form above can do — loses that, and is why a two-document schema resolved
42
+ * its second hop against the first document.
43
+ */
44
+ export function resolveSchemaNodeInScope(schema, scope) {
31
45
  const node = toSchemaObject(schema);
32
- if (node.$ref === undefined)
33
- return node;
34
- return toSchemaObject(resolveRef(node.$ref, root));
46
+ if (node.$ref === undefined) {
47
+ // A node's own `$id` moves the base for everything inside it (§8.2).
48
+ return { node, scope: advanceBase(scope, node) };
49
+ }
50
+ // Following a `$ref` lands in whatever base the TARGET lives in, and
51
+ // resolve-ref already knows it — for a document fetched by URI that is the
52
+ // retrieval URI, which the draft says wins over the document's own `$id`.
53
+ const resolved = resolveRefInScope(node.$ref, scope);
54
+ return { node: toSchemaObject(resolved.schema), scope: resolved.scope };
55
+ }
56
+ /**
57
+ * The ONE place a base URI advances. It was also being done by the caller,
58
+ * and doing it in both meant a relative `$id` was applied twice: a `$ref`
59
+ * of "nested/foo.json" under `$id: "nested/"` went looking for
60
+ * ".../nested/nested/foo.json".
61
+ *
62
+ * A base-setting `$id` starts a new RESOURCE, so the node also becomes the
63
+ * document that `#/definitions/x` written under it resolves against. Moving
64
+ * the base without moving the document is why
65
+ * `{"$id":"a.json","properties":{"foo":{"$id":"b.json","definitions":{...},
66
+ * "allOf":[{"$ref":"#/definitions/inner"}]}}}` looked for `inner` in the
67
+ * OUTER document, where it does not exist.
68
+ */
69
+ export function advanceBase(scope, node) {
70
+ const id = node.$id;
71
+ if (typeof id !== "string" || id === "")
72
+ return scope;
73
+ const baseUri = nextBaseUri(scope.baseUri, id);
74
+ if (baseUri === scope.baseUri)
75
+ return scope;
76
+ return { ...scope, baseUri, document: node };
35
77
  }
36
78
  /**
37
79
  * The pointer a node forwards to, or undefined. Used by the flattener to spot
@@ -2,5 +2,16 @@ import type { CompositeBranch, Rule } from "../plugin-kit/compiled-rule";
2
2
  import type { Draft07Schema } from "./draft07.types";
3
3
  import type { StructuralContext } from "./structural-expansion.types";
4
4
  export declare function collectSubSchemaRules(schema: Draft07Schema, context: StructuralContext): readonly Rule[];
5
- /** Every branch of every composite is built here, so a branch is one shape. */
5
+ /**
6
+ * Every branch of every composite is built here, so a branch is one shape.
7
+ *
8
+ * The call is to the function above and NOT to `context.collectSubSchemaRules`,
9
+ * and the difference is load-bearing. `context` is already the child context
10
+ * that createStructuralContext produced by DESCENDING through this schema, so
11
+ * a `$ref` here is already recorded in `visitedRefs`. Going through the
12
+ * context would descend the same `$ref` a second time, the recursion guard
13
+ * would see it as a cycle, and the branch would come back with no rules at all
14
+ * — `{"items":[{"$ref":"#/definitions/x"}]}` constrained nothing while the
15
+ * inline form `{"items":[{"type":"integer"}]}` worked.
16
+ */
6
17
  export declare function toSchemaBranch(label: string, schema: Draft07Schema, context: StructuralContext): CompositeBranch;
@@ -24,18 +24,15 @@ const create_rule_1 = require("../plugin-kit/create-rule");
24
24
  const types_1 = require("../types");
25
25
  const collect_definitions_1 = require("./collect-definitions");
26
26
  const declare_presence_1 = require("./declare-presence");
27
- const declare_value_keywords_1 = require("./declare-value-keywords");
28
27
  const flatten_array_schema_1 = require("./flatten-array-schema");
29
28
  const schema_to_declarations_1 = require("./schema-to-declarations");
30
29
  const NO_BRANCH_FIELDS = Object.freeze([]);
31
30
  function toBranchField(child, context) {
32
- const node = (0, collect_definitions_1.resolveSchemaNode)(child.schema, context.root);
33
31
  return {
34
32
  path: child.step,
35
33
  rules: Object.freeze([
36
34
  ...(0, declare_presence_1.declarePresenceRules)({
37
35
  isRequired: child.isRequired,
38
- allowsNull: (0, declare_value_keywords_1.permitsNull)(node),
39
36
  severity: context.build.config.defaultSeverity,
40
37
  }),
41
38
  ...context.collectSubSchemaRules(child.schema),
@@ -56,9 +53,31 @@ function composeProperties(children, context) {
56
53
  }),
57
54
  ];
58
55
  }
56
+ /**
57
+ * Every sub-schema subject gets the same one-line statement: null is a VALUE
58
+ * here, so the checks run on it.
59
+ *
60
+ * src/runtime/run-field.ts settles presence BEFORE any check, and a subject
61
+ * with no presence rule carries OPEN_PRESENCE, which ends the field on null.
62
+ * A branch subject and an array element both arrive without one, so every
63
+ * check the sub-schema declared was skipped for null: `[null]` passed
64
+ * `{"items":{"type":"boolean"}}`, and `additionalItems: false` accepted a
65
+ * trailing null. Deciding it from `type` alone is not enough either —
66
+ * `false`, `{"not": {}}` and an `enum` without null forbid null while
67
+ * saying nothing about `type`.
68
+ */
69
+ function declareOwnNullPolicy(context) {
70
+ return (0, declare_presence_1.declarePresenceRules)({
71
+ isRequired: false,
72
+ severity: context.build.config.defaultSeverity,
73
+ });
74
+ }
59
75
  function collectSubSchemaRules(schema, context) {
60
- const node = (0, collect_definitions_1.resolveSchemaNode)(schema, context.root);
61
- const rules = [...(0, schema_to_declarations_1.expandSchemaRules)(node, context)];
76
+ const node = (0, collect_definitions_1.resolveSchemaNodeInScope)(schema, context.scope).node;
77
+ const rules = [
78
+ ...declareOwnNullPolicy(context),
79
+ ...(0, schema_to_declarations_1.expandSchemaRules)(node, context),
80
+ ];
62
81
  const children = (0, schema_to_declarations_1.readChildSchemas)(node);
63
82
  const properties = children.filter((child) => child.step !== flatten_array_schema_1.EACH_STEP);
64
83
  if (properties.length > 0) {
@@ -72,11 +91,22 @@ function collectSubSchemaRules(schema, context) {
72
91
  }
73
92
  return Object.freeze(rules);
74
93
  }
75
- /** Every branch of every composite is built here, so a branch is one shape. */
94
+ /**
95
+ * Every branch of every composite is built here, so a branch is one shape.
96
+ *
97
+ * The call is to the function above and NOT to `context.collectSubSchemaRules`,
98
+ * and the difference is load-bearing. `context` is already the child context
99
+ * that createStructuralContext produced by DESCENDING through this schema, so
100
+ * a `$ref` here is already recorded in `visitedRefs`. Going through the
101
+ * context would descend the same `$ref` a second time, the recursion guard
102
+ * would see it as a cycle, and the branch would come back with no rules at all
103
+ * — `{"items":[{"$ref":"#/definitions/x"}]}` constrained nothing while the
104
+ * inline form `{"items":[{"type":"integer"}]}` worked.
105
+ */
76
106
  function toSchemaBranch(label, schema, context) {
77
107
  return {
78
108
  label,
79
- rules: context.collectSubSchemaRules(schema),
109
+ rules: collectSubSchemaRules(schema, context),
80
110
  fields: NO_BRANCH_FIELDS,
81
111
  };
82
112
  }
@@ -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;