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

Commit

Permalink
add apis for manipulating handlers (#422)
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-smart authored Dec 12, 2023
1 parent 29c2915 commit ca7dcd5
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/small-jokes-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/cli": patch
---

add Command.withHandler,transformHandler,provide,provideEffectDiscard
66 changes: 66 additions & 0 deletions src/Command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -45,6 +46,7 @@ export interface Command<Name extends string, R, E, A>
readonly descriptor: Descriptor.Command<A>
readonly handler: (_: A) => Effect<R, E, void>
readonly tag: Tag<Command.Context<Name>, A>
readonly transform: Command.Transform<R, E, A>
}

/**
Expand Down Expand Up @@ -115,6 +117,12 @@ export declare namespace Command {
readonly options: ReadonlyArray<Options<any>>
readonly tree: ParsedConfigTree
}

/**
* @since 1.0.0
* @category models
*/
export type Transform<R, E, A> = (effect: Effect<any, any, void>, config: A) => Effect<R, E, void>
}

/**
Expand Down Expand Up @@ -243,6 +251,50 @@ export const prompt: <Name extends string, A, R, E>(
handler: (_: A) => Effect<R, E, void>
) => Command<string, R, E, A> = Internal.prompt

/**
* @since 1.0.0
* @category combinators
*/
export const provide: {
<A, LR, LE, LA>(
layer: Layer<LR, LE, LA> | ((_: A) => Layer<LR, LE, LA>)
): <Name extends string, R, E>(
self: Command<Name, R, E, A>
) => Command<Name, LR | Exclude<R, LA>, LE | E, A>
<Name extends string, R, E, A, LR, LE, LA>(
self: Command<Name, R, E, A>,
layer: Layer<LR, LE, LA> | ((_: A) => Layer<LR, LE, LA>)
): Command<Name, LR | Exclude<R, LA>, E | LE, A>
} = Internal.provide

/**
* @since 1.0.0
* @category combinators
*/
export const provideEffectDiscard: {
<A, R2, E2, _>(
effect: Effect<R2, E2, _> | ((_: A) => Effect<R2, E2, _>)
): <Name extends string, R, E>(self: Command<Name, R, E, A>) => Command<Name, R2 | R, E2 | E, A>
<Name extends string, R, E, A, R2, E2, _>(
self: Command<Name, R, E, A>,
effect: Effect<R2, E2, _> | ((_: A) => Effect<R2, E2, _>)
): Command<Name, R | R2, E | E2, A>
} = Internal.provideEffectDiscard

/**
* @since 1.0.0
* @category combinators
*/
export const transformHandler: {
<R, E, A, R2, E2>(
f: (effect: Effect<R, E, void>, config: A) => Effect<R2, E2, void>
): <Name extends string>(self: Command<Name, R, E, A>) => Command<Name, R | R2, E | E2, A>
<Name extends string, R, E, A, R2, E2>(
self: Command<Name, R, E, A>,
f: (effect: Effect<R, E, void>, config: A) => Effect<R2, E2, void>
): Command<Name, R | R2, E | E2, A>
} = Internal.transformHandler

/**
* @since 1.0.0
* @category combinators
Expand All @@ -257,6 +309,20 @@ export const withDescription: {
): Command<Name, R, E, A>
} = Internal.withDescription

/**
* @since 1.0.0
* @category combinators
*/
export const withHandler: {
<A, R, E>(
handler: (_: A) => Effect<R, E, void>
): <Name extends string, XR, XE>(self: Command<Name, XR, XE, A>) => Command<Name, R, E, A>
<Name extends string, XR, XE, A, R, E>(
self: Command<Name, XR, XE, A>,
handler: (_: A) => Effect<R, E, void>
): Command<Name, R, E, A>
} = Internal.withHandler

