Skip to content
This repository has been archived by the owner on Jul 16, 2024. It is now read-only.

Commit

Permalink
support accessing parent commands from subcommands
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-smart committed Nov 27, 2023
1 parent 600e652 commit c82cbc7
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 54 deletions.
32 changes: 19 additions & 13 deletions examples/naval-fate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,20 @@ const speedOption = Options.integer("speed").pipe(
Options.withDefault(10)
)

const shipCommandParent = HandledCommand.makeRequestHelp("ship", {
options: Options.withDefault(Options.boolean("verbose"), false)
})

const newShipCommand = HandledCommand.make("new", {
args: nameArg
}, ({ args: name }) =>
Effect.gen(function*(_) {
const { options: verbose } = yield* _(shipCommandParent)
yield* _(createShip(name))
yield* _(Console.log(`Created ship: '${name}'`))
if (verbose) {
yield* _(Console.log(`Verbose mode enabled`))
}
}))

const moveShipCommand = HandledCommand.make("move", {
Expand All @@ -57,13 +65,13 @@ const shootShipCommand = HandledCommand.make("shoot", {
yield* _(Console.log(`Shot cannons at coordinates (${x}, ${y})`))
}))

const shipCommand = HandledCommand.makeUnit("ship").pipe(
HandledCommand.withSubcommands([
newShipCommand,
moveShipCommand,
shootShipCommand
])
)
const shipCommand = HandledCommand.withSubcommands(shipCommandParent, [
newShipCommand,
moveShipCommand,
shootShipCommand
])

const mineCommandParent = HandledCommand.makeRequestHelp("mine")

const setMineCommand = HandledCommand.make("set", {
args: coordinatesArg,
Expand All @@ -84,12 +92,10 @@ const removeMineCommand = HandledCommand.make("remove", {
yield* _(Console.log(`Removing mine at coordinates (${x}, ${y}), if present`))
}))

const mineCommand = HandledCommand.makeUnit("mine").pipe(
HandledCommand.withSubcommands([
setMineCommand,
removeMineCommand
])
)
const mineCommand = HandledCommand.withSubcommands(mineCommandParent, [
setMineCommand,
removeMineCommand
])

const run = Command.make("naval_fate").pipe(
Command.withDescription("An implementation of the Naval Fate CLI application."),
Expand Down
116 changes: 75 additions & 41 deletions src/HandledCommand.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/**
* @since 1.0.0
*/
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Effectable from "effect/Effectable"
import { dual } from "effect/Function"
import type * as Option from "effect/Option"
import { type Pipeable, pipeArguments } from "effect/Pipeable"
Expand All @@ -10,7 +12,7 @@ import * as CliApp from "./CliApp.js"
import * as Command from "./Command.js"
import type { HelpDoc } from "./HelpDoc.js"
import type { Span } from "./HelpDoc/Span.js"
import type { ValidationError } from "./ValidationError.js"
import * as ValidationError from "./ValidationError.js"

/**
* @since 1.0.0
Expand All @@ -28,14 +30,21 @@ export type TypeId = typeof TypeId
* @since 1.0.0
* @category models
*/
export interface HandledCommand<A, R, E> extends Pipeable {
export interface HandledCommand<A, R, E>
extends Pipeable, Effect.Effect<Command.Command<A>, never, A>
{
readonly [TypeId]: TypeId
readonly command: Command.Command<A>
readonly handler: (_: A) => Effect.Effect<R, E, void>
readonly tag: Context.Tag<Command.Command<A>, A>
}

const Prototype = {
...Effectable.CommitPrototype,
[TypeId]: TypeId,
commit(this: HandledCommand<any, any, any>) {
return this.tag
},
pipe() {
return pipeArguments(this, arguments)
}
Expand All @@ -57,9 +66,28 @@ export const fromCommand = dual<
const self = Object.create(Prototype)
self.command = command
self.handler = handler
self.tag = Context.Tag()
return self
})

/**
* @since 1.0.0
* @category combinators
*/
export const modify = dual<
<A, R, E, A2, R2, E2>(f: (_: HandledCommand<A, R, E>) => HandledCommand<A2, R2, E2>) => (
self: HandledCommand<A, R, E>
) => HandledCommand<A2, R2, E2>,
<A, R, E, A2, R2, E2>(
self: HandledCommand<A, R, E>,
f: (_: HandledCommand<A, R, E>) => HandledCommand<A2, R2, E2>
) => HandledCommand<A2, R2, E2>
>(2, (self, f) => {
const command = f(self)
;(command as any).tag = self.tag
return command
})

/**
* @since 1.0.0
* @category constructors
Expand All @@ -72,10 +100,10 @@ export const fromCommandUnit = <A extends { readonly name: string }>(
* @since 1.0.0
* @category constructors
*/
export const fromCommandOrDie = <A extends { readonly name: string }>(
command: Command.Command<A>,
orDie: () => unknown
): HandledCommand<A, never, never> => fromCommand(command, (_) => Effect.dieSync(orDie))
export const fromCommandRequestHelp = <A extends { readonly name: string }>(
command: Command.Command<A>
): HandledCommand<A, never, ValidationError.ValidationError> =>
fromCommand(command, (_) => Effect.fail(ValidationError.helpRequested(command)))

/**
* @since 1.0.0
Expand Down Expand Up @@ -110,15 +138,14 @@ export const makeUnit = <Name extends string, OptionsType = void, ArgsType = voi
* @since 1.0.0
* @category constructors
*/
export const makeOrDie = <Name extends string, OptionsType = void, ArgsType = void>(
export const makeRequestHelp = <Name extends string, OptionsType = void, ArgsType = void>(
name: Name,
config: Command.Command.ConstructorConfig<OptionsType, ArgsType>,
orDie: () => unknown
config?: Command.Command.ConstructorConfig<OptionsType, ArgsType>
): HandledCommand<
{ readonly name: Name; readonly options: OptionsType; readonly args: ArgsType },
never,
never
> => fromCommandOrDie(Command.make(name, config), orDie)
ValidationError.ValidationError
> => fromCommandRequestHelp(Command.make(name, config))

/**
* @since 1.0.0
Expand All @@ -134,7 +161,8 @@ export const withSubcommands = dual<
{ subcommand: Option.Option<Command.Command.GetParsedType<Subcommand[number]["command"]>> }
>
>,
R | Effect.Effect.Context<ReturnType<Subcommand[number]["handler"]>>,
| R
| Exclude<Effect.Effect.Context<ReturnType<Subcommand[number]["handler"]>>, Command.Command<A>>,
E | Effect.Effect.Error<ReturnType<Subcommand[number]["handler"]>>
>,
<
Expand All @@ -152,37 +180,43 @@ export const withSubcommands = dual<
{ subcommand: Option.Option<Command.Command.GetParsedType<Subcommand[number]["command"]>> }
>
>,
R | Effect.Effect.Context<ReturnType<Subcommand[number]["handler"]>>,
| R
| Exclude<Effect.Effect.Context<ReturnType<Subcommand[number]["handler"]>>, Command.Command<A>>,
E | Effect.Effect.Error<ReturnType<Subcommand[number]["handler"]>>
>
>(2, (self, subcommands) => {
const command = Command.withSubcommands(
self.command,
ReadonlyArray.map(subcommands, (_) => _.command)
)
const handlers = ReadonlyArray.reduce(
subcommands,
{} as Record<string, (_: any) => Effect.Effect<any, any, void>>,
(handlers, subcommand) => {
for (const name of Command.getNames(subcommand.command)) {
handlers[name] = subcommand.handler
>(2, (self, subcommands) =>
modify(self, () => {
const command = Command.withSubcommands(
self.command,
ReadonlyArray.map(subcommands, (_) => _.command)
)
const handlers = ReadonlyArray.reduce(
subcommands,
{} as Record<string, (_: any) => Effect.Effect<any, any, void>>,
(handlers, subcommand) => {
for (const name of Command.getNames(subcommand.command)) {
handlers[name] = subcommand.handler
}
return handlers
}
return handlers
}
)
const handler = (
args: {
readonly name: string
readonly subcommand: Option.Option<{ readonly name: string }>
}
) => {
if (args.subcommand._tag === "Some") {
return handlers[args.subcommand.value.name](args.subcommand.value)
)
function handler(
args: {
readonly name: string
readonly subcommand: Option.Option<{ readonly name: string }>
}
) {
if (args.subcommand._tag === "Some") {
return Effect.provideService(
handlers[args.subcommand.value.name](args.subcommand.value),
(self as any).tag,
args as any
)
}
return self.handler(args as any)
}
return self.handler(args as any)
}
return fromCommand(command as any, handler) as any
})
return fromCommand(command as any, handler) as any
}))

/**
* @since 1.0.0
Expand All @@ -198,15 +232,15 @@ export const toAppAndRun = dual<
self: HandledCommand<A, R, E>
) => (
args: ReadonlyArray<string>
) => Effect.Effect<R | CliApp.CliApp.Environment, E | ValidationError, void>,
) => Effect.Effect<R | CliApp.CliApp.Environment, E | ValidationError.ValidationError, void>,
<A, R, E>(self: HandledCommand<A, R, E>, config: {
readonly name: string
readonly version: string
readonly summary?: Span | undefined
readonly footer?: HelpDoc | undefined
}) => (
args: ReadonlyArray<string>
) => Effect.Effect<R | CliApp.CliApp.Environment, E | ValidationError, void>
) => Effect.Effect<R | CliApp.CliApp.Environment, E | ValidationError.ValidationError, void>
>(2, (self, config) => {
const app = CliApp.make({
...config,
Expand Down

0 comments on commit c82cbc7

Please sign in to comment.