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

Commit

Permalink
add HandledCommand module
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-smart committed Nov 27, 2023
1 parent 215bf5b commit b5f750a
Show file tree
Hide file tree
Showing 2 changed files with 275 additions and 0 deletions.
135 changes: 135 additions & 0 deletions examples/naval-fate-2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Args, CliApp, Command, Options } from "@effect/cli"
import * as Handled from "@effect/cli/HandledCommand"
import * as KeyValueStore from "@effect/platform-node/KeyValueStore"
import * as NodeContext from "@effect/platform-node/NodeContext"
import * as Runtime from "@effect/platform-node/Runtime"
import * as Console from "effect/Console"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as NavalFateStore from "./naval-fate/store.js"

const { createShip, moveShip, removeMine, setMine, shoot } = Effect.serviceFunctions(
NavalFateStore.NavalFateStore
)

// naval_fate [-h | --help] [--version]
// naval_fate ship new <name>...
// naval_fate ship move [--speed=<kn>] <name> <x> <y>
// naval_fate ship shoot <x> <y>
// naval_fate mine set <x> <y> [--moored]
// naval_fate mine remove <x> <y> [--moored]

const nameArg = Args.text({ name: "name" })
const xArg = Args.integer({ name: "x" })
const yArg = Args.integer({ name: "y" })
const nameAndCoordinatesArg = Args.all({ name: nameArg, x: xArg, y: yArg })
const coordinatesArg = Args.all({ x: xArg, y: yArg })

const mooredOption = Options.boolean("moored").pipe(
Options.withDescription("Whether the mine is moored (anchored) or drifting")
)
const speedOption = Options.integer("speed").pipe(
Options.withDescription("Speed in knots"),
Options.withDefault(10)
)

const newShipCommand = Command.make("new", {
args: nameArg
}).pipe(
Handled.make("new", ({ args: name }) =>
createShip(name).pipe(
Effect.zipRight(Console.log(`Created ship: '${name}'`))
))
)

const moveShipCommand = Command.make("move", {
args: nameAndCoordinatesArg,
options: speedOption
}).pipe(
Handled.make("move", ({ args: { name, x, y }, options: speed }) =>
moveShip(name, x, y).pipe(
Effect.zipRight(
Console.log(`Moving ship '${name}' to coordinates (${x}, ${y}) at ${speed} knots`)
)
))
)

const shootShipCommand = Command.make("shoot", {
args: coordinatesArg
}).pipe(
Handled.make("shoot", ({ args: { x, y } }) =>
shoot(x, y).pipe(
Effect.zipRight(Console.log(`Shot cannons at coordinates (${x}, ${y})`))
))
)

const shipCommand = Command.make("ship").pipe(
Handled.makeUnit("ship"),
Handled.withSubcommands([
newShipCommand,
moveShipCommand,
shootShipCommand
])
)

const setMineCommand = Command.make("set", {
args: coordinatesArg,
options: mooredOption
}).pipe(
Handled.make("set", ({ args: { x, y }, options: moored }) =>
setMine(x, y).pipe(
Effect.zipRight(
Console.log(`Set ${moored ? "moored" : "drifting"} mine at coordinates (${x}, ${y})`)
)
))
)

const removeMineCommand = Command.make("remove", {
args: coordinatesArg
}).pipe(
Handled.make("remove", ({ args: { x, y } }) =>
removeMine(x, y).pipe(
Effect.zipRight(Console.log(`Removing mine at coordinates (${x}, ${y}), if present`))
))
)

const mineCommand = Command.make("mine").pipe(
Handled.makeUnit("mine"),
Handled.withSubcommands([
setMineCommand,
removeMineCommand
])
)

const navalFate = Command.make("naval_fate").pipe(
Command.withDescription("An implementation of the Naval Fate CLI application."),
Handled.makeUnit("naval_fate"),
Handled.withSubcommands([shipCommand, mineCommand])
)

const navalFateApp = CliApp.make({
name: "Naval Fate",
version: "1.0.0",
command: navalFate.command
})

const main = Effect.sync(() => globalThis.process.argv.slice(2)).pipe(
Effect.flatMap((argv) =>
CliApp.run(
navalFateApp,
argv,
navalFate.handler
)
)
)

