diff --git a/examples/naval-fate.ts b/examples/naval-fate.ts index b2354d8..7f64248 100644 --- a/examples/naval-fate.ts +++ b/examples/naval-fate.ts @@ -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", { @@ -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, @@ -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."), diff --git a/src/HandledCommand.ts b/src/HandledCommand.ts index a859fa5..5af5b0b 100644 --- a/src/HandledCommand.ts +++ b/src/HandledCommand.ts @@ -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" @@ -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 @@ -28,14 +30,21 @@ export type TypeId = typeof TypeId * @since 1.0.0 * @category models */ -export interface HandledCommand extends Pipeable { +export interface HandledCommand + extends Pipeable, Effect.Effect, never, A> +{ readonly [TypeId]: TypeId readonly command: Command.Command readonly handler: (_: A) => Effect.Effect + readonly tag: Context.Tag, A> } const Prototype = { + ...Effectable.CommitPrototype, [TypeId]: TypeId, + commit(this: HandledCommand) { + return this.tag + }, pipe() { return pipeArguments(this, arguments) } @@ -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< + (f: (_: HandledCommand) => HandledCommand) => ( + self: HandledCommand + ) => HandledCommand, + ( + self: HandledCommand, + f: (_: HandledCommand) => HandledCommand + ) => HandledCommand +>(2, (self, f) => { + const command = f(self) + ;(command as any).tag = self.tag + return command +}) + /** * @since 1.0.0 * @category constructors @@ -72,10 +100,10 @@ export const fromCommandUnit = ( * @since 1.0.0 * @category constructors */ -export const fromCommandOrDie = ( - command: Command.Command, - orDie: () => unknown -): HandledCommand => fromCommand(command, (_) => Effect.dieSync(orDie)) +export const fromCommandRequestHelp = ( + command: Command.Command +): HandledCommand => + fromCommand(command, (_) => Effect.fail(ValidationError.helpRequested(command))) /** * @since 1.0.0 @@ -110,15 +138,14 @@ export const makeUnit = ( +export const makeRequestHelp = ( name: Name, - config: Command.Command.ConstructorConfig, - orDie: () => unknown + config?: Command.Command.ConstructorConfig ): 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 @@ -134,7 +161,8 @@ export const withSubcommands = dual< { subcommand: Option.Option> } > >, - R | Effect.Effect.Context>, + | R + | Exclude>, Command.Command>, E | Effect.Effect.Error> >, < @@ -152,37 +180,43 @@ export const withSubcommands = dual< { subcommand: Option.Option> } > >, - R | Effect.Effect.Context>, + | R + | Exclude>, Command.Command>, E | Effect.Effect.Error> > ->(2, (self, subcommands) => { - const command = Command.withSubcommands( - self.command, - ReadonlyArray.map(subcommands, (_) => _.command) - ) - const handlers = ReadonlyArray.reduce( - subcommands, - {} as Record Effect.Effect>, - (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 Effect.Effect>, + (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 @@ -198,7 +232,7 @@ export const toAppAndRun = dual< self: HandledCommand ) => ( args: ReadonlyArray - ) => Effect.Effect, + ) => Effect.Effect, (self: HandledCommand, config: { readonly name: string readonly version: string @@ -206,7 +240,7 @@ export const toAppAndRun = dual< readonly footer?: HelpDoc | undefined }) => ( args: ReadonlyArray - ) => Effect.Effect + ) => Effect.Effect >(2, (self, config) => { const app = CliApp.make({ ...config,