This repository has been archived by the owner on Jul 16, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
275 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) |