const MainLayer = NavalFateStore.layer.pipe(
Layer.use(KeyValueStore.layerFileSystem("naval-fate-store")),
Layer.merge(NodeContext.layer)
)

main.pipe(
Effect.provide(MainLayer),
Effect.tapErrorCause(Effect.logError),
Runtime.runMain
)
140 changes: 140 additions & 0 deletions src/HandledCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* @since 1.0.0
*/
import * as Effect from "effect/Effect"
import { dual } from "effect/Function"
import type * as Option from "effect/Option"
import type { Pipeable } from "effect/Pipeable"
import ReadonlyArray from "effect/ReadonlyArray"
import * as Command from "./Command.js"

/**
* @since 1.0.0
* @category type ids
*/
export const TypeId = Symbol.for("@effect/cli/HandledCommand")

/**
* @since 1.0.0
* @category type ids
*/
export type TypeId = typeof TypeId

/**
* @since 1.0.0
* @category models
*/
export interface HandledCommand<Name extends string, A, R, E> extends Pipeable {
readonly [TypeId]: TypeId
readonly name: Name
readonly command: Command.Command<A>
readonly handler: (_: A) => Effect.Effect<R, E, void>
}

const Prototype = {
[TypeId]: TypeId
}

/**
* @since 1.0.0
* @category constructors
*/
export const make = dual<
<Name extends string, A, R, E>(
name: Name,
handler: (_: A) => Effect.Effect<R, E, void>
) => (
command: Command.Command<{ readonly name: Name } & A>
) => HandledCommand<Name, A, R, E>,
<Name extends string, A, R, E>(
command: Command.Command<{ readonly name: Name } & A>,
name: Name,
handler: (_: A) => Effect.Effect<R, E, void>
) => HandledCommand<Name, "name", R, E>
>(3, (command, name, handler) => {
const self = Object.create(Prototype)
self.name = name
self.command = command
self.handler = handler
return self
})

/**
* @since 1.0.0
* @category constructors
*/
export const makeUnit = dual<
<Name extends string, A>(
name: Name
) => (
command: Command.Command<{ readonly name: Name } & A>
) => HandledCommand<Name, A, never, never>,
<Name extends string, A>(
command: Command.Command<{ readonly name: Name } & A>,
name: Name
) => HandledCommand<Name, A, never, never>
>(2, (command, name) => make(command, name, (_) => Effect.unit) as any)

/**
* @since 1.0.0
* @category combinators
*/
export const withSubcommands = dual<
<
Subcommand extends ReadonlyArray.NonEmptyReadonlyArray<HandledCommand<any, any, any, any>>
>(
subcommands: Subcommand
) => <Name extends string, A, R, E>(self: HandledCommand<Name, A, R, E>) => HandledCommand<
Name,
Command.Command.ComputeParsedType<
& A
& Readonly<
{ subcommand: Option.Option<Command.Command.GetParsedType<Subcommand[number]["command"]>> }
>
>,
R | Effect.Effect.Context<ReturnType<Subcommand[number]["handler"]>>,
E | Effect.Effect.Error<ReturnType<Subcommand[number]["handler"]>>
>,
<
Name extends string,
A,
R,
E,
Subcommand extends ReadonlyArray.NonEmptyReadonlyArray<HandledCommand<any, any, any, any>>
>(
self: HandledCommand<Name, A, R, E>,
subcommands: Subcommand
) => HandledCommand<
Name,
Command.Command.ComputeParsedType<
& A
& Readonly<
{ subcommand: Option.Option<Command.Command.GetParsedType<Subcommand[number]["command"]>> }
>
>,
R | Effect.Effect.Context<ReturnType<Subcommand[number]["handler"]>>,
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) => {
handlers[subcommand.name] = subcommand.handler
return handlers
}
)
const handler = (
args: { readonly subcommand: Option.Option<{ readonly name: string }> }
) => {
if (args.subcommand._tag === "Some") {
return handlers[args.subcommand.value.name](args.subcommand.value)
}
return self.handler(args as any)
}
return make(command as any, self.name, handler) as any
})

0 comments on commit b5f750a

Please sign in to comment.