diff --git a/.changeset/many-badgers-obey.md b/.changeset/many-badgers-obey.md new file mode 100644 index 0000000000..6ec45f57f8 --- /dev/null +++ b/.changeset/many-badgers-obey.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow schemas provided to CLI flags / arguments to utilize the environment required by the CLI diff --git a/packages/effect/src/unstable/cli/Argument.ts b/packages/effect/src/unstable/cli/Argument.ts index ca0fe5ddaf..950ead512a 100644 --- a/packages/effect/src/unstable/cli/Argument.ts +++ b/packages/effect/src/unstable/cli/Argument.ts @@ -17,6 +17,7 @@ import type * as Redacted from "../../Redacted.ts" import type * as Result from "../../Result.ts" import type * as Schema from "../../Schema.ts" import type * as CliError from "./CliError.ts" +import type { Environment } from "./Command.ts" import * as Param from "./Param.ts" import type * as Primitive from "./Primitive.ts" @@ -261,7 +262,7 @@ export const fileParse = ( */ export const fileSchema = ( name: string, - schema: Schema.Decoder, + schema: Schema.Decoder, options?: Primitive.FileSchemaOptions | undefined ): Argument => Param.fileSchema(Param.argumentKind, name, schema, options) @@ -339,11 +340,11 @@ export const withDescription: { */ export const withDefault: { ( - defaultValue: B | Effect.Effect + defaultValue: B | Effect.Effect ): (self: Argument) => Argument ( self: Argument, - defaultValue: B | Effect.Effect + defaultValue: B | Effect.Effect ): Argument } = Param.withDefault @@ -471,15 +472,15 @@ export const map: { */ export const mapEffect: { ( - f: (a: A) => Effect.Effect + f: (a: A) => Effect.Effect ): (self: Argument) => Argument ( self: Argument, - f: (a: A) => Effect.Effect + f: (a: A) => Effect.Effect ): Argument } = dual(2, ( self: Argument, - f: (a: A) => Effect.Effect + f: (a: A) => Effect.Effect ) => Param.mapEffect(self, f)) /** @@ -589,8 +590,8 @@ export const between: { * @since 4.0.0 */ export const withSchema: { - (schema: Schema.Codec): (self: Argument) => Argument - (self: Argument, schema: Schema.Codec): Argument + (schema: Schema.Codec): (self: Argument) => Argument + (self: Argument, schema: Schema.Codec): Argument } = dual(2, (self: Argument, schema: Schema.Codec) => Param.withSchema(self, schema)) /** diff --git a/packages/effect/src/unstable/cli/Flag.ts b/packages/effect/src/unstable/cli/Flag.ts index 4ab5ee058f..4029a6b1b7 100644 --- a/packages/effect/src/unstable/cli/Flag.ts +++ b/packages/effect/src/unstable/cli/Flag.ts @@ -18,6 +18,7 @@ import type * as Redacted from "../../Redacted.ts" import type * as Result from "../../Result.ts" import type * as Schema from "../../Schema.ts" import type * as CliError from "./CliError.ts" +import type { Environment } from "./Command.ts" import * as Param from "./Param.ts" import type * as Primitive from "./Primitive.ts" @@ -355,7 +356,7 @@ export const fileParse = ( */ export const fileSchema = ( name: string, - schema: Schema.Decoder, + schema: Schema.Decoder, options?: Primitive.FileSchemaOptions | undefined ): Flag => Param.fileSchema(Param.flagKind, name, schema, options) @@ -579,8 +580,8 @@ export const optional = (param: Flag): Flag> => Param.opt * @since 4.0.0 */ export const withDefault: { - (defaultValue: B | Effect.Effect): (self: Flag) => Flag - (self: Flag, defaultValue: B | Effect.Effect): Flag + (defaultValue: B | Effect.Effect): (self: Flag) => Flag + (self: Flag, defaultValue: B | Effect.Effect): Flag } = Param.withDefault /** @@ -677,15 +678,15 @@ export const map: { */ export const mapEffect: { ( - f: (a: A) => Effect.Effect + f: (a: A) => Effect.Effect ): (self: Flag) => Flag ( self: Flag, - f: (a: A) => Effect.Effect + f: (a: A) => Effect.Effect ): Flag } = dual(2, ( self: Flag, - f: (a: A) => Effect.Effect + f: (a: A) => Effect.Effect ) => Param.mapEffect(self, f)) /** @@ -981,6 +982,6 @@ export const orElseResult: { * @since 4.0.0 */ export const withSchema: { - (schema: Schema.Codec): (self: Flag) => Flag - (self: Flag, schema: Schema.Codec): Flag + (schema: Schema.Codec): (self: Flag) => Flag + (self: Flag, schema: Schema.Codec): Flag } = dual(2, (self: Flag, schema: Schema.Codec) => Param.withSchema(self, schema)) diff --git a/packages/effect/src/unstable/cli/Param.ts b/packages/effect/src/unstable/cli/Param.ts index 734ecbc9a9..689a3fc08b 100644 --- a/packages/effect/src/unstable/cli/Param.ts +++ b/packages/effect/src/unstable/cli/Param.ts @@ -12,19 +12,16 @@ */ import * as Config from "../../Config.ts" import * as Effect from "../../Effect.ts" -import type * as FileSystem from "../../FileSystem.ts" import { dual, identity } from "../../Function.ts" import * as Option from "../../Option.ts" -import type * as Path from "../../Path.ts" import { type Pipeable, pipeArguments } from "../../Pipeable.ts" import * as Predicate from "../../Predicate.ts" import type * as Redacted from "../../Redacted.ts" import * as Result from "../../Result.ts" import * as Schema from "../../Schema.ts" -import type * as Terminal from "../../Terminal.ts" import type { Covariant } from "../../Types.ts" -import type { ChildProcessSpawner } from "../process/ChildProcessSpawner.ts" import * as CliError from "./CliError.ts" +import type { Environment } from "./Command.ts" import * as Primitive from "./Primitive.ts" import * as Prompt from "./Prompt.ts" @@ -56,15 +53,6 @@ export interface Param extends Param.Variance */ export type ParamKind = "argument" | "flag" -/** - * Services that parameter parsing can require, such as filesystem, path, - * terminal, and child-process support. - * - * @category models - * @since 4.0.0 - */ -export type Environment = FileSystem.FileSystem | Path.Path | Terminal.Terminal | ChildProcessSpawner - /** * Defines the kind discriminator for positional argument parameters. * @@ -859,7 +847,7 @@ export const fileParse = ( export const fileSchema = ( kind: Kind, name: string, - schema: Schema.Decoder, + schema: Schema.Decoder, options?: Primitive.FileSchemaOptions | undefined ): Param => makeSingle({ @@ -1799,8 +1787,15 @@ export const withMetavar: { * @since 4.0.0 */ export const withSchema: { - (schema: Schema.Codec): (self: Param) => Param - (self: Param, schema: Schema.Codec): Param + ( + schema: Schema.Codec + ): ( + self: Param + ) => Param + ( + self: Param, + schema: Schema.Codec + ): Param } = dual(2, ( self: Param, schema: Schema.Codec diff --git a/packages/effect/src/unstable/cli/Primitive.ts b/packages/effect/src/unstable/cli/Primitive.ts index 53c04b44d2..dd340e80b0 100644 --- a/packages/effect/src/unstable/cli/Primitive.ts +++ b/packages/effect/src/unstable/cli/Primitive.ts @@ -24,6 +24,7 @@ import * as Schema from "../../Schema.ts" import type { Formatter } from "../../SchemaIssue.ts" import type * as Struct from "../../Struct.ts" import type { Covariant } from "../../Types.ts" +import type { Environment } from "./Command.ts" const TypeId = "~effect/cli/Primitive" @@ -58,7 +59,7 @@ const TypeId = "~effect/cli/Primitive" */ export interface Primitive extends Primitive.Variance { readonly _tag: string - readonly parse: (value: string) => Effect.Effect + readonly parse: (value: string) => Effect.Effect } /** @@ -97,9 +98,7 @@ export const isBoolean = (p: Primitive): p is Primitive => p._ const makePrimitive = ( tag: string, - parse: ( - value: string - ) => Effect.Effect + parse: (value: string) => Effect.Effect ): Primitive => Object.assign(Object.create(Proto), { _tag: tag, @@ -108,7 +107,7 @@ const makePrimitive = ( const makeSchemaPrimitive = ( tag: string, - schema: Schema.Codec + schema: Schema.Codec ): Primitive => { const toCodecStringTree = Schema.toCodecStringTree(schema) const decode = Schema.decodeUnknownEffect(toCodecStringTree) @@ -614,7 +613,7 @@ export type FileSchemaOptions = Struct.Simplify< * @since 4.0.0 */ export const fileSchema = ( - schema: Schema.Decoder, + schema: Schema.Decoder, options?: FileSchemaOptions | undefined ): Primitive => { const decode = Schema.decodeUnknownEffect(schema) diff --git a/packages/effect/src/unstable/cli/internal/parser.ts b/packages/effect/src/unstable/cli/internal/parser.ts index 40abd776e8..18fb1e3cc1 100644 --- a/packages/effect/src/unstable/cli/internal/parser.ts +++ b/packages/effect/src/unstable/cli/internal/parser.ts @@ -27,7 +27,7 @@ import * as Effect from "../../../Effect.ts" import * as Option from "../../../Option.ts" import * as CliError from "../CliError.ts" -import type { Command, ParsedTokens } from "../Command.ts" +import type { Command, Environment, ParsedTokens } from "../Command.ts" import * as Param from "../Param.ts" import * as Primitive from "../Primitive.ts" import { suggest } from "./auto-suggest.ts" @@ -50,7 +50,7 @@ export const parseArgs = ( lexResult: LexResult, command: Command.Any, commandPath: ReadonlyArray = [] -): Effect.Effect => +): Effect.Effect => Effect.gen(function*() { const { tokens, trailingOperands: afterEndOfOptions } = lexResult const newCommandPath = [...commandPath, command.name] diff --git a/packages/effect/test/unstable/cli/Arguments.test.ts b/packages/effect/test/unstable/cli/Arguments.test.ts index 29b16436bf..7314bf3755 100644 --- a/packages/effect/test/unstable/cli/Arguments.test.ts +++ b/packages/effect/test/unstable/cli/Arguments.test.ts @@ -1,5 +1,5 @@ import { assert, describe, expect, it } from "@effect/vitest" -import { Effect, FileSystem, Layer, Option, Path, PlatformError, Ref, Result } from "effect" +import { Effect, FileSystem, Layer, Option, Path, PlatformError, Ref, Result, Stdio } from "effect" import { TestConsole } from "effect/testing" import { Argument, CliOutput, Command, Flag } from "effect/unstable/cli" import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner" @@ -32,6 +32,11 @@ const CliOutputLayer = CliOutput.layer( colors: false }) ) +const StdioLayer = Stdio.layerTest({}) +const ChildProcessSpawnerLayer = Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make(() => Effect.die("Not implemented")) +) const TestLayer = Layer.mergeAll( ConsoleLayer, @@ -39,7 +44,8 @@ const TestLayer = Layer.mergeAll( PathLayer, TerminalLayer, CliOutputLayer, - Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.die("Not implemented"))) + StdioLayer, + ChildProcessSpawnerLayer ) describe("Command arguments", () => { diff --git a/packages/effect/test/unstable/cli/Param.test.ts b/packages/effect/test/unstable/cli/Param.test.ts index 42f38f3b70..9e69900743 100644 --- a/packages/effect/test/unstable/cli/Param.test.ts +++ b/packages/effect/test/unstable/cli/Param.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest" -import { Config, ConfigProvider, Effect, FileSystem, Layer, Option, Path, Ref } from "effect" +import { Config, ConfigProvider, Effect, FileSystem, Layer, Option, Path, Ref, Stdio } from "effect" import { TestConsole } from "effect/testing" import { Argument, CliError, Flag, Prompt } from "effect/unstable/cli" import { ChildProcessSpawner } from "effect/unstable/process" @@ -9,13 +9,19 @@ const ConsoleLayer = TestConsole.layer const FileSystemLayer = FileSystem.layerNoop({}) const PathLayer = Path.layer const TerminalLayer = MockTerminal.layer +const StdioLayer = Stdio.layerTest({}) +const ChildProcessSpawnerLayer = Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make(() => Effect.die("Not implemented")) +) const TestLayer = Layer.mergeAll( ConsoleLayer, FileSystemLayer, PathLayer, TerminalLayer, - Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.die("Not implemented"))) + StdioLayer, + ChildProcessSpawnerLayer ) describe("Param", () => { diff --git a/packages/effect/test/unstable/cli/Primitive.test.ts b/packages/effect/test/unstable/cli/Primitive.test.ts index ddbc8e9ebf..0b83b1ee63 100644 --- a/packages/effect/test/unstable/cli/Primitive.test.ts +++ b/packages/effect/test/unstable/cli/Primitive.test.ts @@ -1,13 +1,27 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect, FileSystem, Layer, Path, PlatformError, Redacted } from "effect" +import { Effect, FileSystem, Layer, Path, PlatformError, Redacted, Stdio } from "effect" +import { TestConsole } from "effect/testing/index" import { Primitive } from "effect/unstable/cli" +import { ChildProcessSpawner } from "effect/unstable/process" +import * as MockTerminal from "./services/MockTerminal.ts" +const ConsoleLayer = TestConsole.layer const FileSystemLayer = FileSystem.layerNoop({}) const PathLayer = Path.layer +const TerminalLayer = MockTerminal.layer +const StdioLayer = Stdio.layerTest({}) +const ChildProcessSpawnerLayer = Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make(() => Effect.die("Not implemented")) +) const TestLayer = Layer.mergeAll( + ConsoleLayer, FileSystemLayer, - PathLayer + PathLayer, + TerminalLayer, + StdioLayer, + ChildProcessSpawnerLayer ) // Helper functions to reduce repetition