diff --git a/.changeset/small-jokes-ring.md b/.changeset/small-jokes-ring.md new file mode 100644 index 0000000..c8c247c --- /dev/null +++ b/.changeset/small-jokes-ring.md @@ -0,0 +1,5 @@ +--- +"@effect/cli": patch +--- + +add Command.withHandler,transformHandler,provide,provideEffectDiscard diff --git a/src/Command.ts b/src/Command.ts index 07fab31..f4b2d0a 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -7,6 +7,7 @@ import type { Tag } from "effect/Context" import type { Effect } from "effect/Effect" import type { HashMap } from "effect/HashMap" import type { HashSet } from "effect/HashSet" +import type { Layer } from "effect/Layer" import type { Option } from "effect/Option" import { type Pipeable } from "effect/Pipeable" import type * as Types from "effect/Types" @@ -45,6 +46,7 @@ export interface Command readonly descriptor: Descriptor.Command readonly handler: (_: A) => Effect readonly tag: Tag, A> + readonly transform: Command.Transform } /** @@ -115,6 +117,12 @@ export declare namespace Command { readonly options: ReadonlyArray> readonly tree: ParsedConfigTree } + + /** + * @since 1.0.0 + * @category models + */ + export type Transform = (effect: Effect, config: A) => Effect } /** @@ -243,6 +251,50 @@ export const prompt: ( handler: (_: A) => Effect ) => Command = Internal.prompt +/** + * @since 1.0.0 + * @category combinators + */ +export const provide: { + ( + layer: Layer | ((_: A) => Layer) + ): ( + self: Command + ) => Command, LE | E, A> + ( + self: Command, + layer: Layer | ((_: A) => Layer) + ): Command, E | LE, A> +} = Internal.provide + +/** + * @since 1.0.0 + * @category combinators + */ +export const provideEffectDiscard: { + ( + effect: Effect | ((_: A) => Effect) + ): (self: Command) => Command + ( + self: Command, + effect: Effect | ((_: A) => Effect) + ): Command +} = Internal.provideEffectDiscard + +/** + * @since 1.0.0 + * @category combinators + */ +export const transformHandler: { + ( + f: (effect: Effect, config: A) => Effect + ): (self: Command) => Command + ( + self: Command, + f: (effect: Effect, config: A) => Effect + ): Command +} = Internal.transformHandler + /** * @since 1.0.0 * @category combinators @@ -257,6 +309,20 @@ export const withDescription: { ): Command } = Internal.withDescription +/** + * @since 1.0.0 + * @category combinators + */ +export const withHandler: { + ( + handler: (_: A) => Effect + ): (self: Command) => Command + ( + self: Command, + handler: (_: A) => Effect + ): Command +} = Internal.withHandler + /** * @since 1.0.0 * @category combinators diff --git a/src/internal/command.ts b/src/internal/command.ts index a02f14f..1a2f86e 100644 --- a/src/internal/command.ts +++ b/src/internal/command.ts @@ -3,10 +3,11 @@ import type * as Terminal from "@effect/platform/Terminal" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import * as Effectable from "effect/Effectable" -import { dual } from "effect/Function" +import { dual, identity } from "effect/Function" import { globalValue } from "effect/GlobalValue" import type * as HashMap from "effect/HashMap" import type * as HashSet from "effect/HashSet" +import type * as Layer from "effect/Layer" import type * as Option from "effect/Option" import { pipeArguments } from "effect/Pipeable" import * as ReadonlyArray from "effect/ReadonlyArray" @@ -135,15 +136,33 @@ const getDescriptor = (self: Command.Command( descriptor: Descriptor.Command, handler: (_: A) => Effect.Effect, - tag?: Context.Tag + tag?: Context.Tag, + transform: Command.Command.Transform = identity ): Command.Command => { const self = Object.create(Prototype) self.descriptor = descriptor self.handler = handler + self.transform = transform self.tag = tag ?? Context.Tag() return self } +const makeDerive = ( + self: Command.Command, + options: { + readonly descriptor?: Descriptor.Command + readonly handler?: (_: A) => Effect.Effect + readonly transform?: Command.Command.Transform + } +): Command.Command => { + const command = Object.create(Prototype) + command.descriptor = options.descriptor ?? self.descriptor + command.handler = options.handler ?? self.handler + command.transform = options.transform ?? self.transform + command.tag = self.tag + return command +} + /** @internal */ export const fromDescriptor = dual< { @@ -272,7 +291,7 @@ const mapDescriptor = dual< self: Command.Command, f: (_: Descriptor.Command) => Descriptor.Command ) => Command.Command ->(2, (self, f) => makeProto(f(self.descriptor), self.handler, self.tag)) +>(2, (self, f) => makeDerive(self, { descriptor: f(self.descriptor) })) /** @internal */ export const prompt = ( @@ -288,6 +307,68 @@ export const prompt = ( handler ) +/** @internal */ +export const withHandler = dual< + ( + handler: (_: A) => Effect.Effect + ) => ( + self: Command.Command + ) => Command.Command, + ( + self: Command.Command, + handler: (_: A) => Effect.Effect + ) => Command.Command +>(2, (self, handler) => makeDerive(self, { handler, transform: identity })) + +/** @internal */ +export const transformHandler = dual< + ( + f: (effect: Effect.Effect, config: A) => Effect.Effect + ) => ( + self: Command.Command + ) => Command.Command, + ( + self: Command.Command, + f: (effect: Effect.Effect, config: A) => Effect.Effect + ) => Command.Command +>(2, (self, f) => makeDerive(self, { transform: f })) + +/** @internal */ +export const provide = dual< + ( + layer: Layer.Layer | ((_: A) => Layer.Layer) + ) => ( + self: Command.Command + ) => Command.Command | LR, E | LE, A>, + ( + self: Command.Command, + layer: Layer.Layer | ((_: A) => Layer.Layer) + ) => Command.Command | LR, E | LE, A> +>(2, (self, layer) => + makeDerive(self, { + transform: (effect, config) => + Effect.provide(effect, typeof layer === "function" ? layer(config) : layer) + })) + +/** @internal */ +export const provideEffectDiscard = dual< + ( + effect: Effect.Effect | ((_: A) => Effect.Effect) + ) => ( + self: Command.Command + ) => Command.Command, + ( + self: Command.Command, + effect: Effect.Effect | ((_: A) => Effect.Effect) + ) => Command.Command +>(2, (self, effect_) => + makeDerive(self, { + transform: (self, config) => { + const effect = typeof effect_ === "function" ? effect_(config) : effect_ + return Effect.zipRight(effect, self) + } + })) + /** @internal */ export const withDescription = dual< ( @@ -357,7 +438,7 @@ export const withSubcommands = dual< self.descriptor, ReadonlyArray.map(subcommands, (_) => [_.tag, _.descriptor]) ) - const handlers = ReadonlyArray.reduce( + const subcommandMap = ReadonlyArray.reduce( subcommands, new Map, Command.Command>(), (handlers, subcommand) => { @@ -374,16 +455,17 @@ export const withSubcommands = dual< ) { if (args.subcommand._tag === "Some") { const [tag, value] = args.subcommand.value - const subcommand = handlers.get(tag)! + const subcommand = subcommandMap.get(tag)! + const subcommandEffect = subcommand.transform(subcommand.handler(value), value) return Effect.provideService( - subcommand.handler(value), + subcommandEffect, self.tag, args as any ) } return self.handler(args as any) } - return makeProto(command as any, handler, self.tag) as any + return makeDerive(self as any, { descriptor: command as any, handler }) as any }) /** @internal */ @@ -435,5 +517,6 @@ export const run = dual< command: self.descriptor }) registeredDescriptors.set(self.tag, self.descriptor) - return (args) => InternalCliApp.run(app, args, self.handler) + const handler = (args: any) => self.transform(self.handler(args), args) + return (args) => InternalCliApp.run(app, args, handler) }) diff --git a/test/Command.test.ts b/test/Command.test.ts index 90a5412..e8b1dbd 100644 --- a/test/Command.test.ts +++ b/test/Command.test.ts @@ -8,7 +8,15 @@ const git = Command.make("git", { Options.withAlias("v"), Options.withFallbackConfig(Config.boolean("VERBOSE")) ) -}).pipe(Command.withDescription("the stupid content tracker")) +}).pipe( + Command.withDescription("the stupid content tracker"), + Command.provideEffectDiscard(() => + Effect.flatMap( + Messages, + (_) => _.log("shared") + ) + ) +) const clone = Command.make("clone", { repository: Args.text({ name: "repository" }).pipe( @@ -27,16 +35,20 @@ const clone = Command.make("clone", { const add = Command.make("add", { pathspec: Args.text({ name: "pathspec" }) -}, ({ pathspec }) => - Effect.gen(function*(_) { - const { log } = yield* _(Messages) - const { verbose } = yield* _(git) - if (verbose) { - yield* _(log(`Adding ${pathspec}`)) - } else { - yield* _(log(`Adding`)) - } - })).pipe(Command.withDescription("Add file contents to the index")) +}).pipe( + Command.withHandler(({ pathspec }) => + Effect.gen(function*(_) { + const { log } = yield* _(Messages) + const { verbose } = yield* _(git) + if (verbose) { + yield* _(log(`Adding ${pathspec}`)) + } else { + yield* _(log(`Adding`)) + } + }) + ), + Command.withDescription("Add file contents to the index") +) const run = git.pipe( Command.withSubcommands([clone, add]), @@ -53,7 +65,7 @@ describe("Command", () => { const messages = yield* _(Messages) yield* _(run(["--verbose"])) yield* _(run([])) - assert.deepStrictEqual(yield* _(messages.messages), []) + assert.deepStrictEqual(yield* _(messages.messages), ["shared", "shared"]) }).pipe(Effect.provide(EnvLive), Effect.runPromise)) it("add", () => @@ -62,7 +74,9 @@ describe("Command", () => { yield* _(run(["add", "file"])) yield* _(run(["--verbose", "add", "file"])) assert.deepStrictEqual(yield* _(messages.messages), [ + "shared", "Adding", + "shared", "Adding file" ]) }).pipe(Effect.provide(EnvLive), Effect.runPromise)) @@ -73,7 +87,9 @@ describe("Command", () => { yield* _(run(["clone", "repo"])) yield* _(run(["--verbose", "clone", "repo"])) assert.deepStrictEqual(yield* _(messages.messages), [ + "shared", "Cloning", + "shared", "Cloning repo" ]) }).pipe(Effect.provide(EnvLive), Effect.runPromise)) @@ -83,6 +99,7 @@ describe("Command", () => { const messages = yield* _(Messages) yield* _(run(["clone", "repo"])) assert.deepStrictEqual(yield* _(messages.messages), [ + "shared", "Cloning repo" ]) }).pipe( @@ -98,6 +115,7 @@ describe("Command", () => { const messages = yield* _(Messages) yield* _(run(["clone"])) assert.deepStrictEqual(yield* _(messages.messages), [ + "shared", "Cloning repo" ]) }).pipe(