Skip to content

Commit

Permalink
feat: pass main instance to run fn
Browse files Browse the repository at this point in the history
  • Loading branch information
chenasraf committed Nov 24, 2023
1 parent 73c1ad9 commit 29d6973
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 59 deletions.
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"semi": false,
"printWidth": 120,
"printWidth": 100,
"singleQuote": false
}
110 changes: 71 additions & 39 deletions src/command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { z } from "zod"
import { ValidationError } from "./error"
import MassargOption, { MassargFlag, MassargNumber, OptionConfig, TypedOptionConfig } from "./option"
import { generateCommandsHelpTable, generateOptionsHelpTable, isZodError } from "./utils"
import Massarg from "./massarg"
import MassargOption, { TypedOptionConfig } from "./option"
import { generateCommandsHelpTable, generateOptionsHelpTable, isZodError, setOrPush } from "./utils"

export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
z.object({
Expand All @@ -10,8 +11,10 @@ export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
aliases: z.string().array().optional(),
run: z
.function()
.args(args)
.returns(z.union([z.promise(z.void()), z.void()])) as z.ZodType<(args: z.infer<RunArgs>) => Promise<void> | void>,
.args(args, z.any())
.returns(z.union([z.promise(z.void()), z.void()])) as z.ZodType<
(args: z.infer<RunArgs>, instance: MassargCommand<z.infer<RunArgs>>) => Promise<void> | void
>,
})

export type CommandConfig<T = unknown> = z.infer<ReturnType<typeof CommandConfig<z.ZodType<T>>>>
Expand All @@ -24,7 +27,10 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
name: string
description: string
aliases: string[]
private _run?: (options: Args) => Promise<void> | void
private _run?: <P extends ArgsObject = Args>(
options: Args,
instance: Massarg<P>,
) => Promise<void> | void
options: MassargOption[] = []
commands: MassargCommand<any>[] = []
args: Partial<Args> = {}
Expand All @@ -34,20 +40,22 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
this.name = options.name
this.description = options.description
this.aliases = options.aliases ?? []
this._run = options.run
this._run = options.run as typeof this._run
}

