cli-kiss 0.2.2 → 0.2.3

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/src/lib/Type.ts CHANGED
@@ -7,44 +7,33 @@ import {
7
7
  } from "./Typo";
8
8
 
9
9
  /**
10
- * Describes how to decode a raw CLI string token into a typed TypeScript value.
10
+ * Decodes a raw CLI string into a typed value.
11
+ * A pair of a human-readable `content` name (e.g. `"Number"`) and a `decoder` function.
11
12
  *
12
- * A `Type` is a pair of:
13
- * - a `content` string — a human-readable name shown in help/error messages (e.g.
14
- * `"String"`, `"Number"`, `"Url"`).
15
- * - a `decoder` function — converts the raw string or throws a {@link TypoError} on
16
- * invalid input.
17
- *
18
- * Built-in types: {@link typeString}, {@link typeBoolean}, {@link typeNumber},
13
+ * Built-in: {@link typeString}, {@link typeBoolean}, {@link typeNumber},
19
14
  * {@link typeInteger}, {@link typeDate}, {@link typeUrl}.
15
+ * Composite: {@link typeOneOf}, {@link typeMapped}, {@link typeTuple}, {@link typeList}.
20
16
  *
21
- * Composite types: {@link typeOneOf}, {@link typeConverted}, {@link typeTuple},
22
- * {@link typeList}.
23
- *
24
- * @typeParam Value - The TypeScript type that the decoder produces.
17
+ * @typeParam Value - Type produced by the decoder.
25
18
  */
26
19
  export type Type<Value> = {
27
20
  /**
28
- * Human-readable name for this type, used in help text and error messages.
29
- * Examples: `"String"`, `"Number"`, `"Url"`.
21
+ * Human-readable name shown in help and error messages (e.g. `"String"`, `"Number"`).
30
22
  */
31
23
  content: string;
32
24
  /**
33
- * Decodes a raw string token into a `Value`.
25
+ * Converts a raw CLI string into `Value`.
34
26
  *
35
- * @param value - The raw string from the command line.
27
+ * @param input - Raw string from the command line.
36
28
  * @returns The decoded value.
37
- * @throws {@link TypoError} if the value cannot be decoded.
29
+ * @throws {@link TypoError} on invalid input.
38
30
  */
39
- decoder(value: string): Value;
31
+ decoder(input: string): Value;
40
32
  };
41
33
 
42
34
  /**
43
- * A {@link Type} that decodes `"true"` / `"yes"` to `true` and `"false"` / `"no"` to
44
- * `false` (case-insensitive). Any other value throws a {@link TypoError}.
45
- *
46
- * Primarily used internally by {@link optionFlag} for the `--flag=<value>` syntax, but
47
- * can also be used in positionals or valued options.
35
+ * Decodes `"true"` / `"yes"` `true` and `"false"` / `"no"` → `false` (case-insensitive).
36
+ * Used internally by {@link optionFlag} for the `--flag=<value>` syntax.
48
37
  *
49
38
  * @example
50
39
  * ```ts
@@ -55,31 +44,26 @@ export type Type<Value> = {
55
44
  */
