Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/insights.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ The detailed phase-binding design is recorded in [Binding](./binding.md).

- The resolved route has a directly typed `model`.
- The result also carries path-addressed `models` for every matched route.
- A route's phase tuple is the single source of truth for both its aggregate
model and its children; public helpers derive those views instead of caching
them as additional route type parameters.
- Route identity and result projection are separate concerns.
- Namespacing is a mounting/address operation, not something plugin authors
repeat locally.
Expand All @@ -116,7 +119,9 @@ The detailed phase-binding design is recorded in [Binding](./binding.md).
- A failed requirement never invokes the resolver.
- `RequirementsOf<R>` preserves requirement order; `RequirementOf<R>` is its
head.
- `ContinuationOf<R>` removes the current phase.
- `ContinuationOf<R>` settles the current unresolved phase and retains it as
immutable type history, so later continuations can derive the aggregate model
without a separate model cache.
- The static result type says whether parsing yields another increment or an
intent; callers should not need a runtime `done` check.
- Help and version may require resolving earlier phases when those phases can
Expand Down
21 changes: 15 additions & 6 deletions lib/argument.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { type Param, param } from "./param.ts";
import { type Param, param, type ParamModel } from "./param.ts";
import {
brand,
type Check,
type Fold,
type ParamElement,
type ModelElement,
type Unary,
} from "./pipeline.ts";
import type { CLIRead, ReadCLI } from "./read.ts";
Expand All @@ -28,9 +28,18 @@ export function argument<
let phase = phases.pop()!;
phases.push({
...phase,
params: {
...phase.params,
[added.name]: added,
model: {
params: {
...phase.model.params,
[added.name]: added,
},
steps: phase.model.steps.concat((current, bindings) => ({
ok: true,
value: {
...current,
[added.name]: bindings[added.name],
},
})),
},
});

Expand All @@ -45,7 +54,7 @@ export function argument<
type ValueOf<P> = P extends Param<string, infer T> ? T : never;

type ElementOf<N extends string, P> = P extends Param<N, unknown>
? ParamElement<N, ValueOf<P>>
? ModelElement<ParamModel<N, ValueOf<P>>>
: never;

function positional<P extends Param<string, unknown>>(param: P): P {
Expand Down
19 changes: 15 additions & 4 deletions lib/bind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export function bindPhase(options: {
}): PhaseBinding {
let { phase, segment } = options;
let rest = options.rest;
let params = Object.values(phase.params) as Param<string, unknown>[];
let params = Object.values(phase.model.params) as Param<string, unknown>[];
let pending = new Map(params.map((param) => [param.name, param]));
let results = new Map<string, Result<unknown>>();

Expand Down Expand Up @@ -179,7 +179,7 @@ export function bindPhase(options: {
results.set(param.name, validate(param, undefined, [param.name]));
}

let model: Record<string, unknown> = {};
let bindings: Record<string, unknown> = {};
let issues: Issue[] = [];
let valid = true;

Expand All @@ -188,14 +188,25 @@ export function bindPhase(options: {
for (let param of params) {
let result = results.get(param.name)!;
issues.push(...result.issues ?? []);

if (result.ok) {
model[param.name] = result.value;
bindings[param.name] = result.value;
} else {
valid = false;
}
}

let model = {};
for (let step of phase.model.steps) {
let result = step(model, bindings);
if (!result.ok) {
valid = false;
issues.push(...result.issues);
break;
} else {
model = result.value;
}
}

return { rest, model, issues, valid };
}

Expand Down
7 changes: 4 additions & 3 deletions lib/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import type { Definition, Done, Route } from "./types.ts";
export type CommandZero<N extends string = string> = Route<
N,
"help" | "execute",
{},
[],
[Done<{}, []>]
>;

Expand All @@ -21,7 +19,10 @@ export function command<
...start,
methods: ["help", "execute"],
phases: [{
params: {},
model: {
params: {},
steps: [],
},
routes: [],
values: [],
envs: [],
Expand Down
5 changes: 4 additions & 1 deletion lib/dynamic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export function dynamic<
) => (input: never) => AnyRoute,
});
phases.push({
params: {},
model: {
params: {},
steps: [],
},
routes: [],
values: [],
envs: [],
Expand Down
21 changes: 15 additions & 6 deletions lib/option.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { type Param, param } from "./param.ts";
import { type Param, param, type ParamModel } from "./param.ts";
import { dasherize } from "./dasherize.ts";
import {
brand,
type Check,
type Fold,
type ParamElement,
type ModelElement,
type Unary,
} from "./pipeline.ts";
import { cli } from "./read.ts";
Expand All @@ -28,9 +28,18 @@ export function option<
let phase = phases.pop()!;
phases.push({
...phase,
params: {
...phase.params,
[added.name]: added,
model: {
params: {
...phase.model.params,
[added.name]: added,
},
steps: phase.model.steps.concat((current, bindings) => ({
ok: true,
value: {
...current,
[added.name]: bindings[added.name],
},
})),
},
});
return {
Expand All @@ -44,5 +53,5 @@ export function option<
type ValueOf<P> = P extends Param<string, infer T> ? T : never;

type ElementOf<N extends string, P> = P extends Param<N, unknown>
? ParamElement<N, ValueOf<P>>
? ModelElement<ParamModel<N, ValueOf<P>>>
: never;
19 changes: 12 additions & 7 deletions lib/param.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type Check,
type Fold,
mark,
type ModelPatch,
type Transform,
type TransformElement,
type Unary,
Expand All @@ -17,6 +18,8 @@ export interface Param<K extends string, T> extends Definition<K> {
env?: string;
}

export type ParamModel<K extends string, V> = ModelPatch<{ [P in K]: V }>;

export function param<
const K extends string,
const E extends readonly Unary[],
Expand Down Expand Up @@ -51,17 +54,19 @@ export function param<

export function schema<S extends Schema>(
schema: S,
): TransformElement<SchemaTransform<S>> {
return mark<SchemaTransform<S>>((param: Param<string, unknown>) => ({
...param,
schema,
}));
): TransformElement<SchemaTransform<OutputOf<S>>> {
return mark<SchemaTransform<OutputOf<S>>>(
(param: Param<string, unknown>) => ({
...param,
schema,
}),
);
}

interface SchemaTransform<S extends Schema> extends Transform {
interface SchemaTransform<Output> extends Transform {
readonly input: Param<string, unknown>;
readonly output: this["input"] extends Param<infer N, unknown>
? Param<N, OutputOf<S>>
? Param<N, Output>
: never;
}

Expand Down
25 changes: 18 additions & 7 deletions lib/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function parse(
envs: new Envs().mount([], input.envs ?? []),
};

return resume({
return advance({
segments: [{
id: "/",
route,
Expand All @@ -70,7 +70,8 @@ export function parse(
literals: literals.tokens,
});
}
function resume(

function advance(
state: ParserState,
): Outcome<AnyIntent | AnyIncrement> {
while (true) {
Expand Down Expand Up @@ -203,7 +204,7 @@ function resume(
],
};

return resume({
return advance({
...suspended,
segments: replace(suspended.segments, index, next),
});
Expand Down Expand Up @@ -342,10 +343,17 @@ function stitch(
phases.push({
...phase,
...next,
params: {
...phase.params,
...next.params,
model: {
params: {
...phase.model.params,
...next.model.params,
},
steps: [
...phase.model.steps,
...next.model.steps,
],
},

routes: [
...phase.routes,
...next.routes,
Expand Down Expand Up @@ -419,7 +427,10 @@ function seed(route: AnyRoute): AnyRoute {
return {
...route,
phases: [{
params: {},
model: {
params: {},
steps: [],
},
routes: [],
values: [],
envs: [],
Expand Down
Loading
Loading