@bomb.sh/router 0.7.0 → 0.8.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 (45) hide show
  1. package/README.md +50 -24
  2. package/esm/lib/checkpoint.js +26 -1
  3. package/esm/lib/command.d.ts +3 -2
  4. package/esm/lib/command.js +2 -1
  5. package/esm/lib/dynamic.d.ts +3 -3
  6. package/esm/lib/dynamic.js +20 -3
  7. package/esm/lib/extend.d.ts +3 -2
  8. package/esm/lib/extend.js +3 -1
  9. package/esm/lib/parse.js +6 -5
  10. package/esm/lib/pipeline.d.ts +1 -1
  11. package/esm/lib/route.d.ts +16 -3
  12. package/esm/lib/route.js +5 -2
  13. package/esm/lib/types.d.ts +4 -4
  14. package/esm/mod.d.ts +2 -1
  15. package/esm/mod.js +2 -1
  16. package/package.json +1 -1
  17. package/src/_dnt.polyfills.ts +0 -27
  18. package/src/lib/argument.ts +0 -100
  19. package/src/lib/bind.ts +0 -421
  20. package/src/lib/checkpoint.ts +0 -10
  21. package/src/lib/command.ts +0 -36
  22. package/src/lib/dasherize.ts +0 -7
  23. package/src/lib/decode.ts +0 -55
  24. package/src/lib/definition.ts +0 -17
  25. package/src/lib/dynamic.ts +0 -39
  26. package/src/lib/env.ts +0 -142
  27. package/src/lib/extend.ts +0 -12
  28. package/src/lib/maybe.ts +0 -22
  29. package/src/lib/multiple.ts +0 -23
  30. package/src/lib/option.ts +0 -57
  31. package/src/lib/param.ts +0 -96
  32. package/src/lib/parse.ts +0 -478
  33. package/src/lib/pipeline.ts +0 -549
  34. package/src/lib/print.ts +0 -216
  35. package/src/lib/read.ts +0 -133
  36. package/src/lib/rest.ts +0 -10
  37. package/src/lib/result.ts +0 -10
  38. package/src/lib/route.ts +0 -77
  39. package/src/lib/toggle.ts +0 -132
  40. package/src/lib/tokenize.ts +0 -74
  41. package/src/lib/tokenizer.ts +0 -191
  42. package/src/lib/transform.ts +0 -49
  43. package/src/lib/types.ts +0 -460
  44. package/src/lib/values.ts +0 -131
  45. package/src/mod.ts +0 -74