56
45
  export const typeBoolean: Type<boolean> = {
57
46
  content: "Boolean",
58
- decoder(value: string) {
59
- const lowerValue = value.toLowerCase();
60
- if (lowerValue === "true" || lowerValue === "yes") {
47
+ decoder(input: string) {
48
+ const lower = input.toLowerCase();
49
+ if (lower === "true" || lower === "yes") {
61
50
  return true;
62
51
  }
63
- if (lowerValue === "false" || lowerValue === "no") {
52
+ if (lower === "false" || lower === "no") {
64
53
  return false;
65
54
  }
66
55
  throw new TypoError(
67
56
  new TypoText(
68
57
  new TypoString(`Invalid value: `),
69
- new TypoString(`"${value}"`, typoStyleQuote),
58
+ new TypoString(`"${input}"`, typoStyleQuote),
70
59
  ),
71
60
  );
72
61
  },
73
62
  };
74
63
 
75
64
  /**
76
- * A {@link Type} that parses a date/time string using `Date.parse`.
77
- *
78
- * Accepts any format supported by the JavaScript `Date.parse` API, including ISO 8601
79
- * strings (e.g. `"2024-01-15"`, `"2024-01-15T10:30:00Z"`). Non-parseable values throw
80
- * a {@link TypoError}.
81
- *
82
- * Produces a `Date` object. The decoded value is the result of `new Date(Date.parse(value))`.
65
+ * Parses a date/time string via `Date.parse` into a `Date` object.
66
+ * Accepts any format supported by `Date.parse`, including ISO 8601.
83
67
  *
84
68
  * @example
85
69
  * ```ts
@@ -90,9 +74,9 @@ export const typeBoolean: Type<boolean> = {
90
74
  */
91
75
  export const typeDate: Type<Date> = {
92
76
  content: "Date",
93
- decoder(value: string) {
77
+ decoder(input: string) {
94
78
  try {
95
- const timestampMs = Date.parse(value);
79
+ const timestampMs = Date.parse(input);
96
80
  if (isNaN(timestampMs)) {
97
81
  throw new Error();
98
82
  }
@@ -101,7 +85,7 @@ export const typeDate: Type<Date> = {
101
85
  throw new TypoError(
102
86
  new TypoText(
103
87
  new TypoString(`Not a valid ISO_8601: `),
104
- new TypoString(`"${value}"`, typoStyleQuote),
88
+ new TypoString(`"${input}"`, typoStyleQuote),
105
89
  ),
106
90
  );
107
91
  }
@@ -109,11 +93,8 @@ export const typeDate: Type<Date> = {
109
93
  };
110
94
 
111
95
  /**
112
- * A {@link Type} that parses a string into a JavaScript `number` using the `Number()`
113
- * constructor.
114
- *
115
- * Accepts integers, floating-point values, and scientific notation (e.g. `"3.14"`,
116
- * `"-1"`, `"1e10"`). Values that produce `NaN` throw a {@link TypoError}.
96
+ * Parses a string into a `number` via `Number()`.
97
+ * Accepts integers, floats, and scientific notation; `NaN` throws a {@link TypoError}.
117
98
  *
118
99
  * @example
119
100
  * ```ts
@@ -124,9 +105,9 @@ export const typeDate: Type<Date> = {
124
105
  */
125
106
  export const typeNumber: Type<number> = {
126
107
  content: "Number",
127
- decoder(value: string) {
108
+ decoder(input: string) {
128
109
  try {
129
- const parsed = Number(value);
110
+ const parsed = Number(input);
130
111
  if (isNaN(parsed)) {
131
112
  throw new Error();
132
113
  }
@@ -135,7 +116,7 @@ export const typeNumber: Type<number> = {
135
116
  throw new TypoError(
136
117
  new TypoText(
137
118
  new TypoString(`Unable to parse: `),
138
- new TypoString(`"${value}"`, typoStyleQuote),
119
+ new TypoString(`"${input}"`, typoStyleQuote),
139
120
  ),
140
121
  );
141
122
  }
@@ -143,11 +124,8 @@ export const typeNumber: Type<number> = {
143
124
  };
144
125
 
145
126
  /**
146
- * A {@link Type} that parses a string into a JavaScript `bigint` using the `BigInt()`
147
- * constructor.
148
- *
149
- * Only accepts valid integer strings (e.g. `"42"`, `"-100"`, `"9007199254740993"`).
150
- * Floating-point strings or non-numeric values throw a {@link TypoError}.
127
+ * Parses an integer string into a `bigint` via `BigInt()`.
128
+ * Floats and non-numeric strings throw a {@link TypoError}.
151
129
  *
152
130
  * @example
153
131
  * ```ts
@@ -158,14 +136,14 @@ export const typeNumber: Type<number> = {
158
136
  */
159
137
  export const typeInteger: Type<bigint> = {
160
138
  content: "Integer",
161
- decoder(value: string) {
139
+ decoder(input: string) {
162
140
  try {
163
- return BigInt(value);
141
+ return BigInt(input);
164
142
  } catch {
165
143
  throw new TypoError(
166
144
  new TypoText(
167
145
  new TypoString(`Unable to parse: `),
168
- new TypoString(`"${value}"`, typoStyleQuote),
146
+ new TypoString(`"${input}"`, typoStyleQuote),
169
147
  ),
170
148
  );
171
149
  }
@@ -173,10 +151,8 @@ export const typeInteger: Type<bigint> = {
173
151
  };
174
152
 
175
153
  /**
176
- * A {@link Type} that parses a string into a `URL` object using the `URL` constructor.
177
- *
178
- * The string must be a valid absolute URL (e.g. `"https://example.com/path?q=1"`).
179
- * Relative URLs and malformed strings throw a {@link TypoError}.
154
+ * Parses an absolute URL string into a `URL` object.
155
+ * Relative or malformed URLs throw a {@link TypoError}.
180
156
  *
181
157
  * @example
182
158
  * ```ts
@@ -186,14 +162,14 @@ export const typeInteger: Type<bigint> = {
186
162
  */
187
163
  export const typeUrl: Type<URL> = {
188
164
  content: "Url",
189
- decoder(value: string) {
165
+ decoder(input: string) {
190
166
  try {
191
- return new URL(value);
167
+ return new URL(input);
192
168
  } catch {
193
169
  throw new TypoError(
194
170
  new TypoText(
195
171
  new TypoString(`Unable to parse: `),
196
- new TypoString(`"${value}"`, typoStyleQuote),
172
+ new TypoString(`"${input}"`, typoStyleQuote),
197
173
  ),
198
174
  );
199
175
  }
@@ -201,9 +177,7 @@ export const typeUrl: Type<URL> = {
201
177
  };
202
178
 
203
179
  /**
204
- * A {@link Type} that passes the raw string through unchanged (identity decoder).
205
- *
206
- * This is the simplest type and accepts any string value without validation.
180
+ * Identity decoder passes the raw string through unchanged.
207
181
  *
208
182
  * @example
209
183
  * ```ts
@@ -213,35 +187,27 @@ export const typeUrl: Type<URL> = {
213
187
  */
214
188
  export const typeString: Type<string> = {
215
189
  content: "String",
216
- decoder(value: string) {
217
- return value;
190
+ decoder(input: string) {
191
+ return input;
218
192
  },
219
193
  };
220
194
 
221
195
  /**
222
- * Creates a new {@link Type} by chaining a `before` type decoder with an `after`
223
- * transformation.
224
- *
225
- * The raw string is first decoded by `before.decoder`; its result is then passed to
226
- * `after.decoder`. Errors from `before` are wrapped with a "from: <content>" context
227
- * prefix so that the full decoding path is visible in error messages.
228
- *
229
- * Use this when an existing type (e.g. {@link typeString}, {@link typeOneOf}) produces
230
- * an intermediate value that needs a further transformation (e.g. parsing a
231
- * string-keyed enum into a number).
196
+ * Creates a {@link Type} by chaining `before`'s decoder with an `after` transformation.
197
+ * `before` errors are prefixed with `"from: <content>"` for traceability.
232
198
  *
233
- * @typeParam Before - The intermediate type produced by `before.decoder`.
234
- * @typeParam After - The final type produced by `after.decoder`.
199
+ * @typeParam Before - Intermediate type from `before.decoder`.
200
+ * @typeParam After - Final type from `after.decoder`.
235
201
  *
236
- * @param before - The base type that decodes the raw CLI string.
237
- * @param after - The transformation applied to the `Before` value.
238
- * @param after.content - Human-readable name for the resulting type (shown in errors).
239
- * @param after.decoder - Function that converts a `Before` value into `After`.
240
- * @returns A new {@link Type}`<After>` whose `content` is `after.content`.
202
+ * @param before - Base decoder for the raw string.
203
+ * @param after - Transformation applied to the decoded value.
204
+ * @param after.content - Name for the resulting type (shown in errors).
205
+ * @param after.decoder - Converts a `Before` value to `After`.
206
+ * @returns A {@link Type}`<After>`.
241
207
  *
242
208
  * @example
243
209
  * ```ts
244
- * const typePort = typeConverted(typeNumber, {
210
+ * const typePort = typeMapped(typeNumber, {
245
211
  * content: "Port",
246
212
  * decoder: (n) => {
247
213
  * if (n < 1 || n > 65535) throw new Error("Out of range");
@@ -252,16 +218,16 @@ export const typeString: Type<string> = {
252
218
  * // "--port 99999" → TypoError: --port: <PORT>: Port: Out of range
253
219
  * ```
254
220
  */
255
- export function typeConverted<Before, After>(
221
+ export function typeMapped<Before, After>(
256
222
  before: Type<Before>,
257
223
  after: { content: string; decoder: (value: Before) => After },
258
224
  ): Type<After> {
259
225
  return {
260
226
  content: after.content,
261
- decoder: (value: string) => {
227
+ decoder: (input: string) => {
262
228
  return after.decoder(
263
229
  TypoError.tryWithContext(
264
- () => before.decoder(value),
230
+ () => before.decoder(input),
265
231
  () =>
266
232
  new TypoText(
267
233
  new TypoString("from: "),
@@ -274,18 +240,12 @@ export function typeConverted<Before, After>(
274
240
  }
275
241
 
276
242
  /**
277
- * Creates a {@link Type}`<string>` that only accepts a fixed set of string values.
243
+ * Creates a {@link Type}`<string>` accepting only a fixed set of string values.
244
+ * Out-of-set inputs throw a {@link TypoError} listing up to 5 valid options.
278
245
  *
279
- * The decoder performs an exact (case-sensitive) lookup in `values`. If the input is
280
- * not in the set, a {@link TypoError} is thrown listing up to 5 of the valid options.
281
- *
282
- * Combine with {@link typeConverted} to map the accepted strings to a richer type.
283
- *
284
- * @param content - Human-readable name for this type shown in help text and error
285
- * messages (e.g. `"Environment"`, `"LogLevel"`).
286
- * @param values - The ordered list of accepted string values. The order is preserved in
287
- * the error message preview.
288
- * @returns A {@link Type}`<string>` that validates membership in `values`.
246
+ * @param content - Name shown in help and errors (e.g. `"Environment"`).
247
+ * @param values - Ordered list of accepted values.
248
+ * @returns A {@link Type}`<string>`.
289
249
  *
290
250
  * @example
291
251
  * ```ts
@@ -294,16 +254,17 @@ export function typeConverted<Before, After>(
294
254
  * typeEnv.decoder("unknown") // throws TypoError: Invalid value: "unknown" (expected one of: "dev" | "staging" | "prod")
295
255
  * ```
296
256
  */
297
- export function typeOneOf(
257
+ export function typeOneOf<const Value extends string>(
298
258
  content: string,
299
- values: Array<string>,
300
- ): Type<string> {
301
- const valuesSet = new Set(values);
259
+ values: Array<Value>,
260
+ ): Type<Value> {
302
261
  return {
303
262
  content: content,
304
- decoder(value: string) {
305
- if (valuesSet.has(value)) {
306
- return value;
263
+ decoder(input: string) {
264
+ for (const value of values) {
265
+ if (input === value) {
266
+ return value;
267
+ }
307
268
  }
308
269
  const valuesPreview = [];
309
270
  for (const value of values) {
@@ -319,7 +280,7 @@ export function typeOneOf(
319
280
  throw new TypoError(
320
281
  new TypoText(
321
282
  new TypoString(`Invalid value: `),
322
- new TypoString(`"${value}"`, typoStyleQuote),
283
+ new TypoString(`"${input}"`, typoStyleQuote),
323
284
  new TypoString(` (expected one of: `),
324
285
  ...valuesPreview,
325
286
  new TypoString(`)`),
@@ -330,23 +291,14 @@ export function typeOneOf(
330
291
  }
331
292
 
332
293
  /**
333
- * Creates a {@link Type} that decodes a single delimited string into a fixed-length
334
- * tuple of typed elements.
335
- *
336
- * The raw string is split on `separator` into exactly `elementTypes.length` parts.
337
- * Each part is decoded by its corresponding element type. If the number of splits does
338
- * not match, or if any element's decoder fails, a {@link TypoError} is thrown with the
339
- * index and element type context.
294
+ * Splits a delimited string into a fixed-length typed tuple.
295
+ * Each part is decoded by the corresponding element type; wrong count or decode failure throws {@link TypoError}.
340
296
  *
341
- * The resulting `content` is the element types' `content` values joined by `separator`
342
- * (e.g. `"Number,String"` for a `[number, string]` tuple with `","` separator).
297
+ * @typeParam Elements - Tuple of decoded value types (inferred from `elementTypes`).
343
298
  *
344
- * @typeParam Elements - The tuple type of decoded element values (inferred from
345
- * `elementTypes`).
346
- *
347
- * @param elementTypes - An ordered array of {@link Type}s, one per tuple element.
348
- * @param separator - The string used to split the raw value (default: `","`).
349
- * @returns A {@link Type}`<Elements>` tuple type.
299
+ * @param elementTypes - One {@link Type} per tuple element, in order.
300
+ * @param separator - Delimiter (default `","`).
301
+ * @returns A {@link Type}`<Elements>`.
350
302
  *
351
303
  * @example
352
304
  * ```ts
@@ -391,26 +343,14 @@ export function typeTuple<const Elements extends Array<any>>(
391
343
  }
392
344
 
393
345
  /**
394
- * Creates a {@link Type} that decodes a single delimited string into an array of
395
- * homogeneous typed elements.
396
- *
397
- * The raw string is split on `separator` and each part is decoded by `elementType`.
398
- * If any element's decoder fails, a {@link TypoError} is thrown with the index and
399
- * element type context.
400
- *
401
- * Unlike {@link typeTuple}, the number of elements is not fixed; the result array
402
- * length equals the number of `separator`-delimited parts in the input string. To pass
403
- * an empty array, the user must pass an empty string (`""`), which splits into one
404
- * empty-string element — consider using {@link optionRepeatable} instead if you want a
405
- * naturally empty default.
346
+ * Splits a delimited string into a variable-length typed array.
347
+ * Each part is decoded by `elementType`; failed decodes throw {@link TypoError}.
348
+ * Note: splitting an empty string yields one empty element — prefer {@link optionRepeatable} for a zero-default.
406
349
  *
407
- * The `content` is formatted as `"<elementContent>[<sep><elementContent>]..."` to
408
- * signal repeatability.
350
+ * @typeParam Value - Element type produced by `elementType.decoder`.
409
351
  *
410
- * @typeParam Value - The TypeScript element type produced by `elementType.decoder`.
411
- *
412
- * @param elementType - The {@link Type} used to decode each element.
413
- * @param separator - The string used to split the raw value (default: `","`).
352
+ * @param elementType - Decoder applied to each element.
353
+ * @param separator - Delimiter (default `","`).
414
354
  * @returns A {@link Type}`<Array<Value>>`.
415
355
  *
416
356
  * @example
@@ -429,8 +369,8 @@ export function typeList<Value>(
429
369
  ): Type<Array<Value>> {
430
370
  return {
431
371
  content: `${elementType.content}[${separator}${elementType.content}]...`,
432
- decoder(value: string) {
433
- const splits = value.split(separator);
372
+ decoder(input: string) {
373
+ const splits = input.split(separator);
434
374
  return splits.map((split, index) =>
435
375
  TypoError.tryWithContext(
436
376
  () => elementType.decoder(split),