cli-kiss 0.2.7 → 0.2.9
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 +8 -3
- package/dist/index.d.ts +200 -190
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.mts +1 -1
- package/docs/.vitepress/theme/Layout.vue +16 -0
- package/docs/.vitepress/theme/index.ts +5 -1
- package/docs/.vitepress/theme/style.css +5 -1
- package/docs/guide/01_getting_started.md +2 -2
- package/docs/guide/02_commands.md +3 -3
- package/docs/guide/03_options.md +11 -11
- package/docs/guide/04_positionals.md +9 -9
- package/docs/guide/05_input_types.md +17 -16
- package/docs/guide/06_run_as_cli.md +1 -1
- package/docs/index.md +2 -2
- package/docs/public/favicon.ico +0 -0
- package/docs/public/logo.png +0 -0
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/lib/Command.ts +51 -40
- package/src/lib/Operation.ts +41 -25
- package/src/lib/Option.ts +198 -127
- package/src/lib/Positional.ts +51 -25
- package/src/lib/Reader.ts +188 -226
- package/src/lib/Run.ts +20 -9
- package/src/lib/Suggest.ts +78 -0
- package/src/lib/Type.ts +178 -154
- package/src/lib/Typo.ts +58 -55
- package/src/lib/Usage.ts +12 -12
- package/tests/unit.Reader.commons.ts +86 -123
- package/tests/unit.Reader.parsings.ts +14 -26
- package/tests/unit.Reader.shortBig.ts +75 -101
- package/tests/unit.command.aliases.ts +88 -0
- package/tests/unit.command.execute.ts +6 -6
- package/tests/unit.command.usage.ts +19 -13
- package/tests/unit.fuzzed.alternatives.ts +35 -26
- package/tests/unit.runner.colors.ts +8 -33
- package/tests/unit.runner.cycle.ts +141 -156
- package/tests/unit.runner.errors.ts +25 -22
- package/docs/public/hero.png +0 -0
- package/src/lib/Similarity.ts +0 -41
- package/tests/unit.Reader.aliases.ts +0 -62
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { TypoSegment, TypoString, TypoText } from "./Typo";
|
|
2
|
+
|
|
3
|
+
export function suggestTextPushMessage(
|
|
4
|
+
text: TypoText,
|
|
5
|
+
query: string,
|
|
6
|
+
candidates: Array<{ reference: string; hint: TypoSegment }>,
|
|
7
|
+
) {
|
|
8
|
+
const reasonableHints = suggestReasonablePayloads(
|
|
9
|
+
query,
|
|
10
|
+
candidates.map(({ reference, hint }) => ({ reference, payload: hint })),
|
|
11
|
+
);
|
|
12
|
+
if (reasonableHints.length === 0) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
text.push(new TypoString(" Did you mean: "));
|
|
16
|
+
text.pushJoined(reasonableHints, new TypoString(", "), 3);
|
|
17
|
+
text.push(new TypoString(` ?`));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function suggestReasonablePayloads<Payload>(
|
|
21
|
+
query: string,
|
|
22
|
+
candidates: Array<{ reference: string; payload: Payload }>,
|
|
23
|
+
): Array<Payload> {
|
|
24
|
+
if (candidates.length === 0) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const sortedAlternatives = computeAndSortByDivergences(query, candidates);
|
|
28
|
+
const divergenceThreshold = sortedAlternatives[0]!.divergence + 0.25;
|
|
29
|
+
const acceptablePayloads = new Array<Payload>();
|
|
30
|
+
for (const { divergence, payload } of sortedAlternatives) {
|
|
31
|
+
if (divergence > divergenceThreshold) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
acceptablePayloads.push(payload);
|
|
35
|
+
}
|
|
36
|
+
return acceptablePayloads;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function computeAndSortByDivergences<Payload>(
|
|
40
|
+
query: string,
|
|
41
|
+
candidates: Array<{ reference: string; payload: Payload }>,
|
|
42
|
+
): Array<{ divergence: number; payload: Payload }> {
|
|
43
|
+
const queryNormalized = query.toLowerCase().slice(0, 100);
|
|
44
|
+
const scored = candidates.map(({ reference, payload }) => {
|
|
45
|
+
const referenceNormalized = reference.toLowerCase().slice(0, 100);
|
|
46
|
+
const divergence =
|
|
47
|
+
distanceDamerauLevenshtein(queryNormalized, referenceNormalized) /
|
|
48
|
+
Math.max(queryNormalized.length, referenceNormalized.length);
|
|
49
|
+
return { divergence, reference, payload };
|
|
50
|
+
});
|
|
51
|
+
return scored.sort((a, b) => a.divergence - b.divergence);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function distanceDamerauLevenshtein(a: string, b: string): number {
|
|
55
|
+
const m = a.length;
|
|
56
|
+
const n = b.length;
|
|
57
|
+
const dp = Array.from({ length: m + 1 }, () => Array<number>(n + 1).fill(0));
|
|
58
|
+
for (let i = 0; i <= m; i++) {
|
|
59
|
+
dp[i]![0] = i;
|
|
60
|
+
}
|
|
61
|
+
for (let j = 0; j <= n; j++) {
|
|
62
|
+
dp[0]![j] = j;
|
|
63
|
+
}
|
|
64
|
+
for (let i = 1; i <= m; i++) {
|
|
65
|
+
for (let j = 1; j <= n; j++) {
|
|
66
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
67
|
+
dp[i]![j] = Math.min(
|
|
68
|
+
dp[i - 1]![j]! + 1,
|
|
69
|
+
dp[i]![j - 1]! + 1,
|
|
70
|
+
dp[i - 1]![j - 1]! + cost,
|
|
71
|
+
);
|
|
72
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
73
|
+
dp[i]![j] = Math.min(dp[i]![j]!, dp[i - 2]![j - 2]! + cost);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return dp[m]![n]!;
|
|
78
|
+
}
|
package/src/lib/Type.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { statSync } from "fs";
|
|
2
|
-
import {
|
|
2
|
+
import { suggestTextPushMessage } from "./Suggest";
|
|
3
3
|
import {
|
|
4
4
|
TypoError,
|
|
5
5
|
TypoString,
|
|
@@ -12,27 +12,54 @@ import {
|
|
|
12
12
|
* Decodes a raw CLI string into a typed value.
|
|
13
13
|
* A pair of a human-readable `content` name and a `decoder` function.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Primitive Types:
|
|
16
|
+
* - {@link typeString}
|
|
17
|
+
* - {@link typeBoolean}
|
|
18
|
+
* - {@link typeNumber},
|
|
19
|
+
* - {@link typeInteger}
|
|
20
|
+
* - {@link typeDatetime}
|
|
21
|
+
* - {@link typeUrl}
|
|
22
|
+
* - {@link typePath}
|
|
23
|
+
* - {@link typeChoice}
|
|
24
|
+
*
|
|
25
|
+
* Composed Types:
|
|
26
|
+
* - {@link typeMapped}
|
|
27
|
+
* - {@link typeTuple}
|
|
28
|
+
* - {@link typeList}
|
|
18
29
|
*
|
|
19
30
|
* @typeParam Value - Type produced by the decoder.
|
|
20
31
|
*/
|
|
21
32
|
export type Type<Value> = {
|
|
22
33
|
/**
|
|
23
|
-
* Human-readable name shown in help and errors (e.g. `"name"`, `"
|
|
34
|
+
* Human-readable name shown in help and errors (e.g. `"name"`, `"context"`).
|
|
24
35
|
*/
|
|
25
|
-
content: string;
|
|
36
|
+
content: string; // TODO - add an enforcement mechanism for casing ?
|
|
26
37
|
/**
|
|
27
38
|
* Decodes a raw CLI string into `Value`.
|
|
28
39
|
*
|
|
29
40
|
* @param input - Raw string from the command line.
|
|
30
41
|
* @returns The decoded value.
|
|
31
|
-
* @throws
|
|
42
|
+
* @throws on invalid input.
|
|
32
43
|
*/
|
|
33
44
|
decoder(input: string): Value;
|
|
34
45
|
};
|
|
35
46
|
|
|
47
|
+
/**
|
|
48
|
+
* A named type that accepts any string as input.
|
|
49
|
+
* @param name - Name shown in help and errors (e.g. `"my-value"`).
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* typeString("greeting").decoder("hello") // → "hello"
|
|
53
|
+
* typeString("greeting").decoder("") // → ""
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function typeString(name?: string): Type<string> {
|
|
57
|
+
return {
|
|
58
|
+
content: name ?? "string",
|
|
59
|
+
decoder: (input: string) => input,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
36
63
|
/**
|
|
37
64
|
* Decodes a string to `boolean` (case-insensitive).
|
|
38
65
|
* Used by {@link optionFlag} for `--flag=<value>`.
|
|
@@ -41,10 +68,11 @@ export type Type<Value> = {
|
|
|
41
68
|
* ```ts
|
|
42
69
|
* typeBoolean("flag").decoder("true") // → true
|
|
43
70
|
* typeBoolean("flag").decoder("yes") // → true
|
|
44
|
-
* typeBoolean("flag").decoder("
|
|
45
|
-
* typeBoolean("flag").decoder("
|
|
46
|
-
* typeBoolean("flag").decoder("
|
|
71
|
+
* typeBoolean("flag").decoder("Y") // → true
|
|
72
|
+
* typeBoolean("flag").decoder("FALSE") // → false
|
|
73
|
+
* typeBoolean("flag").decoder("NO") // → false
|
|
47
74
|
* typeBoolean("flag").decoder("n") // → false
|
|
75
|
+
* typeBoolean("flag").decoder("maybe") // throws
|
|
48
76
|
* ```
|
|
49
77
|
*/
|
|
50
78
|
export function typeBoolean(name?: string): Type<boolean> {
|
|
@@ -62,39 +90,9 @@ export function typeBoolean(name?: string): Type<boolean> {
|
|
|
62
90
|
},
|
|
63
91
|
};
|
|
64
92
|
}
|
|
65
|
-
export const typeBooleanValuesTrue = new Set(["true", "yes", "on", "y"]);
|
|
66
|
-
export const typeBooleanValuesFalse = new Set(["false", "no", "off", "n"]);
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Parses a date/time string via `Date.parse`.
|
|
70
|
-
* Accepts any format supported by `Date.parse`, including ISO 8601.
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```ts
|
|
74
|
-
* typeDatetime("my-datetime").decoder("2024-01-15") // → Date object for 2024-01-15
|
|
75
|
-
* typeDatetime("my-datetime").decoder("2024-01-15T13:45:30Z") // → Date object for 2024-01-15 13:45:30 UTC
|
|
76
|
-
* typeDatetime("my-datetime").decoder("not a date") // throws TypoError
|
|
77
|
-
* ```
|
|
78
|
-
*/
|
|
79
|
-
export function typeDatetime(name?: string): Type<Date> {
|
|
80
|
-
return {
|
|
81
|
-
content: name ?? "datetime",
|
|
82
|
-
decoder(input: string) {
|
|
83
|
-
try {
|
|
84
|
-
const timestampMs = Date.parse(input);
|
|
85
|
-
if (isNaN(timestampMs)) {
|
|
86
|
-
throw new Error();
|
|
87
|
-
}
|
|
88
|
-
return new Date(timestampMs);
|
|
89
|
-
} catch {
|
|
90
|
-
throwInvalidValue("a valid ISO_8601 datetime", input);
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
93
|
|
|
96
94
|
/**
|
|
97
|
-
* Parses a string to `number` via `Number()`; `NaN` throws
|
|
95
|
+
* Parses a string to `number` via `Number()`; `NaN` throws.
|
|
98
96
|
*
|
|
99
97
|
* @example
|
|
100
98
|
* ```ts
|
|
@@ -122,7 +120,7 @@ export function typeNumber(name?: string): Type<number> {
|
|
|
122
120
|
|
|
123
121
|
/**
|
|
124
122
|
* Parses an integer string to `bigint` via `BigInt()`.
|
|
125
|
-
* Floats and non-numeric strings
|
|
123
|
+
* Floats and non-numeric strings throws.
|
|
126
124
|
*
|
|
127
125
|
* @example
|
|
128
126
|
* ```ts
|
|
@@ -145,118 +143,68 @@ export function typeInteger(name?: string): Type<bigint> {
|
|
|
145
143
|
}
|
|
146
144
|
|
|
147
145
|
/**
|
|
148
|
-
* Parses
|
|
149
|
-
*
|
|
146
|
+
* Parses a date/time string via `Date.parse`.
|
|
147
|
+
* Accepts any format supported by `Date.parse`, including ISO 8601.
|
|
150
148
|
*
|
|
151
149
|
* @example
|
|
152
150
|
* ```ts
|
|
153
|
-
*
|
|
154
|
-
*
|
|
151
|
+
* typeDatetime("start").decoder("2024-01-15") // → Date object for 2024-01-15
|
|
152
|
+
* typeDatetime("start").decoder("2024-01-15T13:45:30Z") // → Date object for 2024-01-15 13:45:30 UTC
|
|
153
|
+
* typeDatetime("start").decoder("not a date") // throws
|
|
155
154
|
* ```
|
|
156
155
|
*/
|
|
157
|
-
export function
|
|
156
|
+
export function typeDatetime(name?: string): Type<Date> {
|
|
158
157
|
return {
|
|
159
|
-
content: name ?? "
|
|
158
|
+
content: name ?? "datetime",
|
|
160
159
|
decoder(input: string) {
|
|
161
160
|
try {
|
|
162
|
-
|
|
161
|
+
const timestampMs = Date.parse(input);
|
|
162
|
+
if (isNaN(timestampMs)) {
|
|
163
|
+
throw new Error();
|
|
164
|
+
}
|
|
165
|
+
return new Date(timestampMs);
|
|
163
166
|
} catch {
|
|
164
|
-
throwInvalidValue("
|
|
167
|
+
throwInvalidValue("a valid ISO_8601 datetime", input);
|
|
165
168
|
}
|
|
166
169
|
},
|
|
167
170
|
};
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* @example
|
|
174
|
-
* ```ts
|
|
175
|
-
* type("greeting").decoder("hello") // → "hello"
|
|
176
|
-
* type("greeting").decoder("") // → ""
|
|
177
|
-
* ```
|
|
178
|
-
*/
|
|
179
|
-
export function type(name?: string): Type<string> {
|
|
180
|
-
return {
|
|
181
|
-
content: name ?? "string",
|
|
182
|
-
decoder: (input: string) => input,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Chains `before`'s decoder with an `after` transformation.
|
|
188
|
-
* `before` errors are prefixed with `"from: <content>"` for traceability.
|
|
189
|
-
*
|
|
190
|
-
* @typeParam Before - Intermediate type from `before.decoder`.
|
|
191
|
-
* @typeParam After - Final type from `after.decoder`.
|
|
192
|
-
*
|
|
193
|
-
* @param name - Name shown in help and errors (e.g. `"my-value"`).
|
|
194
|
-
* @param before - Base type to decode the raw string.
|
|
195
|
-
* @param mapper - Transforms `before`'s output to the final value; errors are wrapped with context.
|
|
196
|
-
* @returns A {@link Type}`<After>`.
|
|
174
|
+
* Parses an absolute URL string to a `URL` object.
|
|
175
|
+
* Relative or malformed URLs throws.
|
|
197
176
|
*
|
|
198
177
|
* @example
|
|
199
178
|
* ```ts
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* return n;
|
|
203
|
-
* });
|
|
204
|
-
* // "--port 8080" → 8080
|
|
205
|
-
* // "--port 99999" → TypoError: --port: <PORT>: Port: Out of range
|
|
179
|
+
* typeUrl("my-url").decoder("https://example.com") // → URL { href: "https://example.com/", ... }
|
|
180
|
+
* typeUrl("my-url").decoder("not-a-url") // throws
|
|
206
181
|
* ```
|
|
207
182
|
*/
|
|
208
|
-
export function
|
|
209
|
-
name: string,
|
|
210
|
-
before: Type<Before>,
|
|
211
|
-
mapper: (value: Before) => After,
|
|
212
|
-
): Type<After> {
|
|
213
|
-
return {
|
|
214
|
-
content: name,
|
|
215
|
-
decoder: (input: string) => {
|
|
216
|
-
return mapper(
|
|
217
|
-
TypoError.tryWithContext(
|
|
218
|
-
() => before.decoder(input),
|
|
219
|
-
() =>
|
|
220
|
-
new TypoText(
|
|
221
|
-
new TypoString("from: "),
|
|
222
|
-
new TypoString(before.content, typoStyleLogic),
|
|
223
|
-
),
|
|
224
|
-
),
|
|
225
|
-
);
|
|
226
|
-
},
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Adds a name to a {@link Type} for clearer error messages and help text.
|
|
232
|
-
*
|
|
233
|
-
* @param name - Name to use for the type.
|
|
234
|
-
* @param type - Base type to name.
|
|
235
|
-
* @returns A {@link Type} with the given name.
|
|
236
|
-
*/
|
|
237
|
-
export function typeRenamed<Value>(
|
|
238
|
-
type: Type<Value>,
|
|
239
|
-
name: string,
|
|
240
|
-
): Type<Value> {
|
|
183
|
+
export function typeUrl(name?: string): Type<URL> {
|
|
241
184
|
return {
|
|
242
|
-
content: name,
|
|
243
|
-
decoder
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
new TypoString(type.content, typoStyleLogic),
|
|
250
|
-
),
|
|
251
|
-
);
|
|
185
|
+
content: name ?? "url",
|
|
186
|
+
decoder(input: string) {
|
|
187
|
+
try {
|
|
188
|
+
return new URL(input);
|
|
189
|
+
} catch {
|
|
190
|
+
throwInvalidValue("an URL", input);
|
|
191
|
+
}
|
|
252
192
|
},
|
|
253
193
|
};
|
|
254
194
|
}
|
|
255
195
|
|
|
256
196
|
/**
|
|
257
197
|
* Creates a {@link Type} for filesystem paths with optional existence checks.
|
|
198
|
+
*
|
|
258
199
|
* @param checks - Optional checks for path existence and type (file/directory).
|
|
259
200
|
* @returns A {@link Type}`<string>` representing the path.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* const typeInputFile = typePath("input-file", { checkSyncExistAs: "file" });
|
|
205
|
+
* typeInputFile.decoder("data.txt"); // → "data.txt" if it exists and is a file, otherwise throws
|
|
206
|
+
* typeInputFile.decoder("somedir"); // throws if "somedir" exists and is a directory
|
|
207
|
+
* ```
|
|
260
208
|
*/
|
|
261
209
|
export function typePath(
|
|
262
210
|
name?: string,
|
|
@@ -315,7 +263,6 @@ export function typePath(
|
|
|
315
263
|
|
|
316
264
|
/**
|
|
317
265
|
* Creates a {@link Type}`<string>` that only accepts a fixed set of values.
|
|
318
|
-
* Out-of-set inputs throw {@link TypoError} listing up to 5 valid options.
|
|
319
266
|
*
|
|
320
267
|
* @param name - Name shown in help and errors.
|
|
321
268
|
* @param values - Ordered list of accepted values.
|
|
@@ -325,52 +272,122 @@ export function typePath(
|
|
|
325
272
|
* ```ts
|
|
326
273
|
* const typeEnv = typeChoice("environment", ["dev", "staging", "prod"]);
|
|
327
274
|
* typeEnv.decoder("prod") // → "prod"
|
|
328
|
-
* typeEnv.decoder("unknown") // throws
|
|
275
|
+
* typeEnv.decoder("unknown") // throws
|
|
329
276
|
* ```
|
|
330
277
|
*/
|
|
331
278
|
export function typeChoice<const Value extends string>(
|
|
332
279
|
name: string,
|
|
333
280
|
values: Array<Value>,
|
|
334
|
-
caseSensitive: boolean =
|
|
281
|
+
caseSensitive: boolean = true,
|
|
335
282
|
): Type<Value> {
|
|
336
283
|
if (values.length === 0) {
|
|
337
284
|
throw new Error("At least one value is required");
|
|
338
285
|
}
|
|
339
286
|
const normalize = caseSensitive
|
|
340
|
-
? (
|
|
341
|
-
: (
|
|
342
|
-
const
|
|
287
|
+
? (input: string) => input
|
|
288
|
+
: (input: string) => input.toLowerCase();
|
|
289
|
+
const valueByNormalized = new Map(
|
|
343
290
|
values.map((value) => [normalize(value), value]),
|
|
344
291
|
);
|
|
345
292
|
return {
|
|
346
293
|
content: name,
|
|
347
294
|
decoder(input: string) {
|
|
348
|
-
const
|
|
349
|
-
const value = valueByNormalizedKey.get(normalizedKey);
|
|
295
|
+
const value = valueByNormalized.get(normalize(input));
|
|
350
296
|
if (value !== undefined) {
|
|
351
297
|
return value;
|
|
352
298
|
}
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
299
|
+
const errorText = new TypoText();
|
|
300
|
+
errorText.push(new TypoString(`Unknown value: `));
|
|
301
|
+
errorText.push(new TypoString(`"${input}"`, typoStyleQuote));
|
|
302
|
+
errorText.push(new TypoString(`.`));
|
|
303
|
+
suggestTextPushMessage(
|
|
304
|
+
errorText,
|
|
305
|
+
input,
|
|
306
|
+
values.map((value) => ({
|
|
307
|
+
reference: value,
|
|
308
|
+
hint: new TypoString(`"${value}"`, typoStyleQuote),
|
|
361
309
|
})),
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
310
|
+
);
|
|
311
|
+
throw new TypoError(errorText);
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Chains `before`'s decoder with an `after` transformation.
|
|
318
|
+
* `before` errors are prefixed with `"from: <content>"` for traceability.
|
|
319
|
+
*
|
|
320
|
+
* @typeParam Before - Intermediate type from `before.decoder`.
|
|
321
|
+
* @typeParam After - Final type from `after.decoder`.
|
|
322
|
+
*
|
|
323
|
+
* @param name - Name shown in help and errors (e.g. `"my-value"`).
|
|
324
|
+
* @param before - Base type to decode the raw string.
|
|
325
|
+
* @param mapper - Transforms `before`'s output to the final value; errors are wrapped with context.
|
|
326
|
+
* @returns A {@link Type}`<After>`.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```ts
|
|
330
|
+
* const typePort = typeMapped("port", typeNumber(), (n) => {
|
|
331
|
+
* if (n < 1 || n > 65535) {
|
|
332
|
+
* throw new Error("Out of range");
|
|
333
|
+
* }
|
|
334
|
+
* return n;
|
|
335
|
+
* });
|
|
336
|
+
* typePort.decoder("8080"); // → 8080
|
|
337
|
+
* typePort.decoder("70000"); // throws
|
|
338
|
+
* ```
|
|
339
|
+
*/
|
|
340
|
+
export function typeMapped<Before, After>(
|
|
341
|
+
name: string,
|
|
342
|
+
before: Type<Before>,
|
|
343
|
+
mapper: (value: Before) => After,
|
|
344
|
+
): Type<After> {
|
|
345
|
+
return {
|
|
346
|
+
content: name,
|
|
347
|
+
decoder: (input: string) => {
|
|
348
|
+
return mapper(
|
|
349
|
+
TypoError.tryWithContext(
|
|
350
|
+
() => before.decoder(input),
|
|
351
|
+
() =>
|
|
352
|
+
new TypoText(
|
|
353
|
+
new TypoString("from: "),
|
|
354
|
+
new TypoString(before.content, typoStyleLogic),
|
|
355
|
+
),
|
|
356
|
+
),
|
|
357
|
+
);
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Adds a name to a {@link Type} for clearer error messages and help text.
|
|
364
|
+
*
|
|
365
|
+
* @param name - Name to use for the type.
|
|
366
|
+
* @param type - Base type to name.
|
|
367
|
+
* @returns A {@link Type} with the given name.
|
|
368
|
+
*/
|
|
369
|
+
export function typeRenamed<Value>(
|
|
370
|
+
type: Type<Value>,
|
|
371
|
+
name: string,
|
|
372
|
+
): Type<Value> {
|
|
373
|
+
return {
|
|
374
|
+
content: name,
|
|
375
|
+
decoder: (input: string) => {
|
|
376
|
+
return TypoError.tryWithContext(
|
|
377
|
+
() => type.decoder(input),
|
|
378
|
+
() =>
|
|
379
|
+
new TypoText(
|
|
380
|
+
new TypoString("from: "),
|
|
381
|
+
new TypoString(type.content, typoStyleLogic),
|
|
382
|
+
),
|
|
383
|
+
);
|
|
367
384
|
},
|
|
368
385
|
};
|
|
369
386
|
}
|
|
370
387
|
|
|
371
388
|
/**
|
|
372
389
|
* Splits a delimited string into a typed tuple.
|
|
373
|
-
* Each part is decoded by the corresponding element type; wrong count or decode failure throws
|
|
390
|
+
* Each part is decoded by the corresponding element type; wrong count or decode failure throws.
|
|
374
391
|
*
|
|
375
392
|
* @typeParam Elements - Tuple of decoded value types (inferred from `elementTypes`).
|
|
376
393
|
*
|
|
@@ -382,8 +399,9 @@ export function typeChoice<const Value extends string>(
|
|
|
382
399
|
* ```ts
|
|
383
400
|
* const typePoint = typeTuple([typeNumber("x"), typeNumber("y")]);
|
|
384
401
|
* typePoint.decoder("3.14,2.71") // → [3.14, 2.71]
|
|
385
|
-
* typePoint.decoder("1
|
|
386
|
-
* typePoint.decoder("
|
|
402
|
+
* typePoint.decoder("1") // throws
|
|
403
|
+
* typePoint.decoder("1,2,3") // throws
|
|
404
|
+
* typePoint.decoder("x,2") // throws
|
|
387
405
|
* ```
|
|
388
406
|
*/
|
|
389
407
|
export function typeTuple<const Elements extends Array<any>>(
|
|
@@ -402,6 +420,7 @@ export function typeTuple<const Elements extends Array<any>>(
|
|
|
402
420
|
new TypoString(`Found ${splits.length} splits: `),
|
|
403
421
|
new TypoString(`Expected ${elementTypes.length} splits from: `),
|
|
404
422
|
new TypoString(`"${input}"`, typoStyleQuote),
|
|
423
|
+
new TypoString(`.`),
|
|
405
424
|
),
|
|
406
425
|
);
|
|
407
426
|
}
|
|
@@ -422,7 +441,7 @@ export function typeTuple<const Elements extends Array<any>>(
|
|
|
422
441
|
|
|
423
442
|
/**
|
|
424
443
|
* Splits a delimited string into a typed array.
|
|
425
|
-
* Each part is decoded by `elementType`; failed decodes
|
|
444
|
+
* Each part is decoded by `elementType`; failed decodes throws.
|
|
426
445
|
* Note: splitting an empty string yields one empty element — prefer {@link optionRepeatable} for a zero-default.
|
|
427
446
|
*
|
|
428
447
|
* @typeParam Value - Element type produced by `elementType.decoder`.
|
|
@@ -433,12 +452,13 @@ export function typeTuple<const Elements extends Array<any>>(
|
|
|
433
452
|
*
|
|
434
453
|
* @example
|
|
435
454
|
* ```ts
|
|
436
|
-
* const typeNumbers = typeList(typeNumber);
|
|
455
|
+
* const typeNumbers = typeList(typeNumber("v"));
|
|
437
456
|
* typeNumbers.decoder("1,2,3") // → [1, 2, 3]
|
|
438
|
-
* typeNumbers.decoder("1,x,3") // throws
|
|
439
|
-
*
|
|
457
|
+
* typeNumbers.decoder("1,x,3") // throws
|
|
440
458
|
* const typePaths = typeList(typePath(), ":");
|
|
441
|
-
* typePaths.decoder("/
|
|
459
|
+
* typePaths.decoder("/bin:/usr") // → ["/bin", "/usr"]
|
|
460
|
+
* typePaths.decoder("/usr/bin") // → ["/usr/bin"]
|
|
461
|
+
* typePaths.decoder("") // → throws
|
|
442
462
|
* ```
|
|
443
463
|
*/
|
|
444
464
|
export function typeList<Value>(
|
|
@@ -468,6 +488,10 @@ function throwInvalidValue(kind: string, input: string): never {
|
|
|
468
488
|
new TypoText(
|
|
469
489
|
new TypoString(`Not ${kind}: `),
|
|
470
490
|
new TypoString(`"${input}"`, typoStyleQuote),
|
|
491
|
+
new TypoString(`.`),
|
|
471
492
|
),
|
|
472
493
|
);
|
|
473
494
|
}
|
|
495
|
+
|
|
496
|
+
const typeBooleanValuesTrue = new Set(["true", "yes", "on", "y"]);
|
|
497
|
+
const typeBooleanValuesFalse = new Set(["false", "no", "off", "n"]);
|