@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
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.everydayRules = exports.arrays = exports.numbers = exports.strings = exports.presence = void 0;
4
+ // ===========================================================================
5
+ // L7 src/presets/presets.ts — 事前定義したプラグイン束。
6
+ //
7
+ // なぜ要るのか。プラグインを1つずつ import するのは「使った分しか入らない」
8
+ // を成り立たせている仕組みそのものだが、最初の一本を書くのに15行の use() を
9
+ // 並べさせるのは、その正しさの押し付けである。
10
+ //
11
+ // なぜ「小さいのを複数」なのか。全部入りを1つ置くと、5個しか要らない人が
12
+ // 40個分のバイトを払う。束は型としてはただの `PluginBag` なので、必要な束
13
+ // だけを `useAll()` すればよく、混ぜても first-wins で衝突しない:
14
+ //
15
+ // Builder().useAll(presence).useAll(strings).for<User>()
16
+ //
17
+ // 何を入れるかはリポジトリ内の使用実績から決めた (test/ と docs-site/ の
18
+ // use() を数えたもの)。required 138 / stringMin 95 / numberMin 43 /
19
+ // stringEmail 33 / optional 26 という並びで、上位が束の中身になっている。
20
+ //
21
+ // バイト数は config/size-budget.json が測る。プリセットは便利さと引き換えに
22
+ // バイトを払うものなので、いくら払うかは書いてあるべきである。
23
+ // ===========================================================================
24
+ const required_1 = require("../plugins/required");
25
+ const optional_1 = require("../plugins/optional");
26
+ const nullable_1 = require("../plugins/nullable");
27
+ const string_min_1 = require("../plugins/string-min");
28
+ const string_max_1 = require("../plugins/string-max");
29
+ const string_pattern_1 = require("../plugins/string-pattern");
30
+ const string_email_1 = require("../plugins/string-email");
31
+ const number_min_1 = require("../plugins/number-min");
32
+ const number_max_1 = require("../plugins/number-max");
33
+ const number_integer_1 = require("../plugins/number-integer");
34
+ const array_min_length_1 = require("../plugins/array-min-length");
35
+ const array_max_length_1 = require("../plugins/array-max-length");
36
+ const array_each_1 = require("../plugins/array-each");
37
+ /**
38
+ * 在る・無い・null。ほぼ全ての宣言がこの三つのどれかを使う。
39
+ * 使用実績: required 138 / optional 26 / nullable 12。
40
+ */
41
+ exports.presence = Object.freeze({
42
+ required: required_1.requiredPlugin,
43
+ optional: optional_1.optionalPlugin,
44
+ nullable: nullable_1.nullablePlugin,
45
+ });
46
+ /** 文字列の定番。使用実績: stringMin 95 / stringEmail 33 / stringPattern 12。 */
47
+ exports.strings = Object.freeze({
48
+ stringMin: string_min_1.stringMinPlugin,
49
+ stringMax: string_max_1.stringMaxPlugin,
50
+ stringPattern: string_pattern_1.stringPatternPlugin,
51
+ stringEmail: string_email_1.stringEmailPlugin,
52
+ });
53
+ /** 数値の定番。使用実績: numberMin 43 / numberMax 8。 */
54
+ exports.numbers = Object.freeze({
55
+ numberMin: number_min_1.numberMinPlugin,
56
+ numberMax: number_max_1.numberMaxPlugin,
57
+ numberInteger: number_integer_1.numberIntegerPlugin,
58
+ });
59
+ /** 配列の定番。要素ごとの規則は arrayEach が運ぶ。 */
60
+ exports.arrays = Object.freeze({
61
+ arrayMinLength: array_min_length_1.arrayMinLengthPlugin,
62
+ arrayMaxLength: array_max_length_1.arrayMaxLengthPlugin,
63
+ arrayEach: array_each_1.arrayEachPlugin,
64
+ });
65
+ /**
66
+ * 上の四つを合わせたもの。13プラグイン、gzip で +1,452 B。
67
+ *
68
+ * 名前が「common」でないのは、それが何も言っていないからである
69
+ * (lint の禁止語彙にも入っている)。入っているのは presence と、文字列・数値・
70
+ * 配列それぞれの定番で、毎日書くのはこの範囲だ、という主張がこの名前である。
71
+ *
72
+ * これでも「全部入り」ではない。全部入りが要るなら `@maroonedog/luq/plugins`
73
+ * のバレルがあり、そちらは 77 個ぶん、gzip で +17,986 B を払う。
74
+ */
75
+ exports.everydayRules = Object.freeze({
76
+ ...exports.presence,
77
+ ...exports.strings,
78
+ ...exports.numbers,
79
+ ...exports.arrays,
80
+ });
@@ -0,0 +1,77 @@
1
+ // ===========================================================================
2
+ // L7 src/presets/presets.ts — 事前定義したプラグイン束。
3
+ //
4
+ // なぜ要るのか。プラグインを1つずつ import するのは「使った分しか入らない」
5
+ // を成り立たせている仕組みそのものだが、最初の一本を書くのに15行の use() を
6
+ // 並べさせるのは、その正しさの押し付けである。
7
+ //
8
+ // なぜ「小さいのを複数」なのか。全部入りを1つ置くと、5個しか要らない人が
9
+ // 40個分のバイトを払う。束は型としてはただの `PluginBag` なので、必要な束
10
+ // だけを `useAll()` すればよく、混ぜても first-wins で衝突しない:
11
+ //
12
+ // Builder().useAll(presence).useAll(strings).for<User>()
13
+ //
14
+ // 何を入れるかはリポジトリ内の使用実績から決めた (test/ と docs-site/ の
15
+ // use() を数えたもの)。required 138 / stringMin 95 / numberMin 43 /
16
+ // stringEmail 33 / optional 26 という並びで、上位が束の中身になっている。
17
+ //
18
+ // バイト数は config/size-budget.json が測る。プリセットは便利さと引き換えに
19
+ // バイトを払うものなので、いくら払うかは書いてあるべきである。
20
+ // ===========================================================================
21
+ import { requiredPlugin } from "../plugins/required/index.mjs";
22
+ import { optionalPlugin } from "../plugins/optional/index.mjs";
23
+ import { nullablePlugin } from "../plugins/nullable/index.mjs";
24
+ import { stringMinPlugin } from "../plugins/string-min/index.mjs";
25
+ import { stringMaxPlugin } from "../plugins/string-max/index.mjs";
26
+ import { stringPatternPlugin } from "../plugins/string-pattern/index.mjs";
27
+ import { stringEmailPlugin } from "../plugins/string-email/index.mjs";
28
+ import { numberMinPlugin } from "../plugins/number-min/index.mjs";
29
+ import { numberMaxPlugin } from "../plugins/number-max/index.mjs";
30
+ import { numberIntegerPlugin } from "../plugins/number-integer/index.mjs";
31
+ import { arrayMinLengthPlugin } from "../plugins/array-min-length/index.mjs";
32
+ import { arrayMaxLengthPlugin } from "../plugins/array-max-length/index.mjs";
33
+ import { arrayEachPlugin } from "../plugins/array-each/index.mjs";
34
+ /**
35
+ * 在る・無い・null。ほぼ全ての宣言がこの三つのどれかを使う。
36
+ * 使用実績: required 138 / optional 26 / nullable 12。
37
+ */
38
+ export const presence = Object.freeze({
39
+ required: requiredPlugin,
40
+ optional: optionalPlugin,
41
+ nullable: nullablePlugin,
42
+ });
43
+ /** 文字列の定番。使用実績: stringMin 95 / stringEmail 33 / stringPattern 12。 */
44
+ export const strings = Object.freeze({
45
+ stringMin: stringMinPlugin,
46
+ stringMax: stringMaxPlugin,
47
+ stringPattern: stringPatternPlugin,
48
+ stringEmail: stringEmailPlugin,
49
+ });
50
+ /** 数値の定番。使用実績: numberMin 43 / numberMax 8。 */
51
+ export const numbers = Object.freeze({
52
+ numberMin: numberMinPlugin,
53
+ numberMax: numberMaxPlugin,
54
+ numberInteger: numberIntegerPlugin,
55
+ });
56
+ /** 配列の定番。要素ごとの規則は arrayEach が運ぶ。 */
57
+ export const arrays = Object.freeze({
58
+ arrayMinLength: arrayMinLengthPlugin,
59
+ arrayMaxLength: arrayMaxLengthPlugin,
60
+ arrayEach: arrayEachPlugin,
61
+ });
62
+ /**
63
+ * 上の四つを合わせたもの。13プラグイン、gzip で +1,452 B。
64
+ *
65
+ * 名前が「common」でないのは、それが何も言っていないからである
66
+ * (lint の禁止語彙にも入っている)。入っているのは presence と、文字列・数値・
67
+ * 配列それぞれの定番で、毎日書くのはこの範囲だ、という主張がこの名前である。
68
+ *
69
+ * これでも「全部入り」ではない。全部入りが要るなら `@maroonedog/luq/plugins`
70
+ * のバレルがあり、そちらは 77 個ぶん、gzip で +17,986 B を払う。
71
+ */
72
+ export const everydayRules = Object.freeze({
73
+ ...presence,
74
+ ...strings,
75
+ ...numbers,
76
+ ...arrays,
77
+ });
@@ -56,15 +56,31 @@ function createSubjectComposer(template) {
56
56
  const write = (0, create_value_writer_1.createValueWriter)(template);
57
57
  return (siblings, value) => write(siblings ?? {}, value);
58
58
  }
