import {obmap} from "../../tooling/obmap.js" import {ConfigError} from "../../errors/basic.js" import {Command, CommandOptions} from "./types/commands.js" import {InvalidFlagError} from "../../errors/kinds/config.js" import {Arg, CoerceFn, Param, Type, Opts, Args, Params, ValidateFn} from "./types/units.js" import * as tn from "../../tooling/text/tn.js" import * as fmt from "../../tooling/text/formatting.js" export function command< A extends Args, P extends Params, >(o: CommandOptions): Command { return new Command(o) } export const asType = >(type: T) => type export function asTypes>>(coersions: C) { return obmap(coersions, (coerce, name) => ({name, coerce}) ) as {[K in keyof C]: Type>} } export const type = asTypes({ string: string => string, number: string => { const number = Number(string) if (isNaN(number)) throw new Error(`not a number`) return number }, integer: string => { const n = Number(string) if (isNaN(n)) throw new Error(`not a number`) if (!Number.isSafeInteger(n)) throw new Error(`not a safe integer`) return n }, boolean: (() => { const pairs = [ ["1", "0"], ["on", "off"], ["yes", "no"], ["true", "false"], ] const truisms = pairs.map(p => p[0]) const falsisms = pairs.map(p => p[1]) return string => { string = string.toLowerCase() if (truisms.includes(string)) return true else if (falsisms.includes(string)) return false else throw new Error(`invalid boolean, try "true" or "false"`) } })(), }) const {string, number, integer, boolean} = type export {string, number, integer, boolean} type Ingestion = { coerce: CoerceFn validate: ValidateFn } export const ingestors = { default: ({validate, coerce}: Ingestion, fallback: string) => (value: string | undefined) => validate(coerce(value ?? fallback)), required: ({validate, coerce}: Ingestion) => (value: string | undefined) => { if (value === undefined) throw new Error(`required but not provided`) return validate(coerce(value)) }, optional: ({validate, coerce}: Ingestion) => (value: string | undefined) => (value === undefined) ? undefined : validate(coerce(value)), } export const param = { flag(flag: string, o: {help?: string} = {}): Param { flag = flag.startsWith("-") ? flag.slice(1) : flag if (flag.length !== 1) throw new InvalidFlagError(flag) const {name, coerce} = type.boolean return { mode: "flag", type: name, flag, help: o.help, ingest: string => { return (string === undefined) ? false : coerce(string) }, } }, default: ( {name: type, coerce}: Type, fallback: string, {help, validate = x => x}: Opts = {}, ): Param => ({ help, type, fallback, mode: "default", ingest: ingestors.default({coerce, validate}, fallback), }), required: ( {name: type, coerce}: Type, {help, validate = x => x}: Opts = {}, ): Param => ({ help, type, mode: "required", ingest: ingestors.required({coerce, validate}), }), optional: ( {name: type, coerce}: Type, {help, validate = x => x}: Opts = {}, ): Param => ({ help, type, mode: "optional", ingest: ingestors.optional({coerce, validate}), }), } export const arg = (name: N) => ({ default: ( {name: type, coerce}: Type, fallback: string, {help, validate = x => x}: Opts = {}, ): Arg => ({ name, help, type, fallback, mode: "default", ingest: ingestors.default({coerce, validate}, fallback), }), required: ( {name: type, coerce}: Type, {help, validate = x => x}: Opts = {}, ): Arg => ({ name, help, type, mode: "required", ingest: ingestors.required({coerce, validate}), }), optional: ( {name: type, coerce}: Type, {help, validate = x => x}: Opts = {}, ): Arg => ({ name, help, type, mode: "optional", ingest: ingestors.optional({coerce, validate}), }), }) export function choice(allowable: T[], {help}: {help?: string} = {}): Opts { let message: string if (allowable.length === 0) throw new ConfigError(`zero choices doesn't make sense`) else if (allowable.length === 1) message = `can be "${allowable[0]}"` else message = `choose one: ${allowable.map(c => c).join(", ")}` return { help: tn.str(tn.connect("\n", [ help ? fmt.normalize(help) : null, message, ])), validate: item => { if (!allowable.includes(item)) throw new Error(`invalid choice`) return item }, } } export function multipleChoice( allowable: T[], {zeroAllowed = false, help}: {zeroAllowed?: boolean, help?: string} = {}, ): Opts { let message: string if (allowable.length === 0) throw new ConfigError(`zero choices doesn't make sense`) else if (allowable.length === 1) message = `can be "${allowable[0]}"` else message = zeroAllowed ? `choose zero or more: ${allowable.map(c => c).join(", ")}.` : `choose one or more: ${allowable.map(c => c).join(", ")}.` return { help: tn.str(tn.connect("\n", [ help ? fmt.normalize(help) : null, message, ])), validate: list => { if (!zeroAllowed && list.length === 0) throw new Error(`must choose at least one`) for (const item of list) if (!allowable.includes(item)) throw new Error(`invalid choice`) return list }, } } export function list( {name: type, coerce}: Type, delimiter = ",", ): Type { return { name: `${type}-list`, coerce: string => string .split(delimiter) .map(s => s.trim()) .map(coerce), } }