@maroonedog/luq 2.1.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 (62) hide show
  1. package/README.md +269 -52
  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/resolve-args.types.d.ts +4 -2
  11. package/dist/compile/compile-array-node.d.ts +0 -7
  12. package/dist/compile/compile-array-node.js +4 -0
  13. package/dist/compile/compile-array-node.mjs +4 -0
  14. package/dist/compile/compile-field.js +10 -1
  15. package/dist/compile/compile-field.mjs +10 -1
  16. package/dist/compile/split-rules-by-kind.js +31 -7
  17. package/dist/compile/split-rules-by-kind.mjs +31 -7
  18. package/dist/compile/validation-plan.types.d.ts +23 -0
  19. package/dist/json-schema/flatten-schema.js +1 -1
  20. package/dist/json-schema/flatten-schema.mjs +1 -1
  21. package/dist/plugin-kit/marker.types.d.ts +18 -0
  22. package/dist/plugins/index.generated.d.ts +1 -0
  23. package/dist/plugins/index.generated.js +4 -2
  24. package/dist/plugins/index.generated.mjs +1 -0
  25. package/dist/plugins/manifest.generated.js +1 -0
  26. package/dist/plugins/manifest.generated.mjs +1 -0
  27. package/dist/plugins/stitch/stitch.d.ts +28 -8
  28. package/dist/plugins/stitch-with/index.d.ts +2 -0
  29. package/dist/plugins/stitch-with/index.js +5 -0
  30. package/dist/plugins/stitch-with/index.mjs +1 -0
  31. package/dist/plugins/stitch-with/stitch-with.d.ts +12 -0
  32. package/dist/plugins/stitch-with/stitch-with.js +94 -0
  33. package/dist/plugins/stitch-with/stitch-with.mjs +91 -0
  34. package/dist/plugins/stitchWith.d.ts +1 -0
  35. package/dist/plugins/stitchWith.js +2 -0
  36. package/dist/plugins/stitchWith.mjs +1 -0
  37. package/dist/plugins/string-min/string-min.js +21 -3
  38. package/dist/plugins/string-min/string-min.mjs +22 -4
  39. package/dist/presets/index.d.ts +1 -0
  40. package/dist/presets/index.js +9 -0
  41. package/dist/presets/index.mjs +1 -0
  42. package/dist/presets/presets.d.ts +165 -0
  43. package/dist/presets/presets.js +80 -0
  44. package/dist/presets/presets.mjs +77 -0
  45. package/dist/runtime/create-field-validator.js +24 -8
  46. package/dist/runtime/create-field-validator.mjs +24 -8
  47. package/dist/runtime/create-validator.js +38 -8
  48. package/dist/runtime/create-validator.mjs +38 -8
  49. package/dist/runtime/field-rule-context.d.ts +32 -0
  50. package/dist/runtime/field-rule-context.js +48 -0
  51. package/dist/runtime/field-rule-context.mjs +44 -0
  52. package/dist/runtime/index-stack.d.ts +29 -4
  53. package/dist/runtime/index-stack.js +76 -15
  54. package/dist/runtime/index-stack.mjs +76 -15
  55. package/dist/runtime/run-array-node.js +22 -10
  56. package/dist/runtime/run-array-node.mjs +22 -10
  57. package/dist/runtime/run-field.js +47 -17
  58. package/dist/runtime/run-field.mjs +47 -17
  59. package/dist/runtime/run-plan.js +5 -1
  60. package/dist/runtime/run-plan.mjs +5 -1
  61. package/dist/types/index.d.ts +12 -0
  62. package/package.json +35 -19
@@ -4,6 +4,7 @@ exports.APPLIES_DEFAULT_TO_NULL_BY_DEFAULT = void 0;
4
4
  exports.compileField = compileField;
5
5
  exports.compileFieldDeclaration = compileFieldDeclaration;
6
6
  const create_value_reader_1 = require("../path/create-value-reader");
7
+ const format_issue_path_1 = require("../path/format-issue-path");
7
8
  const create_value_writer_1 = require("../path/create-value-writer");
8
9
  const parse_field_path_1 = require("../path/parse-field-path");
9
10
  const resolve_conditional_presence_1 = require("./resolve-conditional-presence");
@@ -12,14 +13,22 @@ const resolve_recursion_1 = require("./resolve-recursion");
12
13
  const split_rules_by_kind_1 = require("./split-rules-by-kind");
13
14
  /** Whether a field with no explicit setting replaces a null with its default. */
14
15
  exports.APPLIES_DEFAULT_TO_NULL_BY_DEFAULT = true;
