import type { AnyStep, Field, Input, Parser, Step } from "./types.ts"; import { matchAll, parseArgument, parseLongOption, parseLongOptionEql, parseNegativeSwitch, parseSwitch, primitive, } from "./parse-args.ts"; import { field } from "./field.ts"; import { optionalBoolean } from "./schema.ts"; export type Attrs = & { [K in keyof T]: Parser<[Step]>; } & {}; export type ObjectValue>>> = & { [K in keyof T]: T[K] extends Parser<[Step]> ? V : boolean | undefined; } & {}; export type ObjectData> = & { [K in keyof A]: A[K] extends Parser<[Step]> ? TData : never; } & {}; export interface ObjectParser extends Parser<[Step>>]> { attrs: Attrs; } export function object>>>( attrs: T, ): ObjectParser> { type V = ObjectValue; let resolved = Object.fromEntries( Object.entries(attrs).map(([key, entry]) => { let parser = typeof entry.parse === "function" ? entry as Parser<[AnyStep]> : { ...field(optionalBoolean), ...entry }; return [key, parser]; }), ) as Attrs; let entries = Object.entries(resolved) as [ keyof V, Parser<[Step]>, ][]; let parsers = entries.map(([key, parser]) => { return [key, { ...parser, path: [String(key), ...parser.path], }] as [keyof V, Parser<[Step]>]; }); return { attrs: resolved, path: [], parse(input) { let value = {} as Record; let data = {} as ObjectData>; let errors: { path: string[]; error: Error }[] = []; // extract CLI args into per-key values let cliValues = new Map(); let args = input.args ?? []; if (args.length > 0) { let matched = new Set(); let prev: string[] | undefined; while ( args.length > 0 && (prev === undefined || args.length < prev.length) ) { prev = args; for (let [key, parser] of parsers) { let f = asField(parser); if (!f) continue; if (matched.has(key) && !f.mods.array) continue; let matcher = matchAll( parseSwitch(f.schema, f.aliases ?? []), parseNegativeSwitch(f.schema), parseLongOption(f.aliases ?? []), parseLongOptionEql(), parseArgument(f.mods.argument), ); let match = matcher(args, parser.path); if (match.matched) { let val = primitive(match.value); if (f.mods.array) { let existing = cliValues.get(key); if (existing && existing.length > 0) { let last = existing[existing.length - 1]; (last.value as unknown[]).push(val); } else { cliValues.set(key, [{ name: "cli", value: [val] }]); } } else { let existing = cliValues.get(key) ?? []; existing.push({ name: "cli", value: val }); cliValues.set(key, existing); } matched.add(key); args = match.remainder; break; } } } } for (let [key, parser] of parsers) { // scope values to this key let scoped: Input = { values: [ ...(input.values ?? []).flatMap((v) => { if (v.value == null) return []; let obj = v.value as Record; if (!(String(key) in obj)) return []; return [{ name: v.name, value: obj[String(key)] }]; }), ...(cliValues.get(key) ?? []), ], envs: input.envs, }; let parsed = parser.parse(scoped); if (parsed.ok) { value[key] = parsed.value; data[key] = parsed.data as ObjectData>[keyof V]; } else { errors.push({ path: parser.path, error: parsed.error }); } } let remainder: Input = { ...input, args }; if (errors.length > 0) { return { ok: false as const, error: new ObjectValidationError(errors), remainder, }; } return { ok: true as const, value: value as V, data, remainder, }; }, }; } function asField(parser: Parser): Field | undefined { return "mods" in parser ? parser as Field : undefined; } export class ObjectValidationError extends Error { constructor(public fields: { path: string[]; error: Error }[]) { let message = fields.map(({ path, error }) => { return `${path.join(".")}: ${error.message}`; }).join("\n"); super(message); this.name = "ObjectValidationError"; } }