/**
* @since 1.0.0
* @category combinators
Expand Down
99 changes: 91 additions & 8 deletions src/internal/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -135,15 +136,33 @@ const getDescriptor = <Name extends string, R, E, A>(self: Command.Command<Name,
const makeProto = <Name extends string, R, E, A>(
descriptor: Descriptor.Command<A>,
handler: (_: A) => Effect.Effect<R, E, void>,
tag?: Context.Tag<any, any>
tag?: Context.Tag<any, any>,
transform: Command.Command.Transform<R, E, A> = identity
): Command.Command<Name, R, E, A> => {
const self = Object.create(Prototype)
self.descriptor = descriptor
self.handler = handler
self.transform = transform
self.tag = tag ?? Context.Tag()
return self
}

const makeDerive = <Name extends string, R, E, A>(
self: Command.Command<Name, any, any, A>,
options: {
readonly descriptor?: Descriptor.Command<A>
readonly handler?: (_: A) => Effect.Effect<R, E, void>
readonly transform?: Command.Command.Transform<R, E, A>
}
): Command.Command<Name, R, E, A> => {
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<
{
Expand Down Expand Up @@ -272,7 +291,7 @@ const mapDescriptor = dual<
self: Command.Command<Name, R, E, A>,
f: (_: Descriptor.Command<A>) => Descriptor.Command<A>
) => Command.Command<Name, R, E, A>
>(2, (self, f) => makeProto(f(self.descriptor), self.handler, self.tag))
>(2, (self, f) => makeDerive(self, { descriptor: f(self.descriptor) }))

/** @internal */
export const prompt = <Name extends string, A, R, E>(
Expand All @@ -288,6 +307,68 @@ export const prompt = <Name extends string, A, R, E>(
handler
)

/** @internal */
export const withHandler = dual<
<A, R, E>(
handler: (_: A) => Effect.Effect<R, E, void>
) => <Name extends string, XR, XE>(
self: Command.Command<Name, XR, XE, A>
) => Command.Command<Name, R, E, A>,
<Name extends string, XR, XE, A, R, E>(
self: Command.Command<Name, XR, XE, A>,
handler: (_: A) => Effect.Effect<R, E, void>
) => Command.Command<Name, R, E, A>
>(2, (self, handler) => makeDerive(self, { handler, transform: identity }))

/** @internal */
export const transformHandler = dual<
<R, E, A, R2, E2>(
f: (effect: Effect.Effect<R, E, void>, config: A) => Effect.Effect<R2, E2, void>
) => <Name extends string>(
self: Command.Command<Name, R, E, A>
) => Command.Command<Name, R | R2, E | E2, A>,
<Name extends string, R, E, A, R2, E2>(
self: Command.Command<Name, R, E, A>,
f: (effect: Effect.Effect<R, E, void>, config: A) => Effect.Effect<R2, E2, void>
) => Command.Command<Name, R | R2, E | E2, A>
>(2, (self, f) => makeDerive(self, { transform: f }))

/** @internal */
export const provide = dual<
<A, LR, LE, LA>(
layer: Layer.Layer<LR, LE, LA> | ((_: A) => Layer.Layer<LR, LE, LA>)
) => <Name extends string, R, E>(
self: Command.Command<Name, R, E, A>
) => Command.Command<Name, Exclude<R, LA> | LR, E | LE, A>,
<Name extends string, R, E, A, LR, LE, LA>(
self: Command.Command<Name, R, E, A>,
layer: Layer.Layer<LR, LE, LA> | ((_: A) => Layer.Layer<LR, LE, LA>)
) => Command.Command<Name, Exclude<R, LA> | 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<
<A, R2, E2, _>(
effect: Effect.Effect<R2, E2, _> | ((_: A) => Effect.Effect<R2, E2, _>)
) => <Name extends string, R, E>(
self: Command.Command<Name, R, E, A>
) => Command.Command<Name, R | R2, E | E2, A>,
<Name extends string, R, E, A, R2, E2, _>(
self: Command.Command<Name, R, E, A>,
effect: Effect.Effect<R2, E2, _> | ((_: A) => Effect.Effect<R2, E2, _>)
) => Command.Command<Name, R | R2, E | E2, A>
>(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<
(
Expand Down Expand Up @@ -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<Context.Tag<any, any>, Command.Command<any, any, any, any>>(),
(handlers, subcommand) => {
Expand All @@ -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 */
Expand Down Expand Up @@ -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)
})
42 changes: 30 additions & 12 deletions test/Command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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]),
Expand All @@ -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", () =>
Expand All @@ -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))
Expand All @@ -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))
Expand All @@ -83,6 +99,7 @@ describe("Command", () => {
const messages = yield* _(Messages)
yield* _(run(["clone", "repo"]))
assert.deepStrictEqual(yield* _(messages.messages), [
"shared",
"Cloning repo"
])
}).pipe(
Expand All @@ -98,6 +115,7 @@ describe("Command", () => {
const messages = yield* _(Messages)
yield* _(run(["clone"]))
assert.deepStrictEqual(yield* _(messages.messages), [
"shared",
"Cloning repo"
])
}).pipe(
Expand Down

0 comments on commit ca7dcd5

Please sign in to comment.