@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.
- package/README.md +269 -52
- package/dist/builder/builder-surface.types.d.ts +1 -0
- package/dist/builder/create-builder.js +9 -0
- package/dist/builder/create-builder.mjs +9 -0
- package/dist/builder/field-builder.types.d.ts +13 -0
- package/dist/chain/bundle-paths.types.d.ts +26 -0
- package/dist/chain/bundle-paths.types.js +2 -0
- package/dist/chain/bundle-paths.types.mjs +1 -0
- package/dist/chain/chain-method.types.d.ts +8 -3
- package/dist/chain/resolve-args.types.d.ts +4 -2
- package/dist/compile/compile-array-node.d.ts +0 -7
- package/dist/compile/compile-array-node.js +4 -0
- package/dist/compile/compile-array-node.mjs +4 -0
- package/dist/compile/compile-field.js +10 -1
- package/dist/compile/compile-field.mjs +10 -1
- package/dist/compile/split-rules-by-kind.js +31 -7
- package/dist/compile/split-rules-by-kind.mjs +31 -7
- package/dist/compile/validation-plan.types.d.ts +23 -0
- package/dist/json-schema/flatten-schema.js +1 -1
- package/dist/json-schema/flatten-schema.mjs +1 -1
- package/dist/plugin-kit/marker.types.d.ts +18 -0
- package/dist/plugins/index.generated.d.ts +1 -0
- package/dist/plugins/index.generated.js +4 -2
- package/dist/plugins/index.generated.mjs +1 -0
- package/dist/plugins/manifest.generated.js +1 -0
- package/dist/plugins/manifest.generated.mjs +1 -0
- package/dist/plugins/stitch/stitch.d.ts +28 -8
- package/dist/plugins/stitch-with/index.d.ts +2 -0
- package/dist/plugins/stitch-with/index.js +5 -0
- package/dist/plugins/stitch-with/index.mjs +1 -0
- package/dist/plugins/stitch-with/stitch-with.d.ts +12 -0
- package/dist/plugins/stitch-with/stitch-with.js +94 -0
- package/dist/plugins/stitch-with/stitch-with.mjs +91 -0
- package/dist/plugins/stitchWith.d.ts +1 -0
- package/dist/plugins/stitchWith.js +2 -0
- package/dist/plugins/stitchWith.mjs +1 -0
- package/dist/plugins/string-min/string-min.js +21 -3
- package/dist/plugins/string-min/string-min.mjs +22 -4
- package/dist/presets/index.d.ts +1 -0
- package/dist/presets/index.js +9 -0
- package/dist/presets/index.mjs +1 -0
- package/dist/presets/presets.d.ts +165 -0
- package/dist/presets/presets.js +80 -0
- package/dist/presets/presets.mjs +77 -0
- package/dist/runtime/create-field-validator.js +24 -8
- package/dist/runtime/create-field-validator.mjs +24 -8
- package/dist/runtime/create-validator.js +38 -8
- package/dist/runtime/create-validator.mjs +38 -8
- package/dist/runtime/field-rule-context.d.ts +32 -0
- package/dist/runtime/field-rule-context.js +48 -0
- package/dist/runtime/field-rule-context.mjs +44 -0
- package/dist/runtime/index-stack.d.ts +29 -4
- package/dist/runtime/index-stack.js +76 -15
- package/dist/runtime/index-stack.mjs +76 -15
- package/dist/runtime/run-array-node.js +22 -10
- package/dist/runtime/run-array-node.mjs +22 -10
- package/dist/runtime/run-field.js +47 -17
- package/dist/runtime/run-field.mjs +47 -17
- package/dist/runtime/run-plan.js +5 -1
- package/dist/runtime/run-plan.mjs +5 -1
- package/dist/types/index.d.ts +12 -0
- package/package.json +35 -19
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// ===========================================================================
|
|
8
8
|
import { check } from "../../plugin-kit/create-rule.mjs";
|
|
9
9
|
import { PluginArgumentError, definePlugin, } from "../../plugin-kit/plugin-definition.mjs";
|
|
10
|
-
import { PASS, fail, isNumber,
|
|
10
|
+
import { PASS, fail, isNumber, isString } from "../../types/index.mjs";
|
|
11
11
|
export const stringMinPlugin = /*#__PURE__*/ definePlugin()({
|
|
12
12
|
name: "stringMin",
|
|
13
13
|
method: "min",
|
|
@@ -20,9 +20,27 @@ export const stringMinPlugin = /*#__PURE__*/ definePlugin()({
|
|
|
20
20
|
code: ctx.code,
|
|
21
21
|
messageFactory: ctx.messageFactory,
|
|
22
22
|
severity: ctx.severity,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// min に届いた時点で止める。全長を数える必要があるのは**落ちる**
|
|
24
|
+
// ときだけで、そのときは値が min より短いのだから走査も短い。
|
|
25
|
+
// 元は countCodePoints を最大二度呼び、どちらも文字列を最後まで
|
|
26
|
+
// 歩いていた。符号位置で数えるのは変えない (UTF-16 単位ではない) —
|
|
27
|
+
// 変えているのは、いつ止めるかだけである。受理パスで 12.7%。
|
|
28
|
+
run: (value) => {
|
|
29
|
+
if (!isString(value))
|
|
30
|
+
return PASS;
|
|
31
|
+
// min が 0 なら空文字列も通る。ループは空文字列で一度も回らないので、
|
|
32
|
+
// この行が無いと `""` が actual 0 で落ちる — 早期脱出に書き換えた
|
|
33
|
+
// ときに実際に開いた穴で、既存のテストは一件も気づかなかった。
|
|
34
|
+
if (min === 0)
|
|
35
|
+
return PASS;
|
|
36
|
+
let count = 0;
|
|
37
|
+
for (const _character of value) {
|
|
38
|
+
count += 1;
|
|
39
|
+
if (count >= min)
|
|
40
|
+
return PASS;
|
|
41
|
+
}
|
|
42
|
+
return fail({ expected: min, actual: count });
|
|
43
|
+
},
|
|
26
44
|
describe: (detail) => `String must have at least ${String(min)} characters, but got ` +
|
|
27
45
|
`${String(detail.actual)}`,
|
|
28
46
|
buildMessageContext: (detail) => ({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { arrays, everydayRules, numbers, presence, strings } from "./presets";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.strings = exports.presence = exports.numbers = exports.everydayRules = exports.arrays = void 0;
|
|
4
|
+
var presets_1 = require("./presets");
|
|
5
|
+
Object.defineProperty(exports, "arrays", { enumerable: true, get: function () { return presets_1.arrays; } });
|
|
6
|
+
Object.defineProperty(exports, "everydayRules", { enumerable: true, get: function () { return presets_1.everydayRules; } });
|
|
7
|
+
Object.defineProperty(exports, "numbers", { enumerable: true, get: function () { return presets_1.numbers; } });
|
|
8
|
+
Object.defineProperty(exports, "presence", { enumerable: true, get: function () { return presets_1.presence; } });
|
|
9
|
+
Object.defineProperty(exports, "strings", { enumerable: true, get: function () { return presets_1.strings; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { arrays, everydayRules, numbers, presence, strings } from "./presets.mjs";
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 在る・無い・null。ほぼ全ての宣言がこの三つのどれかを使う。
|
|
3
|
+
* 使用実績: required 138 / optional 26 / nullable 12。
|
|
4
|
+
*/
|
|
5
|
+
export declare const presence: Readonly<{
|
|
6
|
+
required: import("..").PluginDefinition<"required", "required", readonly import("..").TypeName[], {
|
|
7
|
+
args: readonly [];
|
|
8
|
+
out: import("../plugin-kit").PresenceShift<"excludeMissing">;
|
|
9
|
+
context: import("..").MessageContextExtra;
|
|
10
|
+
}>;
|
|
11
|
+
optional: import("..").PluginDefinition<"optional", "optional", readonly import("..").TypeName[], {
|
|
12
|
+
args: readonly [];
|
|
13
|
+
out: import("../plugin-kit").PresenceShift<"excludeNull">;
|
|
14
|
+
context: import("..").MessageContextExtra;
|
|
15
|
+
}>;
|
|
16
|
+
nullable: import("..").PluginDefinition<"nullable", "nullable", readonly import("..").TypeName[], {
|
|
17
|
+
args: readonly [];
|
|
18
|
+
out: import("../plugin-kit").PresenceShift<"allowNull">;
|
|
19
|
+
context: import("..").MessageContextExtra;
|
|
20
|
+
}>;
|
|
21
|
+
}>;
|
|
22
|
+
/** 文字列の定番。使用実績: stringMin 95 / stringEmail 33 / stringPattern 12。 */
|
|
23
|
+
export declare const strings: Readonly<{
|
|
24
|
+
stringMin: import("..").PluginDefinition<"stringMin", "min", readonly ["string"], {
|
|
25
|
+
args: readonly [min: number];
|
|
26
|
+
out: import("../plugin-kit").Unchanged;
|
|
27
|
+
context: import("../plugins/string-min/string-min").StringMinContext;
|
|
28
|
+
}>;
|
|
29
|
+
stringMax: import("..").PluginDefinition<"stringMax", "max", readonly ["string"], {
|
|
30
|
+
args: readonly [max: number];
|
|
31
|
+
out: import("../plugin-kit").Unchanged;
|
|
32
|
+
context: import("../plugins/string-max/string-max").StringMaxContext;
|
|
33
|
+
}>;
|
|
34
|
+
stringPattern: import("..").PluginDefinition<"stringPattern", "pattern", readonly ["string"], {
|
|
35
|
+
args: readonly [pattern: RegExp];
|
|
36
|
+
out: import("../plugin-kit").Unchanged;
|
|
37
|
+
context: {
|
|
38
|
+
readonly pattern: string;
|
|
39
|
+
};
|
|
40
|
+
}>;
|
|
41
|
+
stringEmail: import("..").PluginDefinition<"stringEmail", "email", readonly ["string"], {
|
|
42
|
+
args: readonly [options?: import("../plugins/string-email/string-email").EmailFormatOptions];
|
|
43
|
+
out: import("../plugin-kit").Unchanged;
|
|
44
|
+
context: {
|
|
45
|
+
readonly reason: string;
|
|
46
|
+
};
|
|
47
|
+
}>;
|
|
48
|
+
}>;
|
|
49
|
+
/** 数値の定番。使用実績: numberMin 43 / numberMax 8。 */
|
|
50
|
+
export declare const numbers: Readonly<{
|
|
51
|
+
numberMin: import("..").PluginDefinition<"numberMin", "min", readonly ["number"], {
|
|
52
|
+
args: readonly [min: number, exclusive?: boolean];
|
|
53
|
+
out: import("../plugin-kit").Unchanged;
|
|
54
|
+
context: import("../plugins/number-min").NumberMinContext;
|
|
55
|
+
}>;
|
|
56
|
+
numberMax: import("..").PluginDefinition<"numberMax", "max", readonly ["number"], {
|
|
57
|
+
args: readonly [max: number, exclusive?: boolean];
|
|
58
|
+
out: import("../plugin-kit").Unchanged;
|
|
59
|
+
context: import("../plugins/number-max").NumberMaxContext;
|
|
60
|
+
}>;
|
|
61
|
+
numberInteger: import("..").PluginDefinition<"numberInteger", "integer", readonly ["number"], {
|
|
62
|
+
args: readonly [];
|
|
63
|
+
out: import("../plugin-kit").Unchanged;
|
|
64
|
+
context: import("..").MessageContextExtra;
|
|
65
|
+
}>;
|
|
66
|
+
}>;
|
|
67
|
+
/** 配列の定番。要素ごとの規則は arrayEach が運ぶ。 */
|
|
68
|
+
export declare const arrays: Readonly<{
|
|
69
|
+
arrayMinLength: import("..").PluginDefinition<"arrayMinLength", "minLength", readonly ["array", "tuple"], {
|
|
70
|
+
args: readonly [min: number];
|
|
71
|
+
out: import("../plugin-kit").Unchanged;
|
|
72
|
+
context: import("../plugins/array-min-length").ArrayMinLengthContext;
|
|
73
|
+
}>;
|
|
74
|
+
arrayMaxLength: import("..").PluginDefinition<"arrayMaxLength", "maxLength", readonly ["array", "tuple"], {
|
|
75
|
+
args: readonly [max: number];
|
|
76
|
+
out: import("../plugin-kit").Unchanged;
|
|
77
|
+
context: import("../plugins/array-max-length").ArrayMaxLengthContext;
|
|
78
|
+
}>;
|
|
79
|
+
arrayEach: import("..").PluginDefinition<"arrayEach", "each", readonly ["array", "tuple"], {
|
|
80
|
+
args: readonly [element: import("../plugin-kit").ElementChain];
|
|
81
|
+
out: import("../plugin-kit").Unchanged;
|
|
82
|
+
context: import("..").MessageContextExtra;
|
|
83
|
+
}>;
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* 上の四つを合わせたもの。13プラグイン、gzip で +1,452 B。
|
|
87
|
+
*
|
|
88
|
+
* 名前が「common」でないのは、それが何も言っていないからである
|
|
89
|
+
* (lint の禁止語彙にも入っている)。入っているのは presence と、文字列・数値・
|
|
90
|
+
* 配列それぞれの定番で、毎日書くのはこの範囲だ、という主張がこの名前である。
|
|
91
|
+
*
|
|
92
|
+
* これでも「全部入り」ではない。全部入りが要るなら `@maroonedog/luq/plugins`
|
|
93
|
+
* のバレルがあり、そちらは 77 個ぶん、gzip で +17,986 B を払う。
|
|
94
|
+
*/
|
|
95
|
+
export declare const everydayRules: Readonly<{
|
|
96
|
+
arrayMinLength: import("..").PluginDefinition<"arrayMinLength", "minLength", readonly ["array", "tuple"], {
|
|
97
|
+
args: readonly [min: number];
|
|
98
|
+
out: import("../plugin-kit").Unchanged;
|
|
99
|
+
context: import("../plugins/array-min-length").ArrayMinLengthContext;
|
|
100
|
+
}>;
|
|
101
|
+
arrayMaxLength: import("..").PluginDefinition<"arrayMaxLength", "maxLength", readonly ["array", "tuple"], {
|
|
102
|
+
args: readonly [max: number];
|
|
103
|
+
out: import("../plugin-kit").Unchanged;
|
|
104
|
+
context: import("../plugins/array-max-length").ArrayMaxLengthContext;
|
|
105
|
+
}>;
|
|
106
|
+
arrayEach: import("..").PluginDefinition<"arrayEach", "each", readonly ["array", "tuple"], {
|
|
107
|
+
args: readonly [element: import("../plugin-kit").ElementChain];
|
|
108
|
+
out: import("../plugin-kit").Unchanged;
|
|
109
|
+
context: import("..").MessageContextExtra;
|
|
110
|
+
}>;
|
|
111
|
+
numberMin: import("..").PluginDefinition<"numberMin", "min", readonly ["number"], {
|
|
112
|
+
args: readonly [min: number, exclusive?: boolean];
|
|
113
|
+
out: import("../plugin-kit").Unchanged;
|
|
114
|
+
context: import("../plugins/number-min").NumberMinContext;
|
|
115
|
+
}>;
|
|
116
|
+
numberMax: import("..").PluginDefinition<"numberMax", "max", readonly ["number"], {
|
|
117
|
+
args: readonly [max: number, exclusive?: boolean];
|
|
118
|
+
out: import("../plugin-kit").Unchanged;
|
|
119
|
+
context: import("../plugins/number-max").NumberMaxContext;
|
|
120
|
+
}>;
|
|
121
|
+
numberInteger: import("..").PluginDefinition<"numberInteger", "integer", readonly ["number"], {
|
|
122
|
+
args: readonly [];
|
|
123
|
+
out: import("../plugin-kit").Unchanged;
|
|
124
|
+
context: import("..").MessageContextExtra;
|
|
125
|
+
}>;
|
|
126
|
+
stringMin: import("..").PluginDefinition<"stringMin", "min", readonly ["string"], {
|
|
127
|
+
args: readonly [min: number];
|
|
128
|
+
out: import("../plugin-kit").Unchanged;
|
|
129
|
+
context: import("../plugins/string-min/string-min").StringMinContext;
|
|
130
|
+
}>;
|
|
131
|
+
stringMax: import("..").PluginDefinition<"stringMax", "max", readonly ["string"], {
|
|
132
|
+
args: readonly [max: number];
|
|
133
|
+
out: import("../plugin-kit").Unchanged;
|
|
134
|
+
context: import("../plugins/string-max/string-max").StringMaxContext;
|
|
135
|
+
}>;
|
|
136
|
+
stringPattern: import("..").PluginDefinition<"stringPattern", "pattern", readonly ["string"], {
|
|
137
|
+
args: readonly [pattern: RegExp];
|
|
138
|
+
out: import("../plugin-kit").Unchanged;
|
|
139
|
+
context: {
|
|
140
|
+
readonly pattern: string;
|
|
141
|
+
};
|
|
142
|
+
}>;
|
|
143
|
+
stringEmail: import("..").PluginDefinition<"stringEmail", "email", readonly ["string"], {
|
|
144
|
+
args: readonly [options?: import("../plugins/string-email/string-email").EmailFormatOptions];
|
|
145
|
+
out: import("../plugin-kit").Unchanged;
|
|
146
|
+
context: {
|
|
147
|
+
readonly reason: string;
|
|
148
|
+
};
|
|
149
|
+
}>;
|
|
150
|
+
required: import("..").PluginDefinition<"required", "required", readonly import("..").TypeName[], {
|
|
151
|
+
args: readonly [];
|
|
152
|
+
out: import("../plugin-kit").PresenceShift<"excludeMissing">;
|
|
153
|
+
context: import("..").MessageContextExtra;
|
|
154
|
+
}>;
|
|
155
|
+
optional: import("..").PluginDefinition<"optional", "optional", readonly import("..").TypeName[], {
|
|
156
|
+
args: readonly [];
|
|
157
|
+
out: import("../plugin-kit").PresenceShift<"excludeNull">;
|
|
158
|
+
context: import("..").MessageContextExtra;
|
|
159
|
+
}>;
|
|
160
|
+
nullable: import("..").PluginDefinition<"nullable", "nullable", readonly import("..").TypeName[], {
|
|
161
|
+
args: readonly [];
|
|
162
|
+
out: import("../plugin-kit").PresenceShift<"allowNull">;
|
|
163
|
+
context: import("..").MessageContextExtra;
|
|
164
|
+
}>;
|
|
165
|
+
}>;
|
|
@@ -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
|
-
|
|
59
|
+
const nest = compileNester(template);
|
|
60
|
+
return (siblings, value) => mergeOntoSiblings(siblings, nest(value));
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
56
|
+
const nest = compileNester(template);
|
|
57
|
+
return (siblings, value) => mergeOntoSiblings(siblings, nest(value));
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
}
|
|
@@ -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
|
+
}
|