Skip to content

Commit 367f7c6

Browse files
committed
✨ Support repeated option decoding
1 parent 781fc48 commit 367f7c6

15 files changed

Lines changed: 285 additions & 111 deletions

‎lib/argument.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Param, param, type ParamModel } from "./param.ts";
1+
import { type AnyParam, type Param, param, type ParamModel } from "./param.ts";
22
import {
33
brand,
44
type Check,
@@ -15,14 +15,14 @@ export function argument<
1515
const E extends readonly Unary[],
1616
>(
1717
named: Definition<N>,
18-
...elements: E & Check<Param<N, unknown>, E>
19-
): ElementOf<N, Fold<Param<N, unknown>, E>> {
18+
...elements: E & Check<Zero<N>, E>
19+
): ElementOf<N, Fold<Zero<N>, E>> {
2020
const added = elements.reduce<unknown>(
2121
(value, element) => element(value as never),
2222
param(named, positional),
23-
) as Param<string, unknown>;
23+
) as AnyParam;
2424

25-
return brand<ElementOf<N, Fold<Param<N, unknown>, E>>>(
25+
return brand<ElementOf<N, Fold<Zero<N>, E>>>(
2626
(route: AnyRoute) => {
2727
let phases = [...route.phases];
2828
let phase = phases.pop()!;
@@ -51,13 +51,13 @@ export function argument<
5151
);
5252
}
5353

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

56-
type ElementOf<N extends string, P> = P extends Param<N, unknown>
57-
? ModelElement<ParamModel<N, ValueOf<P>>>
56+
type ElementOf<N extends string, P> = P extends
57+
Param<N, infer Model, infer _Cardinality> ? ModelElement<ParamModel<N, Model>>
5858
: never;
5959

60-
function positional<P extends Param<string, unknown>>(param: P): P {
60+
function positional<P extends AnyParam>(param: P): P {
6161
return {
6262
...param,
6363
cli: {

‎lib/bind.ts‎

Lines changed: 98 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { Maybe } from "./maybe.ts";
2-
import type { Param } from "./param.ts";
1+
import { Just, type Maybe } from "./maybe.ts";
2+
import type { AnyParam, Param } from "./param.ts";
33
import type { CLIRead, Symbol } from "./read.ts";
44
import type { Rest } from "./rest.ts";
55
import type { Result } from "./result.ts";
66
import type { Word } from "./tokenize.ts";
77
import type { TokenInput, Tokenizer, TokenRange } from "./tokenizer.ts";
8-
import type { AnyPhase, Issue, Path } from "./types.ts";
8+
import type { AnyPhase, Issue, OutputOf, Path } from "./types.ts";
99

1010
export interface Binding<T> {
1111
readonly rest: Rest;
@@ -24,20 +24,20 @@ export interface PhaseSegment {
2424
readonly path: Path;
2525
}
2626

27-
export function fromCLI<const K extends string, T>(options: {
28-
readonly param: Param<K, T>;
27+
export function fromCLI<P extends Param<string, unknown, "one">>(options: {
28+
readonly param: P;
2929
readonly view: TokenInput<Symbol>;
3030
readonly rest: Rest;
31-
}): Maybe<Binding<T>> {
31+
}): Maybe<Binding<OutputOf<P["schema"]>>> {
3232
let { param, view, rest } = options;
3333
return fromRead(param, param.cli.read(view), rest);
3434
}
3535

36-
export function fromValues<const K extends string, T>(options: {
37-
readonly param: Param<K, T>;
36+
export function fromValues<P extends AnyParam>(options: {
37+
readonly param: P;
3838
readonly route: Path;
3939
readonly rest: Rest;
40-
}): Maybe<Binding<T>> {
40+
}): Maybe<Binding<OutputOf<P["schema"]>>> {
4141
let { param, route, rest } = options;
4242
let claim = rest.values.claim({
4343
route,
@@ -60,11 +60,11 @@ export function fromValues<const K extends string, T>(options: {
6060
};
6161
}
6262

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

7979
let value = claim.result.value.value;
8080

81+
let candidates = param.cardinality === "one"
82+
? param.decode(value)
83+
: param.decode([value]);
84+
8185
return {
8286
exists: true,
8387
value: {
8488
rest: {
8589
...rest,
8690
envs: claim.rest,
8791
},
88-
result: decode(param, value, param.decode(value), [param.name]),
92+
result: decode(param, value, candidates, [param.name]),
8993
},
9094
};
9195
}
@@ -97,12 +101,12 @@ export function bindPhase(options: {
97101
}): PhaseBinding {
98102
let { phase, segment } = options;
99103
let rest = options.rest;
100-
let params = Object.values(phase.model.params) as Param<string, unknown>[];
104+
let params = Object.values(phase.model.params) as AnyParam[];
101105
let pending = new Map(params.map((param) => [param.name, param]));
102106
let results = new Map<string, Result<unknown>>();
103107

104108
function settle(
105-
param: Param<string, unknown>,
109+
param: AnyParam,
106110
binding: Binding<unknown>,
107111
): void {
108112
rest = binding.rest;
@@ -111,7 +115,7 @@ export function bindPhase(options: {
111115
}
112116

113117
function accept(
114-
param: Param<string, unknown>,
118+
param: AnyParam,
115119
attempt: Maybe<Binding<unknown>>,
116120
): void {
117121
if (!attempt.exists) {
@@ -121,14 +125,16 @@ export function bindPhase(options: {
121125
settle(param, attempt.value);
122126
}
123127

128+
let captures = new Map<string, Capture>();
129+
124130
// Every pending reader proposes a claim against the same immutable view.
125131
// Commit the proposal beginning earliest in argv, then recompute the view.
126132
// This lets `--port 9000` outrank a positional claim on `9000` without
127133
// attaching binding-policy metadata to either reader.
128134
while (true) {
129135
let horizon = first(rest.tokens, segment.range);
130136
let offer: {
131-
param: Param<string, unknown>;
137+
param: AnyParam;
132138
read: CLIRead;
133139
index: number;
134140
} | undefined;
@@ -154,9 +160,58 @@ export function bindPhase(options: {
154160
break;
155161
}
156162

163+
if (offer.param.cardinality === "many") {
164+
rest = {
165+
...rest,
166+
tokens: offer.read.claim.rest,
167+
};
168+
169+
let current = captures.get(offer.param.name) ?? {
170+
param: offer.param,
171+
values: [],
172+
issues: [],
173+
failed: false,
174+
};
175+
176+
current.issues.push(...offer.read.result.issues ?? []);
177+
178+
if (offer.read.result.ok && offer.read.result.value.exists) {
179+
current.values.push(offer.read.result.value.value);
180+
} else {
181+
current.failed = true;
182+
}
183+
captures.set(offer.param.name, current);
184+
185+
continue;
186+
}
187+
157188
accept(offer.param, fromRead(offer.param, offer.read, rest));
158189
}
159190

191+
// deal wil all multiple
192+
for (let capture of captures.values()) {
193+
if (capture.failed) {
194+
settle(capture.param, {
195+
rest,
196+
result: { ok: false, issues: capture.issues },
197+
});
198+
} else {
199+
let values = capture.values;
200+
201+
let candidates: Maybe<unknown[]>[] = values.every(
202+
(value): value is string => typeof value === "string",
203+
)
204+
? capture.param.decode(values)
205+
: [Just(values)];
206+
207+
let result = merge(
208+
decode(capture.param, values, candidates, [capture.param.name]),
209+
capture.issues,
210+
);
211+
settle(capture.param, { result, rest });
212+
}
213+
}
214+
160215
// Address sources have stable visibility once the route is known. They are
161216
// tried only after CLI has reached its fixed point so a provisional CLI miss
162217
// cannot let a lower-priority source settle the parameter too early.
@@ -210,11 +265,11 @@ export function bindPhase(options: {
210265
return { rest, model, issues, valid };
211266
}
212267

213-
function fromRead<T>(
214-
param: Param<string, T>,
268+
function fromRead<P extends Param<string, unknown, "one">>(
269+
param: P,
215270
read: CLIRead,
216271
rest: Rest,
217-
): Maybe<Binding<T>> {
272+
): Maybe<Binding<OutputOf<P["schema"]>>> {
218273
let path = [param.name];
219274

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

237292
let value = read.result.value.value;
238-
let candidates = typeof value === "string" ? param.decode(value) : [value];
293+
294+
let candidates = typeof value === "string"
295+
? param.decode(value)
296+
: [Just(value)];
239297
let result = merge(
240298
decode(param, value, candidates, path),
241299
read.result.issues,
@@ -253,12 +311,12 @@ function fromRead<T>(
253311
};
254312
}
255313

256-
function decode<T>(
257-
param: Param<string, T>,
314+
function decode<P extends AnyParam>(
315+
param: P,
258316
value: unknown,
259-
candidates: unknown[],
317+
candidates: Maybe<unknown>[],
260318
path: string[],
261-
): Result<T> {
319+
): Result<OutputOf<P["schema"]>> {
262320
if (candidates.length === 0) {
263321
return {
264322
ok: false,
@@ -271,7 +329,11 @@ function decode<T>(
271329

272330
let issues: readonly Issue[] | undefined;
273331

274-
for (let candidate of candidates) {
332+
let values = candidates.flatMap((candidate) =>
333+
candidate.exists ? [candidate.value] : []
334+
);
335+
336+
for (let candidate of values) {
275337
let result = validate(param, candidate, path);
276338
if (result.ok) {
277339
return result;
@@ -285,11 +347,11 @@ function decode<T>(
285347
};
286348
}
287349

288-
function validate<T>(
289-
param: Param<string, T>,
350+
function validate<P extends AnyParam>(
351+
param: P,
290352
value: unknown,
291353
path: string[],
292-
): Result<T> {
354+
): Result<OutputOf<P["schema"]>> {
293355
let validated = param.schema["~standard"].validate(value);
294356
if (validated instanceof Promise) {
295357
return {
@@ -350,3 +412,10 @@ function earliest(tokens: readonly Symbol[]): number {
350412

351413
return first;
352414
}
415+
416+
interface Capture {
417+
param: Param<string, unknown, "many">;
418+
values: (string | boolean)[];
419+
issues: Issue[];
420+
failed: boolean;
421+
}

‎lib/decode.ts‎

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,55 @@
1-
export type Decoder = (value: string) => unknown[];
1+
import { Just, type Maybe, Nothing } from "./maybe.ts";
22

3-
export const number: Decoder = (value) => {
3+
export type Decoder<A, B> = (value: A) => Maybe<B>[];
4+
5+
export const number: Decoder<string, number> = (value) => {
46
if (!numeric.test(value)) {
5-
return [];
7+
return [Nothing()];
68
}
79

810
let decoded = Number(value);
9-
return Number.isFinite(decoded) ? [decoded] : [];
11+
return Number.isFinite(decoded) ? [Just(decoded)] : [Nothing()];
1012
};
1113

12-
export const scalar: Decoder = (value) => {
13-
return [...number(value), value];
14+
export const scalar: Decoder<string, string | number> = (value) => {
15+
return [...number(value), Just(value)];
1416
};
1517

16-
export const boolean: Decoder = (value) => {
18+
export const boolean: Decoder<string, boolean> = (value) => {
1719
if (value === "true") {
18-
return [true];
20+
return [Just(true)];
1921
}
2022
if (value === "false") {
21-
return [false];
23+
return [Just(false)];
2224
}
23-
return [];
25+
return [Nothing()];
2426
};
2527

28+
export function multiple<A, B>(decoder: Decoder<A, B>): Decoder<A[], B[]> {
29+
return (input: A[]): Maybe<B[]>[] => {
30+
let decodings = input.map(decoder);
31+
32+
let result: Maybe<B[]>[] = [];
33+
34+
for (let i = 0; i < decodings.length; i++) {
35+
let decoding = decodings[i];
36+
for (let j = 0; j < decoding.length; j++) {
37+
let row: Maybe<B[]> | undefined = result[j];
38+
if (!row) {
39+
row = result[j] = Just([]);
40+
}
41+
if (row.exists) {
42+
let candidate = decoding[j];
43+
if (candidate.exists) {
44+
row.value[i] = candidate.value;
45+
} else {
46+
result[j] = Nothing();
47+
}
48+
}
49+
}
50+
}
51+
return result;
52+
};
53+
}
54+
2655
const numeric = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/;

‎lib/env.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Maybe } from "./maybe.ts";
2-
import type { Param } from "./param.ts";
2+
import type { AnyParam } from "./param.ts";
33
import { brand, type IdentityElement } from "./pipeline.ts";
44
import type { AnyRoute, RoutePath } from "./types.ts";
55

@@ -28,9 +28,9 @@ export interface EnvClaimOptions {
2828

2929
export function env(
3030
key: string,
31-
): IdentityElement<Param<string, unknown>> {
32-
return brand<IdentityElement<Param<string, unknown>>>(
33-
(param: Param<string, unknown>) => ({ ...param, env: key }),
31+
): IdentityElement<AnyParam> {
32+
return brand<IdentityElement<AnyParam>>(
33+
(param: AnyParam) => ({ ...param, env: key }),
3434
);
3535
}
3636

0 commit comments

Comments
 (0)