16
+ /** 添字はここでは入らない。開いている添字は実行時に接頭辞が持つ。 */
17
+ const NO_INDICES = Object.freeze([]);
15
18
  function compileField(request) {
16
19
  const byKind = (0, split_rules_by_kind_1.splitRulesByKind)(request.rules);
17
20
  const hasDefault = request.defaultOf !== null;
18
21
  const needsWriter = byKind.transforms.length > 0 || hasDefault;
19
22
  const template = Object.freeze(request.template);
23
+ // read を先に作る。ワイルドカードを含むテンプレートを拒むのは
24
+ // createValueReader の役目で、それより先に formatIssuePath を呼ぶと
25
+ // 「添字が足りない」という RangeError が、本来の PathSyntaxError を
26
+ // 追い越して出てしまう (テストがそれを捕まえた)。
27
+ const read = (0, create_value_reader_1.createValueReader)(template);
20
28
  const field = {
21
29
  template,
22
- read: (0, create_value_reader_1.createValueReader)(template),
30
+ renderedPath: (0, format_issue_path_1.formatIssuePath)(template, NO_INDICES),
31
+ read,
23
32
  write: needsWriter ? (0, create_value_writer_1.createValueWriter)(template) : null,
24
33
  defaultOf: request.defaultOf,
25
34
  applyDefaultToNull: request.applyDefaultToNull,
@@ -1,4 +1,5 @@
1
1
  import { createValueReader } from "../path/create-value-reader.mjs";
2
+ import { formatIssuePath } from "../path/format-issue-path.mjs";
2
3
  import { createValueWriter } from "../path/create-value-writer.mjs";
3
4
  import { parseFieldPath } from "../path/parse-field-path.mjs";
4
5
  import { resolveConditionalPresence } from "./resolve-conditional-presence.mjs";
@@ -7,14 +8,22 @@ import { resolveRecursion } from "./resolve-recursion.mjs";
7
8
  import { splitRulesByKind } from "./split-rules-by-kind.mjs";
8
9
  /** Whether a field with no explicit setting replaces a null with its default. */
9
10
  export const APPLIES_DEFAULT_TO_NULL_BY_DEFAULT = true;
11
+ /** 添字はここでは入らない。開いている添字は実行時に接頭辞が持つ。 */
12
+ const NO_INDICES = Object.freeze([]);
10
13
  export function compileField(request) {
11
14
  const byKind = splitRulesByKind(request.rules);
12
15
  const hasDefault = request.defaultOf !== null;
13
16
  const needsWriter = byKind.transforms.length > 0 || hasDefault;
14
17
  const template = Object.freeze(request.template);
18
+ // read を先に作る。ワイルドカードを含むテンプレートを拒むのは
19
+ // createValueReader の役目で、それより先に formatIssuePath を呼ぶと
20
+ // 「添字が足りない」という RangeError が、本来の PathSyntaxError を
21
+ // 追い越して出てしまう (テストがそれを捕まえた)。
22
+ const read = createValueReader(template);
15
23
  const field = {
16
24
  template,
17
- read: createValueReader(template),
25
+ renderedPath: formatIssuePath(template, NO_INDICES),
26
+ read,
18
27
  write: needsWriter ? createValueWriter(template) : null,
19
28
  defaultOf: request.defaultOf,
20
29
  applyDefaultToNull: request.applyDefaultToNull,
@@ -11,6 +11,30 @@ class UnknownRuleKindError extends Error {
11
11
  }
12
12
  }
13
13
  exports.UnknownRuleKindError = UnknownRuleKindError;
14
+ /**
15
+ * 空の束は一つを共有する。
16
+ *
17
+ * 七つの種のうち、どのフィールドもたいてい二つか三つしか使わない。残りは
18
+ * 空配列だが、フィールドごとに新しく Object.freeze([]) を作っていたので、
19
+ * 空であること自体は同じなのにマップの同一性だけが全部違っていた。実行時の
20
+ * `field.gates.length` や `field.transforms` の読み出しは、フィールドを
21
+ * またぐたびに別の受け手を見ることになる。
22
+ *
23
+ * この置き換えは src/compile/resolve-conditional-presence.ts の
24
+ * NO_PRESENCE_OVERRIDES と src/runtime/output-writer.ts の NO_WRITE_TARGETS が
25
+ * 既にやっていることを、残りの種にも広げただけである。
26
+ */
27
+ const NO_RULES_OF_THIS_KIND = Object.freeze([]);
28
+ /**
29
+ * 空なら共有の一つを返す。中身があるならその配列を凍結して返す。
30
+ *
31
+ * `readonly never[]` はどの `readonly R[]` にも代入できるので、共有する
32
+ * ためにアサーションを書く必要はない。型アサーションを書いてよいのは
33
+ * src/core/type-erasure.ts だけである。
34
+ */
35
+ function freezeRules(rules) {
36
+ return rules.length === 0 ? NO_RULES_OF_THIS_KIND : Object.freeze(rules);
37
+ }
14
38
  function splitRulesByKind(rules) {
15
39
  const checks = [];
16
40
  const presences = [];
@@ -38,13 +62,13 @@ function splitRulesByKind(rules) {
38
62
  rejectUnknownRuleKind(rule);
39
63
  }
40
64
  return Object.freeze({
41
- checks: Object.freeze(checks),
42
- presences: Object.freeze(presences),
43
- conditionalPresences: Object.freeze(conditionalPresences),
44
- gates: Object.freeze(gates),
45
- transforms: Object.freeze(transforms),
46
- composites: Object.freeze(composites),
47
- recursions: Object.freeze(recursions),
65
+ checks: freezeRules(checks),
66
+ presences: freezeRules(presences),
67
+ conditionalPresences: freezeRules(conditionalPresences),
68
+ gates: freezeRules(gates),
69
+ transforms: freezeRules(transforms),
70
+ composites: freezeRules(composites),
71
+ recursions: freezeRules(recursions),
48
72
  });
49
73
  }
50
74
  /** The exhaustiveness proof: the parameter is `never`, so an eighth kind is a
@@ -6,6 +6,30 @@ export class UnknownRuleKindError extends Error {
6
6
  this.name = "UnknownRuleKindError";
7
7
  }
8
8
  }
9
+ /**
10
+ * 空の束は一つを共有する。
11
+ *
12
+ * 七つの種のうち、どのフィールドもたいてい二つか三つしか使わない。残りは
13
+ * 空配列だが、フィールドごとに新しく Object.freeze([]) を作っていたので、
14
+ * 空であること自体は同じなのにマップの同一性だけが全部違っていた。実行時の
15
+ * `field.gates.length` や `field.transforms` の読み出しは、フィールドを
16
+ * またぐたびに別の受け手を見ることになる。
17
+ *
18
+ * この置き換えは src/compile/resolve-conditional-presence.ts の
19
+ * NO_PRESENCE_OVERRIDES と src/runtime/output-writer.ts の NO_WRITE_TARGETS が
20
+ * 既にやっていることを、残りの種にも広げただけである。
21
+ */
22
+ const NO_RULES_OF_THIS_KIND = Object.freeze([]);
23
+ /**
24
+ * 空なら共有の一つを返す。中身があるならその配列を凍結して返す。
25
+ *
26
+ * `readonly never[]` はどの `readonly R[]` にも代入できるので、共有する
27
+ * ためにアサーションを書く必要はない。型アサーションを書いてよいのは
28
+ * src/core/type-erasure.ts だけである。
29
+ */
30
+ function freezeRules(rules) {
31
+ return rules.length === 0 ? NO_RULES_OF_THIS_KIND : Object.freeze(rules);
32
+ }
9
33
  export function splitRulesByKind(rules) {
10
34
  const checks = [];
11
35
  const presences = [];
@@ -33,13 +57,13 @@ export function splitRulesByKind(rules) {
33
57
  rejectUnknownRuleKind(rule);
34
58
  }
35
59
  return Object.freeze({
36
- checks: Object.freeze(checks),
37
- presences: Object.freeze(presences),
38
- conditionalPresences: Object.freeze(conditionalPresences),
39
- gates: Object.freeze(gates),
40
- transforms: Object.freeze(transforms),
41
- composites: Object.freeze(composites),
42
- recursions: Object.freeze(recursions),
60
+ checks: freezeRules(checks),
61
+ presences: freezeRules(presences),
62
+ conditionalPresences: freezeRules(conditionalPresences),
63
+ gates: freezeRules(gates),
64
+ transforms: freezeRules(transforms),
65
+ composites: freezeRules(composites),
66
+ recursions: freezeRules(recursions),
43
67
  });
44
68
  }
45
69
  /** The exhaustiveness proof: the parameter is `never`, so an eighth kind is a
@@ -65,6 +65,17 @@ export interface RecursionPolicy {
65
65
  }
66
66
  export interface CompiledField {
67
67
  readonly template: readonly PathSegment[];
68
+ /**
69
+ * この フィールド自身のパスを、配列の添字を除いて描画したもの。
70
+ *
71
+ * コンパイル時に一度だけ作る。実行時に組み直していたのは、テンプレートが
72
+ * 固定なので **毎回同じ文字列を作る** ことを意味していた: 50要素・3
73
+ * フィールドの配列なら、1回の validate で 150 回、issue が1件も出なくても
74
+ * である。実測でそれが要素あたりの費用の 35% を占めていた。
75
+ *
76
+ * 実行時に残るのは、開いている添字の接頭辞と繋ぐ連結ひとつだけになる。
77
+ */
78
+ readonly renderedPath: string;
68
79
  readonly read: (subject: unknown) => unknown;
69
80
  /**
70
81
  * null unless the field declared a transform or a default — the runtime then
@@ -95,6 +106,18 @@ export interface CompiledField {
95
106
  /** Loop interchange: one array is read once however many element fields exist. */
96
107
  export interface ArrayNode {
97
108
  readonly template: readonly PathSegment[];
109
+ /**
110
+ * The node's own path, rendered once at build time — `lines`, never
111
+ * `lines[*]`, because the grouping already cut the wildcard off.
112
+ *
113
+ * CompiledField has carried its `renderedPath` since rebuilding it per
114
+ * element was measured at 35% of the per-element price. The array NODE was
115
+ * left behind and went on rendering the same string on every validate().
116
+ * Rendering can throw on a template with an unconsumed wildcard, so moving
117
+ * it here also moves that throw from validate() to build(), which is the
118
+ * direction this layer is supposed to push everything.
119
+ */
120
+ readonly renderedPath: string;
98
121
  readonly read: (subject: unknown) => unknown;
99
122
  readonly elementFields: readonly CompiledField[];
100
123
  readonly nested: readonly ArrayNode[];
@@ -91,7 +91,7 @@ function hasRootRuleKeyword(node) {
91
91
  /** The root node MINUS the three keywords the flattener applies itself. A rest
92
92
  * destructuring, so the compiler and not a comment keeps the result a schema. */
93
93
  function dropDistributedKeywords(node) {
94
- const { type, required, allOf, ...residual } = node;
94
+ const { type: _type, required: _required, allOf: _allOf, ...residual } = node;
95
95
  return residual;
96
96
  }
97
97
  /** Children of the root, merged across the `allOf` arms, first arm winning. */
@@ -86,7 +86,7 @@ function hasRootRuleKeyword(node) {
86
86
  /** The root node MINUS the three keywords the flattener applies itself. A rest
87
87
  * destructuring, so the compiler and not a comment keeps the result a schema. */
88
88
  function dropDistributedKeywords(node) {
89
- const { type, required, allOf, ...residual } = node;
89
+ const { type: _type, required: _required, allOf: _allOf, ...residual } = node;
90
90
  return residual;
91
91
  }
92
92
  /** Children of the root, merged across the `allOf` arms, first arm winning. */
@@ -45,6 +45,22 @@ export interface TransformOut {
45
45
  export interface GuardOut {
46
46
  readonly [MARKER]: "guardOut";
47
47
  }
48
+ /**
49
+ * Like GuardOut, these exist so ChainMethod can give the method a call-site
50
+ * type parameter. ResolveArgs resolves each argument against a FIXED
51
+ * TRoot/TValue and cannot make argument 2 depend on argument 1, so a plugin
52
+ * whose second argument is typed by its first has to come through this door.
53
+ *
54
+ * `StitchOut` types the FIELD VALUES a cross-field predicate receives, from
55
+ * the paths named in argument 1. `BundleOut` types the SUBJECT of a sub-chain,
56
+ * from the alias-to-path map in argument 1.
57
+ */
58
+ export interface StitchOut {
59
+ readonly [MARKER]: "stitchOut";
60
+ }
61
+ export interface BundleOut {
62
+ readonly [MARKER]: "bundleOut";
63
+ }
48
64
  export type PresenceShiftKind = "excludeMissing" | "excludeUndefined" | "excludeNull" | "allowNull";
49
65
  export interface PresenceShift<K extends PresenceShiftKind> {
50
66
  readonly [MARKER]: "presence";
@@ -75,6 +91,8 @@ export interface OutputMarkerRegistry {
75
91
  readonly unchanged: Unchanged;
76
92
  readonly transformOut: TransformOut;
77
93
  readonly guardOut: GuardOut;
94
+ readonly stitchOut: StitchOut;
95
+ readonly bundleOut: BundleOut;
78
96
  readonly excludeMissing: PresenceShift<"excludeMissing">;
79
97
  readonly excludeUndefined: PresenceShift<"excludeUndefined">;
80
98
  readonly excludeNull: PresenceShift<"excludeNull">;
@@ -41,6 +41,7 @@ export { requiredPlugin } from "./required";
41
41
  export { requiredIfPlugin } from "./required-if";
42
42
  export { skipPlugin } from "./skip";
43
43
  export { stitchPlugin } from "./stitch";
44
+ export { stitchWithPlugin } from "./stitch-with";
44
45
  export { stringAlphanumericPlugin } from "./string-alphanumeric";
45
46
  export { stringBase64Plugin } from "./string-base64";
46
47
  export { stringContentEncodingPlugin } from "./string-content-encoding";
@@ -2,8 +2,8 @@
2
2
  // GENERATED FILE - 手で編集しないでください。
3
3
  // 生成元: プラグインディレクトリ構造 (scripts/catalog/build-plugin-catalog.ts)。
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.stringDurationPlugin = exports.stringDatetimePlugin = exports.stringDatePlugin = exports.stringContentMediaTypePlugin = exports.stringContentEncodingPlugin = exports.stringBase64Plugin = exports.stringAlphanumericPlugin = exports.stitchPlugin = exports.skipPlugin = exports.requiredIfPlugin = exports.requiredPlugin = exports.readOnlyPlugin = exports.orFailPlugin = exports.optionalIfPlugin = exports.optionalPlugin = exports.oneOfPlugin = exports.objectRecursivelyPlugin = exports.objectPropertyNamesPlugin = exports.objectPatternPropertiesPlugin = exports.objectMinPropertiesPlugin = exports.objectMaxPropertiesPlugin = exports.objectDependentSchemasPlugin = exports.objectDependentRequiredPlugin = exports.objectAdditionalPropertiesSchemaPlugin = exports.objectAdditionalPropertiesPlugin = exports.objectPlugin = exports.numberRangePlugin = exports.numberPositivePlugin = exports.numberNegativePlugin = exports.numberMultipleOfPlugin = exports.numberMinPlugin = exports.numberMaxPlugin = exports.numberIntegerPlugin = exports.numberFinitePlugin = exports.nullablePlugin = exports.literalPlugin = exports.jsonSchemaFullFeaturePlugin = exports.jsonSchemaPlugin = exports.fromContextPlugin = exports.customPlugin = exports.conditionalSchemaPlugin = exports.compareFieldPlugin = exports.booleanTruthyPlugin = exports.booleanFalsyPlugin = exports.arrayUniquePlugin = exports.arrayMinLengthPlugin = exports.arrayMaxLengthPlugin = exports.arrayIncludesPlugin = exports.arrayEachPlugin = exports.arrayContainsPlugin = void 0;
6
- exports.writeOnlyPlugin = exports.validateIfPlugin = exports.uuidPlugin = exports.unionGuardPlugin = exports.tupleBuilderPlugin = exports.transformPlugin = exports.stringUrlPlugin = exports.stringUriTemplatePlugin = exports.stringUriReferencePlugin = exports.stringTimePlugin = exports.stringStartsWithPlugin = exports.stringRelativeJsonPointerPlugin = exports.stringRegexPlugin = exports.stringPatternPlugin = exports.stringMinPlugin = exports.stringMaxPlugin = exports.stringJsonPointerPlugin = exports.stringIriReferencePlugin = exports.stringIriPlugin = exports.stringIpv6Plugin = exports.stringIpv4Plugin = exports.stringIdnHostnamePlugin = exports.stringIdnEmailPlugin = exports.stringHostnamePlugin = exports.stringExactLengthPlugin = exports.stringEndsWithPlugin = exports.stringEmailPlugin = void 0;
5
+ exports.stringDatetimePlugin = exports.stringDatePlugin = exports.stringContentMediaTypePlugin = exports.stringContentEncodingPlugin = exports.stringBase64Plugin = exports.stringAlphanumericPlugin = exports.stitchWithPlugin = exports.stitchPlugin = exports.skipPlugin = exports.requiredIfPlugin = exports.requiredPlugin = exports.readOnlyPlugin = exports.orFailPlugin = exports.optionalIfPlugin = exports.optionalPlugin = exports.oneOfPlugin = exports.objectRecursivelyPlugin = exports.objectPropertyNamesPlugin = exports.objectPatternPropertiesPlugin = exports.objectMinPropertiesPlugin = exports.objectMaxPropertiesPlugin = exports.objectDependentSchemasPlugin = exports.objectDependentRequiredPlugin = exports.objectAdditionalPropertiesSchemaPlugin = exports.objectAdditionalPropertiesPlugin = exports.objectPlugin = exports.numberRangePlugin = exports.numberPositivePlugin = exports.numberNegativePlugin = exports.numberMultipleOfPlugin = exports.numberMinPlugin = exports.numberMaxPlugin = exports.numberIntegerPlugin = exports.numberFinitePlugin = exports.nullablePlugin = exports.literalPlugin = exports.jsonSchemaFullFeaturePlugin = exports.jsonSchemaPlugin = exports.fromContextPlugin = exports.customPlugin = exports.conditionalSchemaPlugin = exports.compareFieldPlugin = exports.booleanTruthyPlugin = exports.booleanFalsyPlugin = exports.arrayUniquePlugin = exports.arrayMinLengthPlugin = exports.arrayMaxLengthPlugin = exports.arrayIncludesPlugin = exports.arrayEachPlugin = exports.arrayContainsPlugin = void 0;
6
+ exports.writeOnlyPlugin = exports.validateIfPlugin = exports.uuidPlugin = exports.unionGuardPlugin = exports.tupleBuilderPlugin = exports.transformPlugin = exports.stringUrlPlugin = exports.stringUriTemplatePlugin = exports.stringUriReferencePlugin = exports.stringTimePlugin = exports.stringStartsWithPlugin = exports.stringRelativeJsonPointerPlugin = exports.stringRegexPlugin = exports.stringPatternPlugin = exports.stringMinPlugin = exports.stringMaxPlugin = exports.stringJsonPointerPlugin = exports.stringIriReferencePlugin = exports.stringIriPlugin = exports.stringIpv6Plugin = exports.stringIpv4Plugin = exports.stringIdnHostnamePlugin = exports.stringIdnEmailPlugin = exports.stringHostnamePlugin = exports.stringExactLengthPlugin = exports.stringEndsWithPlugin = exports.stringEmailPlugin = exports.stringDurationPlugin = void 0;
7
7
  var array_contains_1 = require("./array-contains");
8
8
  Object.defineProperty(exports, "arrayContainsPlugin", { enumerable: true, get: function () { return array_contains_1.arrayContainsPlugin; } });
9
9
  var array_each_1 = require("./array-each");
@@ -90,6 +90,8 @@ var skip_1 = require("./skip");
90
90
  Object.defineProperty(exports, "skipPlugin", { enumerable: true, get: function () { return skip_1.skipPlugin; } });
91
91
  var stitch_1 = require("./stitch");
92
92
  Object.defineProperty(exports, "stitchPlugin", { enumerable: true, get: function () { return stitch_1.stitchPlugin; } });
93
+ var stitch_with_1 = require("./stitch-with");
94
+ Object.defineProperty(exports, "stitchWithPlugin", { enumerable: true, get: function () { return stitch_with_1.stitchWithPlugin; } });
93
95
  var string_alphanumeric_1 = require("./string-alphanumeric");
94
96
  Object.defineProperty(exports, "stringAlphanumericPlugin", { enumerable: true, get: function () { return string_alphanumeric_1.stringAlphanumericPlugin; } });
95
97
  var string_base64_1 = require("./string-base64");
@@ -43,6 +43,7 @@ export { requiredPlugin } from "./required/index.mjs";
43
43
  export { requiredIfPlugin } from "./required-if/index.mjs";
44
44
  export { skipPlugin } from "./skip/index.mjs";
45
45
  export { stitchPlugin } from "./stitch/index.mjs";
46
+ export { stitchWithPlugin } from "./stitch-with/index.mjs";
46
47
  export { stringAlphanumericPlugin } from "./string-alphanumeric/index.mjs";
47
48
  export { stringBase64Plugin } from "./string-base64/index.mjs";
48
49
  export { stringContentEncodingPlugin } from "./string-content-encoding/index.mjs";
@@ -46,6 +46,7 @@ exports.PLUGIN_MANIFEST = [
46
46
  { directoryName: "required-if", subpathName: "requiredIf", tier: "isolated", entryFile: "src/plugins/required-if/index.ts", exportedSymbols: ["requiredIfPlugin"] },
47
47
  { directoryName: "skip", subpathName: "skip", tier: "isolated", entryFile: "src/plugins/skip/index.ts", exportedSymbols: ["skipPlugin"] },
48
48
  { directoryName: "stitch", subpathName: "stitch", tier: "isolated", entryFile: "src/plugins/stitch/index.ts", exportedSymbols: ["stitchPlugin"] },
49
+ { directoryName: "stitch-with", subpathName: "stitchWith", tier: "isolated", entryFile: "src/plugins/stitch-with/index.ts", exportedSymbols: ["stitchWithPlugin"] },
49
50
  { directoryName: "string-alphanumeric", subpathName: "stringAlphanumeric", tier: "isolated", entryFile: "src/plugins/string-alphanumeric/index.ts", exportedSymbols: ["stringAlphanumericPlugin"] },
50
51
  { directoryName: "string-base64", subpathName: "stringBase64", tier: "isolated", entryFile: "src/plugins/string-base64/index.ts", exportedSymbols: ["stringBase64Plugin"] },
51
52
  { directoryName: "string-content-encoding", subpathName: "stringContentEncoding", tier: "isolated", entryFile: "src/plugins/string-content-encoding/index.ts", exportedSymbols: ["stringContentEncodingPlugin"] },
@@ -43,6 +43,7 @@ export const PLUGIN_MANIFEST = [
43
43
  { directoryName: "required-if", subpathName: "requiredIf", tier: "isolated", entryFile: "src/plugins/required-if/index.ts", exportedSymbols: ["requiredIfPlugin"] },
44
44
  { directoryName: "skip", subpathName: "skip", tier: "isolated", entryFile: "src/plugins/skip/index.ts", exportedSymbols: ["skipPlugin"] },
45
45
  { directoryName: "stitch", subpathName: "stitch", tier: "isolated", entryFile: "src/plugins/stitch/index.ts", exportedSymbols: ["stitchPlugin"] },
46
+ { directoryName: "stitch-with", subpathName: "stitchWith", tier: "isolated", entryFile: "src/plugins/stitch-with/index.ts", exportedSymbols: ["stitchWithPlugin"] },
46
47
  { directoryName: "string-alphanumeric", subpathName: "stringAlphanumeric", tier: "isolated", entryFile: "src/plugins/string-alphanumeric/index.ts", exportedSymbols: ["stringAlphanumericPlugin"] },
47
48
  { directoryName: "string-base64", subpathName: "stringBase64", tier: "isolated", entryFile: "src/plugins/string-base64/index.ts", exportedSymbols: ["stringBase64Plugin"] },
48
49
  { directoryName: "string-content-encoding", subpathName: "stringContentEncoding", tier: "isolated", entryFile: "src/plugins/string-content-encoding/index.ts", exportedSymbols: ["stringContentEncodingPlugin"] },
@@ -1,6 +1,16 @@
1
+ import type { CrossFieldOutcome } from "../../types";
1
2
  import type { PickPaths } from "../../path/index";
2
- import type { FieldRefs, Unchanged } from "../../plugin-kit/marker.types";
3
- /** The bundle handed to the check, keyed by the path exactly as declared. */
3
+ import type { FieldRefs, StitchOut } from "../../plugin-kit/marker.types";
4
+ /**
5
+ * The bundle handed to the check at RUN TIME, keyed by the path exactly as
6
+ * declared.
7
+ *
8
+ * 呼び出し側がこれを見ることはもう無い。`.stitch(["price"], ...)` と書いた
9
+ * 時点でパスの集合は分かっているので、述語が受け取る束の型は
10
+ * `PickPaths<TRoot, F>` として組まれる (src/chain/chain-method.types.ts の
11
+ * StitchOut の腕)。この型が残っているのは、実行時に集める側が「キーは
12
+ * パス文字列」という事実を書き留めておく場所だからである。
13
+ */
4
14
  export type StitchFieldValues = Readonly<Record<string, unknown>>;
5
15
  /**
6
16
  * PickPaths applied to a declared tuple: `StitchFieldsOf<Order, ["price",
@@ -11,11 +21,21 @@ export type StitchFieldValues = Readonly<Record<string, unknown>>;
11
21
  * the same alternative-2 move readExternalContext makes for async context.
12
22
  */
13
23
  export type StitchFieldsOf<TRoot, TFields extends readonly string[]> = PickPaths<TRoot, TFields>;
14
- /** 1.x's `{ valid, message? }`, unchanged. */
15
- export interface StitchOutcome {
16
- readonly valid: boolean;
17
- readonly message?: string;
18
- }
24
+ /**
25
+ * 1.x の `{ valid, message? }`、名前も形もそのまま。実体は src/types の
26
+ * CrossFieldOutcome で、チェーン層が呼び出し側の型を組むのに参照する —
27
+ * L3 から L7 を import しないための置き場である。
28
+ */
29
+ export type StitchOutcome = CrossFieldOutcome;
30
+ /**
31
+ * 実行時に build() が受け取る形。**呼び出し側が見る型ではない。**
32
+ *
33
+ * `.stitch(["price", "quantity"], ...)` と書いた時点でパスの集合は分かって
34
+ * いるので、述語が受け取る束は `PickPaths<TRoot, F>` として組まれ、キーごとに
35
+ * 値の型が付く (src/chain/chain-method.types.ts の StitchOut の腕)。
36
+ * ここが `Record<string, unknown>` のままだったのが、その腕を足すまでの
37
+ * stitch である。
38
+ */
19
39
  export type StitchCheck = (fieldValues: StitchFieldValues, value: unknown, root: unknown) => StitchOutcome;
20
40
  /** 1.x's messageFactory context for this plugin, minus the re-run. */
21
41
  export interface StitchExtra {
@@ -25,6 +45,6 @@ export interface StitchExtra {
25
45
  }
26
46
  export declare const stitchPlugin: import("../../plugin-kit/plugin-definition").PluginDefinition<"stitch", "stitch", readonly ["string", "number", "boolean", "date", "object", "array", "tuple", "union"], {
27
47
  args: readonly [fields: FieldRefs, check: StitchCheck];
28
- out: Unchanged;
48
+ out: StitchOut;
29
49
  context: StitchExtra;
30
50
  }>;
@@ -0,0 +1,2 @@
1
+ export { stitchWithPlugin } from "./stitch-with";
2
+ export type { BundleAliasMap, StitchWithExtra } from "./stitch-with";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stitchWithPlugin = void 0;
4
+ var stitch_with_1 = require("./stitch-with");
5
+ Object.defineProperty(exports, "stitchWithPlugin", { enumerable: true, get: function () { return stitch_with_1.stitchWithPlugin; } });
@@ -0,0 +1 @@
1
+ export { stitchWithPlugin } from "./stitch-with.mjs";
@@ -0,0 +1,12 @@
1
+ import type { MessageContextExtra } from "../../types";
2
+ import type { BundleOut, NarrowedChain } from "../../plugin-kit/marker.types";
3
+ /** 別名 -> ルートのパス。実行時はただの文字列の対応表である。 */
4
+ export type BundleAliasMap = Readonly<Record<string, string>>;
5
+ export interface StitchWithExtra extends MessageContextExtra {
6
+ readonly aliases: readonly string[];
7
+ }
8
+ export declare const stitchWithPlugin: import("../../plugin-kit/plugin-definition").PluginDefinition<"stitchWith", "stitchWith", readonly ["string", "number", "boolean", "date", "object", "array", "tuple", "union"], {
9
+ args: readonly [fields: BundleAliasMap, define: NarrowedChain];
10
+ out: BundleOut;
11
+ context: StitchWithExtra;
12
+ }>;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stitchWithPlugin = void 0;
4
+ // ===========================================================================
5
+ // L7 src/plugins/stitch-with/stitch-with.ts — EXPERIMENTAL.
6
+ // `stitch` の型付き後継。クロスフィールド検証のためのメソッドである。
7
+ //
8
+ // stitch の核は「**複数のフィールドを1つの判定にまとめる**」ことなので、
9
+ // 主体は束そのものであって、別名ごとではない。別名ごとにルールを並べる形も
10
+ // 書けるが、それでは `total === price * quantity` が書けず、stitch では
11
+ // なくなる。
12
+ //
13
+ // stitch との違いは一点だけである。stitch は束を
14
+ // `Readonly<Record<string, unknown>>` として手書きの述語に渡すので、束の中身に
15
+ // ついて型が何も言わない。ここでは束が対応表から組まれて **型が付く**:
16
+ //
17
+ // .v("total", (b) => b.number.stitchWith(
18
+ // { cost: "price", count: "quantity" },
19
+ // (f) => f.object.custom((bundle) => bundle.cost * bundle.count === 100)
20
+ // ))
21
+ //
22
+ // `bundle` は `{ cost: number; count: number }` であって Record ではない。
23
+ // メンバー名を綴り違えれば、型を取り違えれば、コンパイルエラーになる。
24
+ //
25
+ // なぜ別名を経由するのか。束をパス文字列でキーすると、その文字列は宣言の場で
26
+ // **パスとして** 解釈される: `"user.name"` は束の中の `user.name` を探しに
27
+ // 行き、束は平たいので見つからない (実測して分かった)。別名は素の識別子なので
28
+ // その衝突が起きず、参照できるのは宣言した別名だけになる。
29
+ //
30
+ // このファイルに判定は無い。サブチェーンは NarrowedChain と同じ経路で
31
+ // `readonly Rule[]` に解決され、branch がそれを枝にし、エンジンが走らせる。
32
+ // 束専用の収集器をコアに置く案も作って動かしたが、実測でコアが 220 B 増えた
33
+ // (7,590 -> 7,810 B)。stitchWith を使わない利用者が払う形なので採らなかった。
34
+ // 既存の経路に乗せると追加は 0 B である。
35
+ //
36
+ // 1.x が同じ責務の実装を3つ持っていたのは、ここで「もう1つ書く」を選んだ
37
+ // からである。
38
+ // ===========================================================================
39
+ const types_1 = require("../../types");
40
+ const create_rule_1 = require("../../plugin-kit/create-rule");
41
+ const plugin_definition_1 = require("../../plugin-kit/plugin-definition");
42
+ const index_1 = require("../../path/index");
43
+ const STITCH_WITH_SLOTS = [
44
+ "string",
45
+ "number",
46
+ "boolean",
47
+ "date",
48
+ "object",
49
+ "array",
50
+ "tuple",
51
+ "union",
52
+ ];
53
+ const BUNDLE_BRANCH_LABEL = "bundle";
54
+ function readMembers(aliasMap) {
55
+ return Object.freeze(Object.entries(aliasMap).map(([alias, path]) => ({
56
+ alias,
57
+ read: (0, index_1.createValueReader)((0, index_1.parseFieldPath)(path)),
58
+ })));
59
+ }
60
+ /** ルートから束を組む。枝の主体はこのオブジェクトになる。 */
61
+ function collectBundle(members, root) {
62
+ const bundle = {};
63
+ for (const member of members)
64
+ bundle[member.alias] = member.read(root);
65
+ return bundle;
66
+ }
67
+ exports.stitchWithPlugin = (0, plugin_definition_1.definePlugin)()({
68
+ name: "stitchWith",
69
+ method: "stitchWith",
70
+ slots: STITCH_WITH_SLOTS,
71
+ build: (ctx, aliasMap, rules) => {
72
+ const members = readMembers((0, types_1.isPlainObject)(aliasMap) ? aliasMap : {});
73
+ const aliases = Object.freeze(members.map((member) => member.alias));
74
+ return (0, create_rule_1.composite)({
75
+ code: ctx.code,
76
+ messageFactory: ctx.messageFactory,
77
+ severity: ctx.severity,
78
+ branches: [(0, create_rule_1.branch)(BUNDLE_BRANCH_LABEL, rules)],
79
+ // 主体の値は見ない。見るのはルートから組んだ束だけである。
80
+ combine: (runners) => {
81
+ const runner = runners[0];
82
+ if (runner === undefined)
83
+ return () => types_1.PASS;
84
+ return (_value, runCtx) => {
85
+ const outcome = runner.run(collectBundle(members, runCtx.root), runCtx);
86
+ return outcome.ok ? types_1.PASS : (0, types_1.fail)({ ...outcome.detail });
87
+ };
88
+ },
89
+ describe: (_detail, msgCtx) => `Cross-field validation failed for ${msgCtx.path}`,
90
+ buildMessageContext: () => ({ aliases }),
91
+ });
92
+ },
93
+ subChainArguments: [1],
94
+ });
@@ -0,0 +1,91 @@
1
+ // ===========================================================================
2
+ // L7 src/plugins/stitch-with/stitch-with.ts — EXPERIMENTAL.
3
+ // `stitch` の型付き後継。クロスフィールド検証のためのメソッドである。
4
+ //
5
+ // stitch の核は「**複数のフィールドを1つの判定にまとめる**」ことなので、
6
+ // 主体は束そのものであって、別名ごとではない。別名ごとにルールを並べる形も
7
+ // 書けるが、それでは `total === price * quantity` が書けず、stitch では
8
+ // なくなる。
9
+ //
10
+ // stitch との違いは一点だけである。stitch は束を
11
+ // `Readonly<Record<string, unknown>>` として手書きの述語に渡すので、束の中身に
12
+ // ついて型が何も言わない。ここでは束が対応表から組まれて **型が付く**:
13
+ //
14
+ // .v("total", (b) => b.number.stitchWith(
15
+ // { cost: "price", count: "quantity" },
16
+ // (f) => f.object.custom((bundle) => bundle.cost * bundle.count === 100)
17
+ // ))
18
+ //
19
+ // `bundle` は `{ cost: number; count: number }` であって Record ではない。
20
+ // メンバー名を綴り違えれば、型を取り違えれば、コンパイルエラーになる。
21
+ //
22
+ // なぜ別名を経由するのか。束をパス文字列でキーすると、その文字列は宣言の場で
23
+ // **パスとして** 解釈される: `"user.name"` は束の中の `user.name` を探しに
24
+ // 行き、束は平たいので見つからない (実測して分かった)。別名は素の識別子なので
25
+ // その衝突が起きず、参照できるのは宣言した別名だけになる。
26
+ //
27
+ // このファイルに判定は無い。サブチェーンは NarrowedChain と同じ経路で
28
+ // `readonly Rule[]` に解決され、branch がそれを枝にし、エンジンが走らせる。
29
+ // 束専用の収集器をコアに置く案も作って動かしたが、実測でコアが 220 B 増えた
30
+ // (7,590 -> 7,810 B)。stitchWith を使わない利用者が払う形なので採らなかった。
31
+ // 既存の経路に乗せると追加は 0 B である。
32
+ //
33
+ // 1.x が同じ責務の実装を3つ持っていたのは、ここで「もう1つ書く」を選んだ
34
+ // からである。
35
+ // ===========================================================================
36
+ import { PASS, fail, isPlainObject } from "../../types/index.mjs";
37
+ import { branch, composite } from "../../plugin-kit/create-rule.mjs";
38
+ import { definePlugin } from "../../plugin-kit/plugin-definition.mjs";
39
+ import { createValueReader, parseFieldPath } from "../../path/index.mjs";
40
+ const STITCH_WITH_SLOTS = [
41
+ "string",
42
+ "number",
43
+ "boolean",
44
+ "date",
45
+ "object",
46
+ "array",
47
+ "tuple",
48
+ "union",
49
+ ];
50
+ const BUNDLE_BRANCH_LABEL = "bundle";
51
+ function readMembers(aliasMap) {
52
+ return Object.freeze(Object.entries(aliasMap).map(([alias, path]) => ({
53
+ alias,
54
+ read: createValueReader(parseFieldPath(path)),
55
+ })));
56
+ }
57
+ /** ルートから束を組む。枝の主体はこのオブジェクトになる。 */
58
+ function collectBundle(members, root) {
59
+ const bundle = {};
60
+ for (const member of members)
61
+ bundle[member.alias] = member.read(root);
62
+ return bundle;
63
+ }
64
+ export const stitchWithPlugin = /*#__PURE__*/ definePlugin()({
65
+ name: "stitchWith",
66
+ method: "stitchWith",
67
+ slots: STITCH_WITH_SLOTS,
68
+ build: (ctx, aliasMap, rules) => {
69
+ const members = readMembers(isPlainObject(aliasMap) ? aliasMap : {});
70
+ const aliases = Object.freeze(members.map((member) => member.alias));
71
+ return composite({
72
+ code: ctx.code,
73
+ messageFactory: ctx.messageFactory,
74
+ severity: ctx.severity,
75
+ branches: [branch(BUNDLE_BRANCH_LABEL, rules)],
76
+ // 主体の値は見ない。見るのはルートから組んだ束だけである。
77
+ combine: (runners) => {
78
+ const runner = runners[0];
79
+ if (runner === undefined)
80
+ return () => PASS;
81
+ return (_value, runCtx) => {
82
+ const outcome = runner.run(collectBundle(members, runCtx.root), runCtx);
83
+ return outcome.ok ? PASS : fail({ ...outcome.detail });
84
+ };
85
+ },
86
+ describe: (_detail, msgCtx) => `Cross-field validation failed for ${msgCtx.path}`,
87
+ buildMessageContext: () => ({ aliases }),
88
+ });
89
+ },
90
+ subChainArguments: [1],
91
+ });
@@ -0,0 +1 @@
1
+ export * from "./stitch-with/index";
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ module.exports = require("./stitch-with/index");
@@ -0,0 +1 @@
1
+ export * from "./stitch-with/index.mjs";
@@ -23,9 +23,27 @@ exports.stringMinPlugin = (0, plugin_definition_1.definePlugin)()({
23
23
  code: ctx.code,
24
24
  messageFactory: ctx.messageFactory,
25
25
  severity: ctx.severity,
26
- run: (value) => !(0, types_1.isString)(value) || (0, types_1.countCodePoints)(value) >= min
27
- ? types_1.PASS
28
- : (0, types_1.fail)({ expected: min, actual: (0, types_1.countCodePoints)(value) }),
26
+ // min に届いた時点で止める。全長を数える必要があるのは**落ちる**
27
+ // ときだけで、そのときは値が min より短いのだから走査も短い。
28
+ // 元は countCodePoints を最大二度呼び、どちらも文字列を最後まで
29
+ // 歩いていた。符号位置で数えるのは変えない (UTF-16 単位ではない) —
30
+ // 変えているのは、いつ止めるかだけである。受理パスで 12.7%。
31
+ run: (value) => {
32
+ if (!(0, types_1.isString)(value))
33
+ return types_1.PASS;
34
+ // min が 0 なら空文字列も通る。ループは空文字列で一度も回らないので、
35
+ // この行が無いと `""` が actual 0 で落ちる — 早期脱出に書き換えた
36
+ // ときに実際に開いた穴で、既存のテストは一件も気づかなかった。
37
+ if (min === 0)
38
+ return types_1.PASS;
39
+ let count = 0;
40
+ for (const _character of value) {
41
+ count += 1;
42
+ if (count >= min)
43
+ return types_1.PASS;
44
+ }
45
+ return (0, types_1.fail)({ expected: min, actual: count });
46
+ },
29
47
  describe: (detail) => `String must have at least ${String(min)} characters, but got ` +
30
48
  `${String(detail.actual)}`,
31
49
  buildMessageContext: (detail) => ({