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
18 changes: 9 additions & 9 deletions lib/argument.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Param, param, type ParamModel } from "./param.ts";
import { type AnyParam, type Param, param, type ParamModel } from "./param.ts";
import {
brand,
type Check,
Expand All @@ -15,14 +15,14 @@ export function argument<
const E extends readonly Unary[],
>(
named: Definition<N>,
...elements: E & Check<Param<N, unknown>, E>
): ElementOf<N, Fold<Param<N, unknown>, E>> {
...elements: E & Check<Zero<N>, E>
): ElementOf<N, Fold<Zero<N>, E>> {
const added = elements.reduce<unknown>(
(value, element) => element(value as never),
param(named, positional),
) as Param<string, unknown>;
) as AnyParam;

return brand<ElementOf<N, Fold<Param<N, unknown>, E>>>(
return brand<ElementOf<N, Fold<Zero<N>, E>>>(
(route: AnyRoute) => {
let phases = [...route.phases];
let phase = phases.pop()!;
Expand Down Expand Up @@ -51,13 +51,13 @@ export function argument<
);
}

type ValueOf<P> = P extends Param<string, infer T> ? T : never;
type Zero<N extends string> = Param<N, unknown, "one">;

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

function positional<P extends Param<string, unknown>>(param: P): P {
function positional<P extends AnyParam>(param: P): P {
return {
...param,
cli: {
Expand Down
127 changes: 98 additions & 29 deletions lib/bind.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Maybe } from "./maybe.ts";
import type { Param } from "./param.ts";
import { Just, type Maybe } from "./maybe.ts";
import type { AnyParam, Param } from "./param.ts";
import type { CLIRead, Symbol } from "./read.ts";
import type { Rest } from "./rest.ts";
import type { Result } from "./result.ts";
import type { Word } from "./tokenize.ts";
import type { TokenInput, Tokenizer, TokenRange } from "./tokenizer.ts";
import type { AnyPhase, Issue, Path } from "./types.ts";
import type { AnyPhase, Issue, OutputOf, Path } from "./types.ts";

export interface Binding<T> {
readonly rest: Rest;
Expand All @@ -24,20 +24,20 @@ export interface PhaseSegment {
readonly path: Path;
}

export function fromCLI<const K extends string, T>(options: {
readonly param: Param<K, T>;
export function fromCLI<P extends Param<string, unknown, "one">>(options: {
readonly param: P;
readonly view: TokenInput<Symbol>;
readonly rest: Rest;
}): Maybe<Binding<T>> {
}): Maybe<Binding<OutputOf<P["schema"]>>> {
let { param, view, rest } = options;
return fromRead(param, param.cli.read(view), rest);
}

export function fromValues<const K extends string, T>(options: {
readonly param: Param<K, T>;
export function fromValues<P extends AnyParam>(options: {
readonly param: P;
readonly route: Path;
readonly rest: Rest;
}): Maybe<Binding<T>> {
}): Maybe<Binding<OutputOf<P["schema"]>>> {
let { param, route, rest } = options;
let claim = rest.values.claim({
route,
Expand All @@ -60,11 +60,11 @@ export function fromValues<const K extends string, T>(options: {
};
}

export function fromEnv<const K extends string, T>(options: {
readonly param: Param<K, T>;
export function fromEnv<P extends AnyParam>(options: {
readonly param: P;
readonly route: Path;
readonly rest: Rest;
}): Maybe<Binding<T>> {
}): Maybe<Binding<OutputOf<P["schema"]>>> {
let { param, route, rest } = options;
let claim = rest.envs.claim({
route,
Expand All @@ -78,14 +78,18 @@ export function fromEnv<const K extends string, T>(options: {

let value = claim.result.value.value;

let candidates = param.cardinality === "one"
? param.decode(value)
: param.decode([value]);

return {
exists: true,
value: {
rest: {
...rest,
envs: claim.rest,
},
result: decode(param, value, param.decode(value), [param.name]),
result: decode(param, value, candidates, [param.name]),
},
};
}
Expand All @@ -97,12 +101,12 @@ export function bindPhase(options: {
}): PhaseBinding {
let { phase, segment } = options;
let rest = options.rest;
let params = Object.values(phase.model.params) as Param<string, unknown>[];
let params = Object.values(phase.model.params) as AnyParam[];
let pending = new Map(params.map((param) => [param.name, param]));
let results = new Map<string, Result<unknown>>();

function settle(
param: Param<string, unknown>,
param: AnyParam,
binding: Binding<unknown>,
): void {
rest = binding.rest;
Expand All @@ -111,7 +115,7 @@ export function bindPhase(options: {
}

function accept(
param: Param<string, unknown>,
param: AnyParam,
attempt: Maybe<Binding<unknown>>,
): void {
if (!attempt.exists) {
Expand All @@ -121,14 +125,16 @@ export function bindPhase(options: {
settle(param, attempt.value);
}

let captures = new Map<string, Capture>();

// Every pending reader proposes a claim against the same immutable view.
// Commit the proposal beginning earliest in argv, then recompute the view.
// This lets `--port 9000` outrank a positional claim on `9000` without
// attaching binding-policy metadata to either reader.
while (true) {
let horizon = first(rest.tokens, segment.range);
let offer: {
param: Param<string, unknown>;
param: AnyParam;
read: CLIRead;
index: number;
} | undefined;
Expand All @@ -154,9 +160,58 @@ export function bindPhase(options: {
break;
}

if (offer.param.cardinality === "many") {
rest = {
...rest,
tokens: offer.read.claim.rest,
};

let current = captures.get(offer.param.name) ?? {
param: offer.param,
values: [],
issues: [],
failed: false,
};

current.issues.push(...offer.read.result.issues ?? []);

if (offer.read.result.ok && offer.read.result.value.exists) {
current.values.push(offer.read.result.value.value);
} else {
current.failed = true;
}
captures.set(offer.param.name, current);

continue;
}

accept(offer.param, fromRead(offer.param, offer.read, rest));
}

// deal wil all multiple
for (let capture of captures.values()) {
if (capture.failed) {
settle(capture.param, {
rest,
result: { ok: false, issues: capture.issues },
});
} else {
let values = capture.values;

let candidates: Maybe<unknown[]>[] = values.every(
(value): value is string => typeof value === "string",
)
? capture.param.decode(values)
: [Just(values)];

let result = merge(
decode(capture.param, values, candidates, [capture.param.name]),
capture.issues,
);
settle(capture.param, { result, rest });
}
}

// Address sources have stable visibility once the route is known. They are
// tried only after CLI has reached its fixed point so a provisional CLI miss
// cannot let a lower-priority source settle the parameter too early.
Expand Down Expand Up @@ -210,11 +265,11 @@ export function bindPhase(options: {
return { rest, model, issues, valid };
}

function fromRead<T>(
param: Param<string, T>,
function fromRead<P extends Param<string, unknown, "one">>(
param: P,
read: CLIRead,
rest: Rest,
): Maybe<Binding<T>> {
): Maybe<Binding<OutputOf<P["schema"]>>> {
let path = [param.name];

if (!read.result.ok) {
Expand All @@ -235,7 +290,10 @@ function fromRead<T>(
}

let value = read.result.value.value;
let candidates = typeof value === "string" ? param.decode(value) : [value];

let candidates = typeof value === "string"
? param.decode(value)
: [Just(value)];
let result = merge(
decode(param, value, candidates, path),
read.result.issues,
Expand All @@ -253,12 +311,12 @@ function fromRead<T>(
};
}

function decode<T>(
param: Param<string, T>,
function decode<P extends AnyParam>(
param: P,
value: unknown,
candidates: unknown[],
candidates: Maybe<unknown>[],
path: string[],
): Result<T> {
): Result<OutputOf<P["schema"]>> {
if (candidates.length === 0) {
return {
ok: false,
Expand All @@ -271,7 +329,11 @@ function decode<T>(

let issues: readonly Issue[] | undefined;

for (let candidate of candidates) {
let values = candidates.flatMap((candidate) =>
candidate.exists ? [candidate.value] : []
);

for (let candidate of values) {
let result = validate(param, candidate, path);
if (result.ok) {
return result;
Expand All @@ -285,11 +347,11 @@ function decode<T>(
};
}

function validate<T>(
param: Param<string, T>,
function validate<P extends AnyParam>(
param: P,
value: unknown,
path: string[],
): Result<T> {
): Result<OutputOf<P["schema"]>> {
let validated = param.schema["~standard"].validate(value);
if (validated instanceof Promise) {
return {
Expand Down Expand Up @@ -350,3 +412,10 @@ function earliest(tokens: readonly Symbol[]): number {

return first;
}

interface Capture {
param: Param<string, unknown, "many">;
values: (string | boolean)[];
issues: Issue[];
failed: boolean;
}
49 changes: 39 additions & 10 deletions lib/decode.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,55 @@
export type Decoder = (value: string) => unknown[];
import { Just, type Maybe, Nothing } from "./maybe.ts";

export const number: Decoder = (value) => {
export type Decoder<A, B> = (value: A) => Maybe<B>[];

export const number: Decoder<string, number> = (value) => {
if (!numeric.test(value)) {
return [];
return [Nothing()];
}

let decoded = Number(value);
return Number.isFinite(decoded) ? [decoded] : [];
return Number.isFinite(decoded) ? [Just(decoded)] : [Nothing()];
};

export const scalar: Decoder = (value) => {
return [...number(value), value];
export const scalar: Decoder<string, string | number> = (value) => {
return [...number(value), Just(value)];
};

export const boolean: Decoder = (value) => {
export const boolean: Decoder<string, boolean> = (value) => {
if (value === "true") {
return [true];
return [Just(true)];
}
if (value === "false") {
return [false];
return [Just(false)];
}
return [];
return [Nothing()];
};

export function multiple<A, B>(decoder: Decoder<A, B>): Decoder<A[], B[]> {
return (input: A[]): Maybe<B[]>[] => {
let decodings = input.map(decoder);

let result: Maybe<B[]>[] = [];

for (let i = 0; i < decodings.length; i++) {
let decoding = decodings[i];
for (let j = 0; j < decoding.length; j++) {
let row: Maybe<B[]> | undefined = result[j];
if (!row) {
row = result[j] = Just([]);
}
if (row.exists) {
let candidate = decoding[j];
if (candidate.exists) {
row.value[i] = candidate.value;
} else {
result[j] = Nothing();
}
}
}
}
return result;
};
}

const numeric = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
8 changes: 4 additions & 4 deletions lib/env.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Maybe } from "./maybe.ts";
import type { Param } from "./param.ts";
import type { AnyParam } from "./param.ts";
import { brand, type IdentityElement } from "./pipeline.ts";
import type { AnyRoute, RoutePath } from "./types.ts";

Expand Down Expand Up @@ -28,9 +28,9 @@ export interface EnvClaimOptions {

export function env(
key: string,
): IdentityElement<Param<string, unknown>> {
return brand<IdentityElement<Param<string, unknown>>>(
(param: Param<string, unknown>) => ({ ...param, env: key }),
): IdentityElement<AnyParam> {
return brand<IdentityElement<AnyParam>>(
(param: AnyParam) => ({ ...param, env: key }),
);
}

Expand Down
Loading
Loading