package/README.md CHANGED
@@ -40,7 +40,6 @@ import {
40
40
  name,
41
41
  option,
42
42
  route,
43
- routes,
44
43
  schema,
45
44
  toggle,
46
45
  version,
@@ -52,19 +51,15 @@ export const app = command(
52
51
  description("Run and manage local service simulators."),
53
52
  version("1.0.0"),
54
53
  toggle(name("verbose")),
55
- routes(
54
+ command(
55
+ name("serve"),
56
+ option(name("port"), schema(z.number().default(4000))),
57
+ ),
58
+ route(
59
+ name("database"),
56
60
  command(
57
- name("serve"),
58
- option(name("port"), schema(z.number().default(4000))),
59
- ),
60
- route(
61
- name("database"),
62
- routes(
63
- command(
64
- name("clean"),
65
- toggle(name("dryRun")),
66
- ),
67
- ),
61
+ name("clean"),
62
+ toggle(name("dryRun")),
68
63
  ),
69
64
  ),
70
65
  );
@@ -79,6 +74,10 @@ HELP /database
79
74
  HELP /database/clean EXECUTE /database/clean
80
75
  ```
81
76
 
77
+ Nest `route()` and `command()` directly inside another route, command, or
78
+ `extend()`. Each child is mounted at that point in the pipeline. Use `extend()`
79
+ to package reusable groups of children and other elements.
80
+
82
81
  The root name identifies the executable; it is not repeated in route IDs.
83
82
  `simulacrum serve` therefore selects `/serve`, not `/simulacrum/serve`.
84
83
 
@@ -195,6 +194,32 @@ Dynamic phases serve two common cases:
195
194
  route-extension mechanism. Both keep I/O in the application while preserving the
196
195
  exact type of what parsing can produce next.
197
196
 
197
+ `dynamic(schema, extension)` requires a Standard Schema for the value supplied
198
+ to `resume()`. The schema's input type determines what `resume()` accepts; its
199
+ validated output is passed to the extension, including any schema transforms.
200
+
201
+ ```ts
202
+ import { command, dynamic, extend, name, parse } from "@bomb.sh/router";
203
+ import * as z from "zod";
204
+
205
+ const app = command(
206
+ name("plugins"),
207
+ dynamic(
208
+ z.array(z.string()),
209
+ (plugins) => extend(...plugins.map((plugin) => command(name(plugin)))),
210
+ ),
211
+ );
212
+
213
+ const step = parse(app, { argv: ["serve"] });
214
+ if (step.ok) {
215
+ const result = step.resume(["serve"]); // EXECUTE /serve
216
+ }
217
+ ```
218
+
219
+ Invalid input produces `unprocessable-content` with the schema's issues and
220
+ paths, without applying the extension. Validation must be synchronous; async
221
+ schemas also produce `unprocessable-content`.
222
+
198
223
  ### Help and version cross checkpoints
199
224
 
200
225
  `--help` and `--version` request methods; they do not settle an intent or bypass
@@ -216,12 +241,12 @@ app --config app.json auth0 --help
216
241
 
217
242
  Help and version are not escape hatches around configuration loading. Do not
218
243
  inspect `argv` to skip a checkpoint. A phase may be required by `HELP`,
219
- `VERSION`, or `EXECUTE`, so its driver work must be safe for all three: return
220
- loading and validation failures as `Result` issues, avoid command side effects,
221
- and defer execution until an `EXECUTE` intent. If discovery fails, report that
222
- failure rather than printing incomplete help for an unresolved route graph.
223
- Routes without dynamic phases still resolve directly; the rule is to stop only
224
- at an intent or failure, never merely because the arguments look informational.
244
+ `VERSION`, or `EXECUTE`, so its driver work must be safe for all three: handle
245
+ loading failures in the application, avoid command side effects, and defer
246
+ execution until an `EXECUTE` intent. If discovery fails, report that failure
247
+ rather than printing incomplete help for an unresolved route graph. Routes
248
+ without dynamic phases still resolve directly; the rule is to stop only at an
249
+ intent or failure, never merely because the arguments look informational.
225
250
 
226
251
  ```ts
227
252
  import process from "node:process";
@@ -234,7 +259,6 @@ import {
234
259
  printErrors,
235
260
  printHelp,
236
261
  printVersion,
237
- type Result,
238
262
  schema,
239
263
  type ValueSource,
240
264
  version,
@@ -280,13 +304,15 @@ switch (result.method) {
280
304
  break;
281
305
  }
282
306
 
283
- declare function load(path: string): Promise<Result<ValueSource[]>>;
307
+ declare function load(path: string): Promise<ValueSource[]>;
284
308
  ```
285
309
 
286
310
  The parser remains synchronous and performs no I/O. The caller loads the file
287
- and resumes with a `Result`; loader failures enter the ordinary issue path.
288
- Unconsumed CLI input survives the pause, so a later `--port 5000` can override
289
- the value loaded from the file.
311
+ and resumes directly with a value-source array. `checkpoint()` validates the
312
+ source names and the presence of their values; later parameter schemas validate
313
+ the contents. The application handles I/O failures before resuming. Unconsumed
314
+ CLI input survives the pause, so a later `--port 5000` can override the value
315
+ loaded from the file.
290
316
 
291
317
  The same phase mechanism can add options or routes from runtime data. Parsing
292
318
  then continues against the expanded route graph, and the continuation type
@@ -1,5 +1,30 @@
1
1
  import { dynamic } from "./dynamic.js";
2
2
  import { withValues } from "./values.js";
3
3
  export function checkpoint() {
4
- return dynamic((values) => withValues(values));
4
+ return dynamic(sources, (values) => withValues(values));
5
5
  }
6
+ const sources = {
7
+ "~standard": {
8
+ version: 1,
9
+ vendor: "@bomb.sh/router",
10
+ validate(value) {
11
+ if (!Array.isArray(value)) {
12
+ return { issues: [{ message: "expected an array of value sources" }] };
13
+ }
14
+ let issues = [];
15
+ for (let [index, source] of value.entries()) {
16
+ if (source === null || typeof source !== "object") {
17
+ issues.push({ message: "expected a value source", path: [index] });
18
+ continue;
19
+ }
20
+ if (typeof source.name !== "string") {
21
+ issues.push({ message: "expected a string", path: [index, "name"] });
22
+ }
23
+ if (!("value" in source)) {
24
+ issues.push({ message: "expected a value", path: [index, "value"] });
25
+ }
26
+ }
27
+ return issues.length > 0 ? { issues } : { value: value };
28
+ },
29
+ },
30
+ };
@@ -1,6 +1,7 @@
1
- import type { Check, Fold, Materialize, Unary } from "./pipeline.js";
1
+ import type { Fold, Materialize } from "./pipeline.js";
2
+ import { type Input, type Normalize, type Validate } from "./route.js";
2
3
  import type { Definition, Done, Route } from "./types.js";
3
4
  export type CommandZero<N extends string = string> = Route<N, "help" | "execute", [
4
5
  Done<{}, []>
5
6
  ]>;
6
- export declare function command<const N extends string, const E extends readonly Unary[]>(start: Definition<N>, ...elements: E & Check<CommandZero<N>, E>): Materialize<Fold<CommandZero<N>, E>>;
7
+ export declare function command<const N extends string, const E extends readonly Input[]>(start: Definition<N>, ...elements: E & Validate<CommandZero<N>, E>): Materialize<Fold<CommandZero<N>, Normalize<E>>>;
@@ -1,3 +1,4 @@
1
+ import { normalize, } from "./route.js";
1
2
  export function command(start, ...elements) {
2
3
  let zero = {
3
4
  ...start,
@@ -12,5 +13,5 @@ export function command(start, ...elements) {
12
13
  envs: [],
13
14
  }],
14
15
  };
15
- return elements.reduce((value, element) => element(value), zero);
16
+ return normalize(elements).reduce((value, element) => element(value), zero);
16
17
  }
@@ -1,6 +1,6 @@
1
- import type { AnyRoute } from "./types.js";
2
- import { type AnyElement, type DynamicElement } from "./pipeline.js";
1
+ import type { AnyRoute, Schema } from "./types.js";
2
+ import { type AnyElement, type DynamicElement, type Element } from "./pipeline.js";
3
3
  export type { ConjoinPhases, Seed } from "./pipeline.js";
4
4
  export type PhaseOf<R extends AnyRoute> = R["phases"][0];
5
5
  export type PhasesOf<R extends AnyRoute> = R["phases"];
6
- export declare function dynamic<Requirement, E>(extension: (requires: Requirement) => E, ..._valid: E extends AnyElement ? [] : [never]): DynamicElement<Requirement, Extract<E, AnyElement>>;
6
+ export declare function dynamic<Input, Output, E extends Element>(schema: Schema<Input, Output>, extension: (requires: Output) => E): DynamicElement<Input, Extract<E, AnyElement>>;
@@ -1,11 +1,28 @@
1
- import { brand } from "./pipeline.js";
2
- export function dynamic(extension, ..._valid) {
1
+ import { brand, } from "./pipeline.js";
2
+ // Constrain only the brand while inferring E. The full operation would
3
+ // contextually widen extend() tuples and erase their continuation types.
4
+ export function dynamic(schema, extension) {
3
5
  return brand((route) => {
4
6
  let phases = [...route.phases];
5
7
  let phase = phases.pop();
6
8
  phases.push({
7
9
  ...phase,
8
- resolver: extension,
10
+ resolver(requirement) {
11
+ let result = schema["~standard"].validate(requirement);
12
+ if (result instanceof Promise) {
13
+ return {
14
+ ok: false,
15
+ issues: [{ message: "async schemas are not allowed" }],
16
+ };
17
+ }
18
+ if (result.issues) {
19
+ return { ok: false, issues: result.issues };
20
+ }
21
+ return {
22
+ ok: true,
23
+ value: extension(result.value),
24
+ };
25
+ },
9
26
  });
10
27
  phases.push({
11
28
  model: {
@@ -1,2 +1,3 @@
1
- import { type Extension, type Unary } from "./pipeline.js";
2
- export declare function extend<const E extends readonly Unary[]>(...elements: E): Extension<E>;
1
+ import { type Input, type Normalize } from "./route.js";
2
+ import { type Extension } from "./pipeline.js";
3
+ export declare function extend<const E extends readonly Input[]>(...elements: E): Extension<Normalize<E>>;
package/esm/lib/extend.js CHANGED
@@ -1,4 +1,6 @@
1
+ import { normalize } from "./route.js";
1
2
  import { brand } from "./pipeline.js";
2
3
  export function extend(...elements) {
3
- return brand((start) => elements.reduce((value, element) => element(value), start));
4
+ let normalized = normalize(elements);
5
+ return brand((start) => normalized.reduce((value, element) => element(value), start));
4
6
  }
package/esm/lib/parse.js CHANGED
@@ -118,16 +118,17 @@ function advance(state) {
118
118
  ok: true,
119
119
  route: segment.id,
120
120
  model: binding.model,
121
- resume(result) {
122
- if (!result.ok) {
123
- return unprocessableContent(segment, result.issues);
124
- }
121
+ resume(value) {
125
122
  // AnyPhase erases these exact types with `never`; dynamic() already
126
123
  // proved them at its public boundary.
127
124
  let resolver = phase.resolver;
125
+ let result = resolver(value);
126
+ if (!result.ok) {
127
+ return unprocessableContent(segment, result.issues);
128
+ }
128
129
  // The extension operates against the same aggregate route metadata,
129
130
  // but begins with one fresh, empty phase.
130
- let continuation = resolver(result.value)(seed(segment.route));
131
+ let continuation = result.value(seed(segment.route));
131
132
  let phases = stitch(continuation.phases, segment.phases.slice(1));
132
133
  let history = [...segment.history, phase];
133
134
  let next = {
@@ -1,5 +1,5 @@
1
1
  import type { AddRoutesToLast, AnyPhase, AnyPhases, AnyRoute, Done, Method, MethodsOf, Next, Phase, Route } from "./types.js";
2
- export type Element<D extends Delta> = {
2
+ export type Element<D = unknown> = {
3
3
  readonly [operation]: D;
4
4
  };
5
5
  export type AnyElement = Element<Delta>;
@@ -1,9 +1,22 @@
1
- import { type Check, type Fold, type Materialize, type MethodElement, type RoutesElement, type Unary } from "./pipeline.js";
1
+ import { type Fold, type Materialize, type MethodElement, type RoutesElement, type Unary } from "./pipeline.js";
2
2
  import type { AnyRoute, Definition, Done, Route } from "./types.js";
3
3
  export type RouteZero<N extends string = string> = Route<N, "help", [
4
4
  Done<{}, []>
5
5
  ]>;
6
- export declare function route<const N extends string, const E extends readonly Unary[]>(start: Definition<N>, ...elements: E & Check<RouteZero<N>, E>): Materialize<Fold<RouteZero<N>, E>>;
6
+ export declare function route<const N extends string, const E extends readonly Input[]>(start: Definition<N>, ...elements: E & Validate<RouteZero<N>, E>): Materialize<Fold<RouteZero<N>, Normalize<E>>>;
7
7
  export declare function version(semver: string): MethodElement<"version">;
8
8
  export declare function executable(): MethodElement<"execute">;
9
- export declare function routes<const C extends readonly AnyRoute[]>(...children: C): RoutesElement<C>;
9
+ export type Input = Unary | AnyRoute;
10
+ export type Normalize<E extends readonly Input[]> = {
11
+ [K in keyof E]: Normalized<E[K]>;
12
+ };
13
+ export type Validate<S, E extends readonly Input[]> = [
14
+ Fold<S, Normalize<E>>
15
+ ] extends [never] ? Invalid<S, E> : unknown;
16
+ export declare function normalize<const E extends readonly Input[]>(elements: E): Normalize<E>;
17
+ type Normalized<E extends Input> = E extends Unary ? E : E extends AnyRoute ? RoutesElement<readonly [E]> : never;
18
+ type Invalid<S, E extends readonly Input[]> = E extends readonly [
19
+ infer Head extends Input,
20
+ ...infer Tail extends readonly Input[]
21
+ ] ? Fold<S, readonly [Normalized<Head>]> extends infer Next ? [Next] extends [never] ? readonly [never, ...Tail] : readonly [Head, ...Invalid<Next, Tail>] : never : E;
22
+ export {};
package/esm/lib/route.js CHANGED
@@ -14,7 +14,7 @@ export function route(start, ...elements) {
14
14
  envs: [],
15
15
  }],
16
16
  };
17
- return elements.reduce((value, element) => element(value), zero);
17
+ return normalize(elements).reduce((value, element) => element(value), zero);
18
18
  }
19
19
  export function version(semver) {
20
20
  return brand((route) => ({
@@ -29,7 +29,10 @@ export function executable() {
29
29
  methods: [...route.methods, "execute"],
30
30
  }));
31
31
  }
32
- export function routes(...children) {
32
+ export function normalize(elements) {
33
+ return elements.map((element) => typeof element === "function" ? element : routes(element));
34
+ }
35
+ function routes(...children) {
33
36
  return brand((route) => {
34
37
  let phases = [...route.phases];
35
38
  let phase = phases.pop();
@@ -27,7 +27,7 @@ export type Next<Model extends object, Routes extends readonly AnyRoute[], T> =
27
27
  readonly routes: Routes;
28
28
  readonly values: readonly ValueSource[];
29
29
  readonly envs: readonly EnvSource[];
30
- readonly resolver: (requirement: T) => (input: AnyRoute) => AnyRoute;
30
+ readonly resolver: (requirement: T) => Result<(input: AnyRoute) => AnyRoute>;
31
31
  };
32
32
  export type Done<Model extends object, Routes extends readonly AnyRoute[]> = {
33
33
  readonly [phaseModel]?: Model;
@@ -44,7 +44,7 @@ export interface ParseIncrement<R extends AnyRoute, P extends RoutePath = "/", M
44
44
  readonly ok: true;
45
45
  readonly route: P;
46
46
  readonly model: IncrementModelOf<R>;
47
- resume(result: Result<RequirementOf<R>>): Outcome<ParseAt<ContinuationOf<R>, P, Models>>;
47
+ resume(value: RequirementOf<R>): Outcome<ParseAt<ContinuationOf<R>, P, Models>>;
48
48
  }
49
49
  export type ContinuationOf<R extends AnyRoute> = Route<R["name"], MethodsOf<R>, SettleNext<R["phases"]>>;
50
50
  export type ModelOf<R extends AnyRoute, P extends RoutePath = "/"> = ModelOfRoute<RouteAt<R, P>>;
@@ -65,7 +65,7 @@ export interface AnyPhase {
65
65
  readonly routes: readonly AnyRoute[];
66
66
  readonly values: readonly ValueSource[];
67
67
  readonly envs: readonly EnvSource[];
68
- readonly resolver?: (requirement: never) => (route: never) => AnyRoute;
68
+ readonly resolver?: (requirement: never) => Result<(route: never) => AnyRoute>;
69
69
  }
70
70
  export type AnyPhases = readonly [AnyPhase, ...AnyPhase[]];
71
71
  export type Method = "help" | "version" | "execute";
@@ -154,7 +154,7 @@ type NextModelIn<P extends readonly AnyPhase[]> = P extends readonly [
154
154
  ...infer Tail extends readonly AnyPhase[]
155
155
  ] ? Head extends Next<infer Model, readonly AnyRoute[], infer _Requirement> ? Model : NextModelIn<Tail> : never;
156
156
  type RequirementIn<P extends AnyPhase> = P extends {
157
- readonly resolver: (requirement: infer Requirement) => (route: AnyRoute) => AnyRoute;
157
+ readonly resolver: (requirement: infer Requirement) => Result<(route: AnyRoute) => AnyRoute>;
158
158
  } ? Requirement : never;
159
159
  type RequirementInPhases<P extends readonly AnyPhase[]> = P extends readonly [
160
160
  infer Head extends AnyPhase,
package/esm/mod.d.ts CHANGED
@@ -4,6 +4,7 @@ export { extend } from "./lib/extend.js";
4
4
  export { command } from "./lib/command.js";
5
5
  export type { CommandZero } from "./lib/command.js";
6
6
  export { checkpoint } from "./lib/checkpoint.js";
7
+ export { dynamic } from "./lib/dynamic.js";
7
8
  export { description, name } from "./lib/definition.js";
8
9
  export { env, withEnvs } from "./lib/env.js";
9
10
  export type { Environment, EnvSource } from "./lib/env.js";
@@ -18,7 +19,7 @@ export { printErrors, printHelp, printVersion } from "./lib/print.js";
18
19
  export type { Rest } from "./lib/rest.js";
19
20
  export { cli } from "./lib/read.js";
20
21
  export type { CLIOptions, CLIRead, ReadCLI, Symbol as CLISymbol, } from "./lib/read.js";
21
- export { executable, route, routes, version } from "./lib/route.js";
22
+ export { executable, route, version } from "./lib/route.js";
22
23
  export type { RouteZero } from "./lib/route.js";
23
24
  export { toggle } from "./lib/toggle.js";
24
25
  export { transform } from "./lib/transform.js";
package/esm/mod.js CHANGED
@@ -3,6 +3,7 @@ export { argument } from "./lib/argument.js";
3
3
  export { extend } from "./lib/extend.js";
4
4
  export { command } from "./lib/command.js";
5
5
  export { checkpoint } from "./lib/checkpoint.js";
6
+ export { dynamic } from "./lib/dynamic.js";
6
7
  export { description, name } from "./lib/definition.js";
7
8
  export { env, withEnvs } from "./lib/env.js";
8
9
  export { option } from "./lib/option.js";
@@ -12,7 +13,7 @@ export { parse } from "./lib/parse.js";
12
13
  export { mark } from "./lib/pipeline.js";
13
14
  export { printErrors, printHelp, printVersion } from "./lib/print.js";
14
15
  export { cli } from "./lib/read.js";
15
- export { executable, route, routes, version } from "./lib/route.js";
16
+ export { executable, route, version } from "./lib/route.js";
16
17
  export { toggle } from "./lib/toggle.js";
17
18
  export { transform } from "./lib/transform.js";
18
19
  export { withValues } from "./lib/values.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bomb.sh/router",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Configuration for programs",
5
5
  "author": "Bombshell Authors (https://github.com/bombshell-dev)",
6
6
  "repository": {
@@ -1,27 +0,0 @@
1
- // https://github.com/tc39/proposal-accessible-object-hasownproperty/blob/main/polyfill.js
2
- if (!Object.hasOwn) {
3
- Object.defineProperty(Object, "hasOwn", {
4
- value: function (object: any, property: any) {
5
- if (object == null) {
6
- throw new TypeError("Cannot convert undefined or null to object");
7
- }
8
- return Object.prototype.hasOwnProperty.call(Object(object), property);
9
- },
10
- configurable: true,
11
- enumerable: false,
12
- writable: true,
13
- });
14
- }
15
-
16
- declare global {
17
- interface Object {
18
- /**
19
- * Determines whether an object has a property with the specified name.
20
- * @param o An object.
21
- * @param v A property name.
22
- */
23
- hasOwn(o: object, v: PropertyKey): boolean;
24
- }
25
- }
26
-
27
- export {};
@@ -1,100 +0,0 @@
1
- import { type AnyParam, type Param, param, type ParamModel } from "./param.js";
2
- import {
3
- brand,
4
- type Check,
5
- type Fold,
6
- type ModelElement,
7
- type Unary,
8
- } from "./pipeline.js";
9
- import type { CLIRead, ReadCLI } from "./read.js";
10
- import type { Word } from "./tokenize.js";
11
- import type { AnyRoute, Definition } from "./types.js";
12
-
13
- export function argument<
14
- const N extends string,
15
- const E extends readonly Unary[],
16
- >(
17
- named: Definition<N>,
18
- ...elements: E & Check<Zero<N>, E>
19
- ): ElementOf<N, Fold<Zero<N>, E>> {
20
- const added = elements.reduce<unknown>(
21
- (value, element) => element(value as never),
22
- param(named, positional),
23
- ) as AnyParam;
24
-
25
- return brand<ElementOf<N, Fold<Zero<N>, E>>>(
26
- (route: AnyRoute) => {
27
- let phases = [...route.phases];
28
- let phase = phases.pop()!;
29
- phases.push({
30
- ...phase,
31
- model: {
32
- params: {
33
- ...phase.model.params,
34
- [added.name]: added,
35
- },
36
- steps: phase.model.steps.concat((current, bindings) => ({
37
- ok: true,
38
- value: {
39
- ...current,
40
- [added.name]: bindings[added.name],
41
- },
42
- })),
43
- },
44
- });
45
-
46
- return {
47
- ...route,
48
- phases,
49
- };
50
- },
51
- );
52
- }
53
-
54
- type Zero<N extends string> = Param<N, unknown, "one">;
55
-
56
- type ElementOf<N extends string, P> = P extends
57
- Param<N, infer Model, infer _Cardinality> ? ModelElement<ParamModel<N, Model>>
58
- : never;
59
-
60
- function positional<P extends AnyParam>(param: P): P {
61
- return {
62
- ...param,
63
- cli: {
64
- read,
65
- syntax: {
66
- type: "argument",
67
- label: `<${param.name.toUpperCase()}>`,
68
- },
69
- },
70
- };
71
- }
72
-
73
- const read: ReadCLI = (tokens): CLIRead => {
74
- let claim = tokens.claimOne((token): token is Word => token.type === "word");
75
- let [word] = claim.tokens;
76
-
77
- return word
78
- ? {
79
- claim,
80
- result: {
81
- ok: true,
82
- value: { exists: true, value: word.text },
83
- issues: [],
84
- },
85
- }
86
- : nothing(tokens);
87
- };
88
-
89
- function nothing(tokens: Parameters<ReadCLI>[0]): CLIRead {
90
- let claim = tokens.claimAll(() => false);
91
-
92
- return {
93
- claim,
94
- result: {
95
- ok: true,
96
- value: { exists: false },
97
- issues: [],
98
- },
99
- };
100
- }