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),
}
}