command<A extends ArgsObject = Args>(config: CommandConfig<A>): MassargCommand<Args>
command<A extends ArgsObject = Args>(config: MassargCommand<A>): MassargCommand<Args>
command<A extends ArgsObject = Args>(config: CommandConfig<A> | MassargCommand<A>): MassargCommand<Args> {
command<A extends ArgsObject = Args>(
config: CommandConfig<A> | MassargCommand<A>,
): MassargCommand<Args> {
try {
const command = config instanceof MassargCommand ? config : new MassargCommand(config)
this.commands.push(command)
return this
} catch (e) {
if (isZodError(e)) {
throw new ValidationError({
path: [config.name, ...e.issues[0].path.map((p) => p.toString())],
path: [this.name, config.name, ...e.issues[0].path.map((p) => p.toString())],
code: e.issues[0].code,
message: e.issues[0].message,
})
Expand All @@ -60,13 +68,14 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
option<T = string>(config: TypedOptionConfig<T>): MassargCommand<Args>
option<T = string>(config: TypedOptionConfig<T> | MassargOption<T>): MassargCommand<Args> {
try {
const option = config instanceof MassargOption ? config : MassargOption.fromTypedConfig(config)
const option =
config instanceof MassargOption ? config : MassargOption.fromTypedConfig(config)
this.options.push(option as MassargOption)
return this
} catch (e) {
if (isZodError(e)) {
throw new ValidationError({
path: [config.name, ...e.issues[0].path.map((p) => p.toString())],
path: [this.name, config.name, ...e.issues[0].path.map((p) => p.toString())],
code: e.issues[0].code,
message: e.issues[0].message,
})
Expand All @@ -75,65 +84,84 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
}
}

main(run: (options: Args) => Promise<void> | void): MassargCommand<Args> {
this._run = run
main<A extends ArgsObject = Args>(
run: (options: Args, instance: MassargCommand<A>) => Promise<void> | void,
): MassargCommand<Args> {
this._run = run as typeof this._run
return this
}

parse(argv: string[], args?: Partial<Args>): Promise<void> | void {
console.log("parse:", this.name)
console.log(argv)
parse(argv: string[], args?: Partial<Args>, parent?: MassargCommand<Args>): Promise<void> | void {
this.args ??= {}
this.args = { ...this.args, ...args }
let _argv = [...argv]
while (_argv.length) {
const arg = _argv.shift()!
console.log("parsing:", arg, _argv)
const found = this.options.some((o) => o._isOption(arg))
if (found) {
console.log("option:", arg, _argv)
_argv = this.parseOption(arg, _argv)
continue
}

const command = this.commands.find((c) => c.name === arg || c.aliases.includes(arg))
if (command) {
console.log("command:", arg, _argv)
return command.parse(_argv, this.args)
return command.parse(_argv, this.args, parent ?? this)
}
// TODO pass all un-handled args to an "args" option
console.log("Nothing to do", arg, _argv)
}
if (this._run) {
console.log("run:", this.args)
this._run({ ...args, ...this.args } as Args)
this._run({ ...args, ...this.args } as Args, parent ?? this)
}
}

private parseOption(arg: string, argv: string[]): string[] {
private parseOption(arg: string, argv: string[]) {
const option = this.options.find((o) => o._match(arg))

if (!option) {
// TODO create custom error object
throw new Error(`Unknown option ${arg}`)
}
const res = option.valueFromArgv([arg, ...argv])
console.log("option class name", option.constructor.name)
if (option.isArray) {
this.args[res.key as keyof Args] ??= [] as Args[keyof Args]
const _a = this.args[res.key as keyof Args] as unknown[]
_a.push(res.value) as Args[keyof Args]
} else {
this.args[res.key as keyof Args] = res.value as Args[keyof Args]
throw new ValidationError({
path: [arg],
code: "unknown_option",
message: "Unknown option",
})
}
console.log("option response:", { value: res.value, argv: res.argv })
const res = option._parseDetails([arg, ...argv])
this.args[res.key as keyof Args] = setOrPush<Args[keyof Args]>(
res.value,
this.args[res.key as keyof Args],
option.isArray,
)
return res.argv
}

getArgs(argv: string[]): Args {
console.log("getArgs:", this.name)
console.log(argv)
return {} as Args
let args: Args = {} as Args
let _argv = [...argv]
while (_argv.length) {
const arg = _argv.shift()!
const found = this.options.some((o) => o._isOption(arg))
if (found) {
const option = this.options.find((o) => o._match(arg))
if (!option) {
throw new ValidationError({
path: [arg],
code: "unknown_option",
message: "Unknown option",
})
}
const res = option._parseDetails(argv)
args[res.key as keyof Args] = setOrPush<Args[keyof Args]>(
res.value,
args[res.key as keyof Args],
option.isArray,
)
continue
}

const command = this.commands.find((c) => c.name === arg || c.aliases.includes(arg))
if (command) {
break
}
}
return args
}

helpString(): string {
Expand All @@ -151,6 +179,10 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
.filter((s) => typeof s === "string")
.join("\n")
}

printHelp(): void {
console.log(this.helpString())
}
}

export { MassargCommand }
19 changes: 17 additions & 2 deletions src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,28 @@ export class ParseError extends Error {
path: string[]
code: string
message: string
received: unknown

constructor({ path, code, message }: { path: string[]; code: string; message: string }) {
const msg = `${path.join(".")}: ${message}`
constructor({
path,
code,
message,
received,
}: {
path: string[]
code: string
message: string
received?: unknown
}) {
let msg = `${path.join(".")}: ${message}`
if (received) {
msg += ` (received: ${received})`
}
super(msg)
this.path = path
this.code = code
this.message = msg
this.name = "ParseError"
this.received = received
}
}
13 changes: 7 additions & 6 deletions src/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ const args = massarg<A>({
name: "my-cli",
description: "This is an example CLI",
})
// .main((opts) => {
// console.log("Main command - printing all opts")
// console.log(opts)
// })
.main((opts, parser) => {
console.log("Main command - printing all opts")
console.log(opts, "\n")
parser.printHelp()
})
.command(
massarg<{ component: string }>({
name: "add",
Expand Down Expand Up @@ -70,7 +71,7 @@ const args = massarg<A>({
)
.option({
name: "bool",
description: "Example number option",
description: "Example boolean option",
aliases: ["b"],
type: "boolean",
})
Expand All @@ -83,6 +84,6 @@ const args = massarg<A>({

const opts = args.getArgs(process.argv.slice(2))

console.log("Opts:", opts)
console.log("Opts:", opts, "\n")

args.parse(process.argv.slice(2))
2 changes: 1 addition & 1 deletion src/massarg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default class Massarg<Args extends ArgsObject = ArgsObject> extends Massa
export { Massarg }

export function massarg<Args extends ArgsObject = ArgsObject>(
options: MinimalCommandConfig<Args>,
options: MinimalCommandConfig<Args>
): MassargCommand<Args> {
return new Massarg(options)
}
30 changes: 22 additions & 8 deletions src/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,21 @@ export const TypedOptionConfig = <T extends z.ZodType>(type: T) =>
type: z.enum(["string", "number", "boolean"]).optional(),
}),
)
export type TypedOptionConfig<T = unknown> = z.infer<ReturnType<typeof TypedOptionConfig<z.ZodType<T>>>>
export type TypedOptionConfig<T = unknown> = z.infer<
ReturnType<typeof TypedOptionConfig<z.ZodType<T>>>
>

export const ArrayOptionConfig = <T extends z.ZodType>(type: T) =>
TypedOptionConfig(z.array(type)).merge(
z.object({
defaultValue: z.array(type).optional(),
}),
)
export type ArrayOptionConfig<T = unknown> = z.infer<ReturnType<typeof ArrayOptionConfig<z.ZodType<T>>>>
export type ArrayOptionConfig<T = unknown> = z.infer<
ReturnType<typeof ArrayOptionConfig<z.ZodType<T>>>
>

// TODO turn to options
const OPT_FULL_PREFIX = "--"
const OPT_SHORT_PREFIX = "-"
const NEGATE_FULL_PREFIX = "no-"
Expand Down Expand Up @@ -64,18 +69,21 @@ export default class MassargOption<T = unknown> {
return new MassargOption(config as OptionConfig<T>)
}

valueFromArgv(argv: string[]): ArgvValue<T> {
_parseDetails(argv: string[]): ArgvValue<T> {
// TODO: support --option=value
argv.shift()
let input = ""
try {
const value = this.parse(argv.shift()!)
input = argv.shift()!
const value = this.parse(input)
return { key: this.name, value, argv }
} catch (e) {
if (isZodError(e)) {
throw new ParseError({
path: [this.name, ...e.issues[0].path.map((p) => p.toString())],
code: e.issues[0].code,
message: e.issues[0].message,
received: JSON.stringify(input),
})
}
throw e
Expand Down Expand Up @@ -109,7 +117,11 @@ export default class MassargOption<T = unknown> {
}

_isOption(arg: string): boolean {
return arg.startsWith(OPT_FULL_PREFIX) || arg.startsWith(OPT_SHORT_PREFIX) || arg.startsWith(NEGATE_SHORT_PREFIX)
return (
arg.startsWith(OPT_FULL_PREFIX) ||
arg.startsWith(OPT_SHORT_PREFIX) ||
arg.startsWith(NEGATE_SHORT_PREFIX)
)
}
}

Expand All @@ -121,14 +133,15 @@ export class MassargNumber extends MassargOption<number> {
})
}

valueFromArgv(argv: string[]): ArgvValue<number> {
_parseDetails(argv: string[]): ArgvValue<number> {
try {
const { argv: _argv, value } = super.valueFromArgv(argv)
const { argv: _argv, value } = super._parseDetails(argv)
if (isNaN(value)) {
throw new ParseError({
path: [this.name],
code: "invalid_type",
message: "Expected a number",
received: JSON.stringify(argv[0]),
})
}
return { key: this.name, value, argv: _argv }
Expand All @@ -138,6 +151,7 @@ export class MassargNumber extends MassargOption<number> {
path: [this.name, ...e.issues[0].path.map((p) => p.toString())],
code: e.issues[0].code,
message: e.issues[0].message,
received: JSON.stringify(argv[0]),
})
}
throw e
Expand All @@ -153,7 +167,7 @@ export class MassargFlag extends MassargOption<boolean> {
})
}

valueFromArgv(argv: string[]): ArgvValue<boolean> {
_parseDetails(argv: string[]): ArgvValue<boolean> {
try {
const isNegation = argv[0]?.startsWith("^")
argv.shift()
Expand Down
Loading

0 comments on commit 29d6973

Please sign in to comment.