59
- return (siblings, value) => mergeOntoSiblings(siblings, nestValue(template, 0, value));
59
+ const nest = compileNester(template);
60
+ return (siblings, value) => mergeOntoSiblings(siblings, nest(value));
60
61
  }
61
- function nestValue(template, from, value) {
62
- const segment = template[from];
63
- if (segment === undefined)
64
- return value;
65
- if (segment.kind === "each")
66
- return [nestValue(template, from + 1, value)];
67
- return { [segment.key]: nestValue(template, from + 1, value) };
62
+ /**
63
+ * テンプレートを **一度だけ** 歩いて、包む関数に畳んでおく。
64
+ *
65
+ * 以前はここが `nestValue(template, 0, value)` で、検証のたびにテンプレートを
66
+ * 歩き直して `segment.kind` を読み直していた。src/compile/validation-plan.types.ts
67
+ * が書いているとおり「実行時はルールが何であるかを決め直してはならない。
68
+ * 決めるのはコンパイルが済ませたこと」であり、ここはその例外になっていた。
69
+ * 非ワイルドカードの経路 (createValueWriter) は最初からこの形である。
70
+ */
71
+ function compileNester(template) {
72
+ let nest = (value) => value;
73
+ for (let index = template.length - 1; index >= 0; index -= 1) {
74
+ const segment = template[index];
75
+ if (segment === undefined)
76
+ continue;
77
+ const inner = nest;
78
+ nest =
79
+ segment.kind === "each"
80
+ ? (value) => [inner(value)]
81
+ : ((key) => (value) => ({ [key]: inner(value) }))(segment.key);
82
+ }
83
+ return nest;
68
84
  }
69
85
  /** The wrapper wins on the key it names; every other sibling survives. */
70
86
  function mergeOntoSiblings(siblings, fragment) {
@@ -53,15 +53,31 @@ function createSubjectComposer(template) {
53
53
  const write = createValueWriter(template);
54
54
  return (siblings, value) => write(siblings ?? {}, value);
55
55
  }
56
- return (siblings, value) => mergeOntoSiblings(siblings, nestValue(template, 0, value));
56
+ const nest = compileNester(template);
57
+ return (siblings, value) => mergeOntoSiblings(siblings, nest(value));
57
58
  }
58
- function nestValue(template, from, value) {
59
- const segment = template[from];
60
- if (segment === undefined)
61
- return value;
62
- if (segment.kind === "each")
63
- return [nestValue(template, from + 1, value)];
64
- return { [segment.key]: nestValue(template, from + 1, value) };
59
+ /**
60
+ * テンプレートを **一度だけ** 歩いて、包む関数に畳んでおく。
61
+ *
62
+ * 以前はここが `nestValue(template, 0, value)` で、検証のたびにテンプレートを
63
+ * 歩き直して `segment.kind` を読み直していた。src/compile/validation-plan.types.ts
64
+ * が書いているとおり「実行時はルールが何であるかを決め直してはならない。
65
+ * 決めるのはコンパイルが済ませたこと」であり、ここはその例外になっていた。
66
+ * 非ワイルドカードの経路 (createValueWriter) は最初からこの形である。
67
+ */
68
+ function compileNester(template) {
69
+ let nest = (value) => value;
70
+ for (let index = template.length - 1; index >= 0; index -= 1) {
71
+ const segment = template[index];
72
+ if (segment === undefined)
73
+ continue;
74
+ const inner = nest;
75
+ nest =
76
+ segment.kind === "each"
77
+ ? (value) => [inner(value)]
78
+ : ((key) => (value) => ({ [key]: inner(value) }))(segment.key);
79
+ }
80
+ return nest;
65
81
  }
66
82
  /** The wrapper wins on the key it names; every other sibling survives. */
67
83
  function mergeOntoSiblings(siblings, fragment) {
@@ -39,7 +39,34 @@ function rejectMissingRoot() {
39
39
  function hasRejectingIssue(issues) {
40
40
  return issues.some((issue) => issue.severity === "error");
41
41
  }
42
- function runRoot(plan, value, options, targets, shouldApplyTransforms) {
42
+ /**
43
+ * 再帰しないプランのための、共有の何もしないランナー。
44
+ *
45
+ * createRecursionRunner は WeakSet と四つのクロージャを作る。プランに
46
+ * 再帰規則が一つも無くても validate() のたびに作っていて、それが固定コストの
47
+ * 88% を占めていた — 計測で 9.7%。プランが再帰を含むかは build() 時に
48
+ * 分かることで、実行時に問い直すことではない。
49
+ *
50
+ * 呼ばれることはない。呼ばれるのは field.recursion が null でない場合だけで、
51
+ * そのときは planCanRecurse が true を返しているので本物が渡っている。
52
+ */
53
+ const NO_RECURSION = () => { };
54
+ /**
55
+ * ルートのランナーが要るか。
56
+ *
57
+ * 見るのはルートの直下と配列ノードの要素だけでよい。合成 (oneOf など) の
58
+ * 枝は run-branch が自前のランナーを作って入れ子のプランを回すので、
59
+ * ここで渡すものを使わない。
60
+ */
61
+ function planCanRecurse(plan) {
62
+ return (plan.fields.some((field) => field.recursion !== null) ||
63
+ plan.arrays.some(nodeCanRecurse));
64
+ }
65
+ function nodeCanRecurse(node) {
66
+ return (node.elementFields.some((field) => field.recursion !== null) ||
67
+ node.nested.some(nodeCanRecurse));
68
+ }
69
+ function runRoot(plan, value, options, targets, shouldApplyTransforms, canRecurse) {
43
70
  if (value === null || value === undefined)
44
71
  return rejectMissingRoot();
45
72
  const sink = new issue_sink_1.IssueSink((0, issue_sink_1.resolveAbortPolicy)(options));
@@ -48,11 +75,13 @@ function runRoot(plan, value, options, targets, shouldApplyTransforms) {
48
75
  sink,
49
76
  indices: new index_stack_1.IndexStack(),
50
77
  shouldApplyTransforms,
51
- runRecursion: (0, run_recursion_1.createRecursionRunner)({
52
- root: value,
53
- sink,
54
- external: options?.external,
55
- }),
78
+ runRecursion: canRecurse
79
+ ? (0, run_recursion_1.createRecursionRunner)({
80
+ root: value,
81
+ sink,
82
+ external: options?.external,
83
+ })
84
+ : NO_RECURSION,
56
85
  external: options?.external,
57
86
  }, targets);
58
87
  const issues = Object.freeze(sink.issues);
@@ -63,8 +92,9 @@ function runRoot(plan, value, options, targets, shouldApplyTransforms) {
63
92
  function createValidator(plan) {
64
93
  const parseTargets = (0, output_writer_1.createPlanWriteTargets)(plan);
65
94
  const shouldWriteOutput = parseTargets !== null;
95
+ const canRecurse = planCanRecurse(plan);
66
96
  return {
67
- validate: (value, options) => runRoot(plan, value, options, output_writer_1.NO_WRITE_TARGETS, false),
68
- parse: (value, options) => runRoot(plan, value, options, parseTargets ?? output_writer_1.NO_WRITE_TARGETS, shouldWriteOutput),
97
+ validate: (value, options) => runRoot(plan, value, options, output_writer_1.NO_WRITE_TARGETS, false, canRecurse),
98
+ parse: (value, options) => runRoot(plan, value, options, parseTargets ?? output_writer_1.NO_WRITE_TARGETS, shouldWriteOutput, canRecurse),
69
99
  };
70
100
  }
@@ -34,7 +34,34 @@ function rejectMissingRoot() {
34
34
  export function hasRejectingIssue(issues) {
35
35
  return issues.some((issue) => issue.severity === "error");
36
36
  }
37
- function runRoot(plan, value, options, targets, shouldApplyTransforms) {
37
+ /**
38
+ * 再帰しないプランのための、共有の何もしないランナー。
39
+ *
40
+ * createRecursionRunner は WeakSet と四つのクロージャを作る。プランに
41
+ * 再帰規則が一つも無くても validate() のたびに作っていて、それが固定コストの
42
+ * 88% を占めていた — 計測で 9.7%。プランが再帰を含むかは build() 時に
43
+ * 分かることで、実行時に問い直すことではない。
44
+ *
45
+ * 呼ばれることはない。呼ばれるのは field.recursion が null でない場合だけで、
46
+ * そのときは planCanRecurse が true を返しているので本物が渡っている。
47
+ */
48
+ const NO_RECURSION = () => { };
49
+ /**
50
+ * ルートのランナーが要るか。
51
+ *
52
+ * 見るのはルートの直下と配列ノードの要素だけでよい。合成 (oneOf など) の
53
+ * 枝は run-branch が自前のランナーを作って入れ子のプランを回すので、
54
+ * ここで渡すものを使わない。
55
+ */
56
+ function planCanRecurse(plan) {
57
+ return (plan.fields.some((field) => field.recursion !== null) ||
58
+ plan.arrays.some(nodeCanRecurse));
59
+ }
60
+ function nodeCanRecurse(node) {
61
+ return (node.elementFields.some((field) => field.recursion !== null) ||
62
+ node.nested.some(nodeCanRecurse));
63
+ }
64
+ function runRoot(plan, value, options, targets, shouldApplyTransforms, canRecurse) {
38
65
  if (value === null || value === undefined)
39
66
  return rejectMissingRoot();
40
67
  const sink = new IssueSink(resolveAbortPolicy(options));
@@ -43,11 +70,13 @@ function runRoot(plan, value, options, targets, shouldApplyTransforms) {
43
70
  sink,
44
71
  indices: new IndexStack(),
45
72
  shouldApplyTransforms,
46
- runRecursion: createRecursionRunner({
47
- root: value,
48
- sink,
49
- external: options?.external,
50
- }),
73
+ runRecursion: canRecurse
74
+ ? createRecursionRunner({
75
+ root: value,
76
+ sink,
77
+ external: options?.external,
78
+ })
79
+ : NO_RECURSION,
51
80
  external: options?.external,
52
81
  }, targets);
53
82
  const issues = Object.freeze(sink.issues);
@@ -58,8 +87,9 @@ function runRoot(plan, value, options, targets, shouldApplyTransforms) {
58
87
  export function createValidator(plan) {
59
88
  const parseTargets = createPlanWriteTargets(plan);
60
89
  const shouldWriteOutput = parseTargets !== null;
90
+ const canRecurse = planCanRecurse(plan);
61
91
  return {
62
- validate: (value, options) => runRoot(plan, value, options, NO_WRITE_TARGETS, false),
63
- parse: (value, options) => runRoot(plan, value, options, parseTargets ?? NO_WRITE_TARGETS, shouldWriteOutput),
92
+ validate: (value, options) => runRoot(plan, value, options, NO_WRITE_TARGETS, false, canRecurse),
93
+ parse: (value, options) => runRoot(plan, value, options, parseTargets ?? NO_WRITE_TARGETS, shouldWriteOutput, canRecurse),
64
94
  };
65
95
  }
@@ -6,7 +6,8 @@ const create_issue_1 = require("./create-issue");
6
6
  function decidePresence(field, value, ruleContext, sink) {
7
7
  const policy = selectPolicy(field, ruleContext);
8
8
  const isMissing = value === undefined || (policy.emptyStringIsMissing && value === "");
9
- if (!isMissing && value !== null)
9
+ // null continues to the checks when the subject says null is a value.
10
+ if (!isMissing && (value !== null || policy.nullIsValue))
10
11
  return true;
11
12
  const isAllowed = isMissing ? policy.allowUndefined : policy.allowNull;
12
13
  return reportUnlessAllowed(policy, isAllowed, value, ruleContext.path, sink);
@@ -3,7 +3,8 @@ import { createIssue } from "./create-issue.mjs";
3
3
  export function decidePresence(field, value, ruleContext, sink) {
4
4
  const policy = selectPolicy(field, ruleContext);
5
5
  const isMissing = value === undefined || (policy.emptyStringIsMissing && value === "");
6
- if (!isMissing && value !== null)
6
+ // null continues to the checks when the subject says null is a value.
7
+ if (!isMissing && (value !== null || policy.nullIsValue))
7
8
  return true;
8
9
  const isAllowed = isMissing ? policy.allowUndefined : policy.allowNull;
9
10
  return reportUnlessAllowed(policy, isAllowed, value, ruleContext.path, sink);
@@ -0,0 +1,32 @@
1
+ import type { ArrayItemContext, RuleContext } from "../types";
2
+ import type { IndexStack } from "./index-stack";
3
+ export declare class FieldRuleContext implements RuleContext {
4
+ readonly root: unknown;
5
+ private readonly indices;
6
+ /** Built at compile time, relative to the subject this field reads from. */
7
+ private readonly ownPath;
8
+ readonly item: ArrayItemContext | undefined;
9
+ readonly external: Readonly<Record<string, unknown>> | undefined;
10
+ /**
11
+ * The path once something has asked for it. `null` rather than `""` because
12
+ * the empty string is a real path — the root.
13
+ *
14
+ * It is remembered because the index stack underneath is MUTABLE and is
15
+ * popped when the element ends. A rule that kept its context and read `path`
16
+ * afterwards would otherwise see wherever the runner had moved on to.
17
+ * Reading it during the rule's own call — which is what every rule does —
18
+ * pins it.
19
+ */
20
+ private rendered;
21
+ constructor(root: unknown, indices: IndexStack,
22
+ /** Built at compile time, relative to the subject this field reads from. */
23
+ ownPath: string, item: ArrayItemContext | undefined, external: Readonly<Record<string, unknown>> | undefined);
24
+ get path(): string;
25
+ /**
26
+ * Serialising a context is a thing people do when they log a failure, and a
27
+ * prototype getter would silently drop `path` from the output. This pins the
28
+ * JSON shape to the four DECLARED members and, as a side effect, stops the
29
+ * internals above from reaching a log line.
30
+ */
31
+ toJSON(): Record<string, unknown>;
32
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FieldRuleContext = void 0;
4
+ class FieldRuleContext {
5
+ constructor(root, indices,
6
+ /** Built at compile time, relative to the subject this field reads from. */
7
+ ownPath, item, external) {
8
+ this.root = root;
9
+ this.indices = indices;
10
+ this.ownPath = ownPath;
11
+ this.item = item;
12
+ this.external = external;
13
+ /**
14
+ * The path once something has asked for it. `null` rather than `""` because
15
+ * the empty string is a real path — the root.
16
+ *
17
+ * It is remembered because the index stack underneath is MUTABLE and is
18
+ * popped when the element ends. A rule that kept its context and read `path`
19
+ * afterwards would otherwise see wherever the runner had moved on to.
20
+ * Reading it during the rule's own call — which is what every rule does —
21
+ * pins it.
22
+ */
23
+ this.rendered = null;
24
+ }
25
+ get path() {
26
+ const already = this.rendered;
27
+ if (already !== null)
28
+ return already;
29
+ const built = this.indices.renderFieldPath(this.ownPath);
30
+ this.rendered = built;
31
+ return built;
32
+ }
33
+ /**
34
+ * Serialising a context is a thing people do when they log a failure, and a
35
+ * prototype getter would silently drop `path` from the output. This pins the
36
+ * JSON shape to the four DECLARED members and, as a side effect, stops the
37
+ * internals above from reaching a log line.
38
+ */
39
+ toJSON() {
40
+ return {
41
+ root: this.root,
42
+ path: this.path,
43
+ item: this.item,
44
+ external: this.external,
45
+ };
46
+ }
47
+ }
48
+ exports.FieldRuleContext = FieldRuleContext;
@@ -0,0 +1,44 @@
1
+ export class FieldRuleContext {
2
+ constructor(root, indices,
3
+ /** Built at compile time, relative to the subject this field reads from. */
4
+ ownPath, item, external) {
5
+ this.root = root;
6
+ this.indices = indices;
7
+ this.ownPath = ownPath;
8
+ this.item = item;
9
+ this.external = external;
10
+ /**
11
+ * The path once something has asked for it. `null` rather than `""` because
12
+ * the empty string is a real path — the root.
13
+ *
14
+ * It is remembered because the index stack underneath is MUTABLE and is
15
+ * popped when the element ends. A rule that kept its context and read `path`
16
+ * afterwards would otherwise see wherever the runner had moved on to.
17
+ * Reading it during the rule's own call — which is what every rule does —
18
+ * pins it.
19
+ */
20
+ this.rendered = null;
21
+ }
22
+ get path() {
23
+ const already = this.rendered;
24
+ if (already !== null)
25
+ return already;
26
+ const built = this.indices.renderFieldPath(this.ownPath);
27
+ this.rendered = built;
28
+ return built;
29
+ }
30
+ /**
31
+ * Serialising a context is a thing people do when they log a failure, and a
32
+ * prototype getter would silently drop `path` from the output. This pins the
33
+ * JSON shape to the four DECLARED members and, as a side effect, stops the
34
+ * internals above from reaching a log line.
35
+ */
36
+ toJSON() {
37
+ return {
38
+ root: this.root,
39
+ path: this.path,
40
+ item: this.item,
41
+ external: this.external,
42
+ };
43
+ }
44
+ }
@@ -1,11 +1,30 @@
1
- import type { PathSegment } from "../path/path-segment.types";
2
1
  export declare class IndexStack {
3
- private readonly prefixes;
2
+ private readonly nodePaths;
4
3
  private readonly openIndices;
4
+ /**
5
+ * The rendered prefix, or `null` when it has not been asked for since the
6
+ * last push or pop. `null` and not `""` because the empty string is a real
7
+ * answer — the root.
8
+ *
9
+ * Invalidating rather than recomputing on push is the point: an element that
10
+ * raises no issue never renders its own prefix at all.
11
+ */
12
+ private currentPrefix;
5
13
  /** How many array levels are currently open. */
6
14
  get depth(): number;
7
15
  /** The open indices, outermost first: `[0, 2]` inside `grid[0][2]`. */
8
16
  get indices(): readonly number[];
17
+ /**
18
+ * The prefix for the currently open frames — `""` when none are open,
19
+ * `items[0]` inside one, `grid[0][2]` inside two.
20
+ *
21
+ * Indexed, never `nodePaths[nodePaths.length - 1]`. That expression at
22
+ * depth 0 is `nodePaths[-1]`, which is not an element read at all: -1 is
23
+ * outside the array, so V8 falls back to a named-property lookup and walks
24
+ * the prototype chain. The `prefix` getter used to be written that way and
25
+ * it took 12% of the array shape's self time.
26
+ */
27
+ private readTop;
9
28
  /** `""` at the root, `items[0]` inside the first element of `items`. */
10
29
  get prefix(): string;
11
30
  /**
@@ -21,8 +40,14 @@ export declare class IndexStack {
21
40
  push(nodePath: string, index: number): void;
22
41
  /** An unbalanced pop means a runner lost track of its own nesting. */
23
42
  pop(): void;
24
- /** The issue path of a field read from the subject at the current level. */
25
- renderFieldPath(template: readonly PathSegment[]): string;
43
+ /**
44
+ * The issue path of a field read from the subject at the current level.
45
+ *
46
+ * `renderedPath` was built once, at compile time. This used to walk the
47
+ * template on every call, which for an array meant rebuilding the same
48
+ * string once per element.
49
+ */
50
+ renderFieldPath(renderedPath: string): string;
26
51
  }
27
52
  /** `""` is the root path, so it never contributes a separator dot. */
28
53
  export declare function joinIssuePath(prefix: string, own: string): string;