From 7838cbdb4f4e874dc8c62f2cf54558c5a8593507 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 2 Dec 2023 11:39:09 +1300 Subject: [PATCH 1/6] add Args/Options.withFallbackConfig --- .changeset/violet-berries-appear.md | 5 ++ examples/minigit.ts | 14 +++- src/Options.ts | 11 +++ src/internal/args.ts | 98 ++++++++++++++++++++-- src/internal/options.ts | 121 ++++++++++++++++++++++++---- 5 files changed, 225 insertions(+), 24 deletions(-) create mode 100644 .changeset/violet-berries-appear.md diff --git a/.changeset/violet-berries-appear.md b/.changeset/violet-berries-appear.md new file mode 100644 index 0000000..6fb6ee4 --- /dev/null +++ b/.changeset/violet-berries-appear.md @@ -0,0 +1,5 @@ +--- +"@effect/cli": patch +--- + +add Args/Options.withFallbackConfig diff --git a/examples/minigit.ts b/examples/minigit.ts index 046b1dd..e25b942 100644 --- a/examples/minigit.ts +++ b/examples/minigit.ts @@ -1,6 +1,6 @@ import { Args, Command, Options } from "@effect/cli" import { NodeContext, Runtime } from "@effect/platform-node" -import { Console, Effect, Option, ReadonlyArray } from "effect" +import { Config, ConfigProvider, Console, Effect, Option, ReadonlyArray } from "effect" // minigit [--version] [-h | --help] [-c =] const configs = Options.keyValueMap("c").pipe(Options.optional) @@ -17,7 +17,11 @@ const minigit = Command.make("minigit", { configs }, ({ configs }) => // minigit add [-v | --verbose] [--] [...] const pathspec = Args.text({ name: "pathspec" }).pipe(Args.repeated) -const verbose = Options.boolean("verbose").pipe(Options.withAlias("v")) +const verbose = Options.boolean("verbose", { + fallbackConfig: Config.boolean("VERBOSE") +}).pipe( + Options.withAlias("v") +) const minigitAdd = Command.make("add", { pathspec, verbose }, ({ pathspec, verbose }) => { const paths = ReadonlyArray.match(pathspec, { onEmpty: () => "", @@ -29,7 +33,10 @@ const minigitAdd = Command.make("add", { pathspec, verbose }, ({ pathspec, verbo // minigit clone [--depth ] [--] [] const repository = Args.text({ name: "repository" }) const directory = Args.directory().pipe(Args.optional) -const depth = Options.integer("depth").pipe(Options.optional) +const depth = Options.integer("depth").pipe( + Options.withFallbackConfig(Config.integer("DEPTH")), + Options.optional +) const minigitClone = Command.make( "clone", { repository, directory, depth }, @@ -58,6 +65,7 @@ const cli = Command.run(command, { }) Effect.suspend(() => cli(process.argv.slice(2))).pipe( + Effect.withConfigProvider(ConfigProvider.nested(ConfigProvider.fromEnv(), "GIT")), Effect.provide(NodeContext.layer), Runtime.runMain ) diff --git a/src/Options.ts b/src/Options.ts index 54d01d7..56118e4 100644 --- a/src/Options.ts +++ b/src/Options.ts @@ -3,6 +3,7 @@ */ import type { FileSystem } from "@effect/platform/FileSystem" import type { QuitException, Terminal } from "@effect/platform/Terminal" +import type { Config } from "effect/Config" import type { Effect } from "effect/Effect" import type { Either } from "effect/Either" import type { HashMap } from "effect/HashMap" @@ -56,6 +57,7 @@ export declare namespace Options { readonly ifPresent?: boolean readonly negationNames?: NonEmptyReadonlyArray readonly aliases?: NonEmptyReadonlyArray + readonly fallbackConfig?: Config } /** @@ -451,6 +453,15 @@ export const withDefault: { (self: Options, fallback: B): Options } = InternalOptions.withDefault +/** + * @since 1.0.0 + * @category combinators + */ +export const withFallbackConfig: { + (config: Config): (self: Options) => Options + (self: Options, config: Config): Options +} = InternalOptions.withFallbackConfig + /** * @since 1.0.0 * @category combinators diff --git a/src/internal/args.ts b/src/internal/args.ts index 3c4d559..48cf8fd 100644 --- a/src/internal/args.ts +++ b/src/internal/args.ts @@ -1,5 +1,6 @@ import type * as FileSystem from "@effect/platform/FileSystem" import type * as Terminal from "@effect/platform/Terminal" +import type * as Config from "effect/Config" import * as Console from "effect/Console" import * as Effect from "effect/Effect" import * as Either from "effect/Either" @@ -51,6 +52,7 @@ export type Instruction = | Both | Variadic | WithDefault + | WithFallbackConfig /** @internal */ export interface Empty extends Op<"Empty", {}> {} @@ -98,6 +100,14 @@ export interface WithDefault extends }> {} +/** @internal */ +export interface WithFallbackConfig extends + Op<"WithFallbackConfig", { + readonly args: Args.Args + readonly config: Config.Config + }> +{} + // ============================================================================= // Refinements // ============================================================================= @@ -333,6 +343,12 @@ export const withDefault = dual< (self: Args.Args, fallback: B) => Args.Args >(2, (self, fallback) => makeWithDefault(self, fallback)) +/** @internal */ +export const withFallbackConfig = dual< + (config: Config.Config) => (self: Args.Args) => Args.Args, + (self: Args.Args, config: Config.Config) => Args.Args +>(2, (self, config) => makeWithFallbackConfig(self, config)) + /** @internal */ export const withDescription = dual< (description: string) => (self: Args.Args) => Args.Args, @@ -432,6 +448,20 @@ const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => { } ) } + case "WithFallbackConfig": { + return InternalHelpDoc.mapDescriptionList( + getHelpInternal(self.args as Instruction), + (span, block) => [ + span, + InternalHelpDoc.sequence( + block, + InternalHelpDoc.p( + "This setting is optional. Defaults to loading from environment variables." + ) + ) + ] + ) + } } } @@ -445,7 +475,8 @@ const getIdentifierInternal = (self: Instruction): Option.Option => { } case "Map": case "Variadic": - case "WithDefault": { + case "WithDefault": + case "WithFallbackConfig": { return getIdentifierInternal(self.args as Instruction) } case "Both": { @@ -464,7 +495,8 @@ const getIdentifierInternal = (self: Instruction): Option.Option => { const getMinSizeInternal = (self: Instruction): number => { switch (self._tag) { case "Empty": - case "WithDefault": { + case "WithDefault": + case "WithFallbackConfig": { return 0 } case "Single": { @@ -494,7 +526,8 @@ const getMaxSizeInternal = (self: Instruction): number => { return 1 } case "Map": - case "WithDefault": { + case "WithDefault": + case "WithFallbackConfig": { return getMaxSizeInternal(self.args as Instruction) } case "Both": { @@ -532,7 +565,8 @@ const getUsageInternal = (self: Instruction): Usage.Usage => { case "Variadic": { return InternalUsage.repeated(getUsageInternal(self.args as Instruction)) } - case "WithDefault": { + case "WithDefault": + case "WithFallbackConfig": { return InternalUsage.optional(getUsageInternal(self.args as Instruction)) } } @@ -582,6 +616,17 @@ const makeWithDefault = ( return op } +const makeWithFallbackConfig = ( + options: Args.Args, + config: Config.Config +): Args.Args => { + const op = Object.create(proto) + op._tag = "WithFallbackConfig" + op.options = options + op.config = config + return op +} + const makeVariadic = ( args: Args.Args, min: Option.Option, @@ -699,6 +744,15 @@ const validateInternal = ( ])) ) } + case "WithFallbackConfig": { + return validateInternal(self.args as Instruction, args, config).pipe( + Effect.catchTag("MissingValue", (e) => + Effect.map( + Effect.mapError(Effect.config(self.config), () => e), + (value) => [args, value] as [ReadonlyArray, any] + )) + ) + } } } @@ -733,6 +787,12 @@ const withDescriptionInternal = (self: Instruction, description: string): Args.A self.fallback ) } + case "WithFallbackConfig": { + return makeWithFallbackConfig( + withDescriptionInternal(self.args as Instruction, description), + self.config + ) + } } } @@ -816,6 +876,27 @@ const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect. ) ) } + case "WithFallbackConfig": { + const defaultHelp = InternalHelpDoc.p(`Try load this option from the environment?`) + const message = pipe( + getHelpInternal(self.args as Instruction), + InternalHelpDoc.sequence(defaultHelp) + ) + return InternalSelectPrompt.select({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + choices: [ + { title: `Use environment variables`, value: true }, + { title: "Custom", value: false } + ] + }).pipe( + Effect.zipLeft(Console.log()), + Effect.flatMap((useFallback) => + useFallback + ? Effect.succeed(ReadonlyArray.empty()) + : wizardInternal(self.args as Instruction, config) + ) + ) + } } } @@ -834,7 +915,8 @@ const getShortDescription = (self: Instruction): string => { } case "Map": case "Variadic": - case "WithDefault": { + case "WithDefault": + case "WithFallbackConfig": { return getShortDescription(self.args as Instruction) } } @@ -869,7 +951,8 @@ export const getFishCompletions = (self: Instruction): ReadonlyArray => } case "Map": case "Variadic": - case "WithDefault": { + case "WithDefault": + case "WithFallbackConfig": { return getFishCompletions(self.args as Instruction) } } @@ -913,7 +996,8 @@ export const getZshCompletions = ( ? getZshCompletions(self.args as Instruction, { ...state, multiple: true }) : getZshCompletions(self.args as Instruction, state) } - case "WithDefault": { + case "WithDefault": + case "WithFallbackConfig": { return getZshCompletions(self.args as Instruction, { ...state, optional: true }) } } diff --git a/src/internal/options.ts b/src/internal/options.ts index b5fe78f..8effdc8 100644 --- a/src/internal/options.ts +++ b/src/internal/options.ts @@ -1,5 +1,6 @@ import type * as FileSystem from "@effect/platform/FileSystem" import type * as Terminal from "@effect/platform/Terminal" +import type * as Config from "effect/Config" import * as Console from "effect/Console" import * as Effect from "effect/Effect" import * as Either from "effect/Either" @@ -56,6 +57,7 @@ export type Instruction = | Map | Both | OrElse + | WithFallbackConfig | Variadic | WithDefault @@ -109,6 +111,14 @@ export interface OrElse extends }> {} +/** @internal */ +export interface WithFallbackConfig extends + Op<"WithFallbackConfig", { + readonly options: Options.Options + readonly config: Config.Config + }> +{} + /** @internal */ export interface Variadic extends Op<"Variadic", { @@ -214,9 +224,18 @@ export const boolean = ( tail, InternalPrimitive.boolean(Option.some(!ifPresent)) ) - return withDefault(orElse(option, negationOption), !ifPresent) + return withDefault( + orElse( + options.fallbackConfig ? withFallbackConfig(option, options.fallbackConfig) : option, + negationOption + ), + !ifPresent + ) } - return withDefault(option, !ifPresent) + return withDefault( + options.fallbackConfig ? withFallbackConfig(option, options.fallbackConfig) : option, + !ifPresent + ) } /** @internal */ @@ -519,6 +538,12 @@ export const withDefault = dual< (self: Options.Options, fallback: B) => Options.Options >(2, (self, fallback) => makeWithDefault(self, fallback)) +/** @internal */ +export const withFallbackConfig = dual< + (config: Config.Config) => (self: Options.Options) => Options.Options, + (self: Options.Options, config: Config.Config) => Options.Options +>(2, (self, config) => makeWithFallbackConfig(self, config)) + /** @internal */ export const withDescription = dual< (description: string) => (self: Options.Options) => Options.Options, @@ -667,6 +692,20 @@ const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => { } ) } + case "WithFallbackConfig": { + return InternalHelpDoc.mapDescriptionList( + getHelpInternal(self.options as Instruction), + (span, block) => [ + span, + InternalHelpDoc.sequence( + block, + InternalHelpDoc.p( + "This setting is optional. Defaults to loading from environment variables." + ) + ) + ] + ) + } } } @@ -694,6 +733,7 @@ const getIdentifierInternal = (self: Instruction): Option.Option => { return getIdentifierInternal(self.argumentOption as Instruction) } case "Map": + case "WithFallbackConfig": case "WithDefault": { return getIdentifierInternal(self.options as Instruction) } @@ -703,7 +743,8 @@ const getIdentifierInternal = (self: Instruction): Option.Option => { const getMinSizeInternal = (self: Instruction): number => { switch (self._tag) { case "Empty": - case "WithDefault": { + case "WithDefault": + case "WithFallbackConfig": { return 0 } case "Single": @@ -742,7 +783,9 @@ const getMaxSizeInternal = (self: Instruction): number => { case "KeyValueMap": { return Number.MAX_SAFE_INTEGER } - case "Map": { + case "Map": + case "WithDefault": + case "WithFallbackConfig": { return getMaxSizeInternal(self.options as Instruction) } case "Both": { @@ -760,9 +803,6 @@ const getMaxSizeInternal = (self: Instruction): number => { const optionsMaxSize = getMaxSizeInternal(self.argumentOption as Instruction) return Math.floor(selfMaxSize * optionsMaxSize) } - case "WithDefault": { - return getMaxSizeInternal(self.options as Instruction) - } } } @@ -801,7 +841,8 @@ const getUsageInternal = (self: Instruction): Usage.Usage => { case "Variadic": { return InternalUsage.repeated(getUsageInternal(self.argumentOption as Instruction)) } - case "WithDefault": { + case "WithDefault": + case "WithFallbackConfig": { return InternalUsage.optional(getUsageInternal(self.options as Instruction)) } } @@ -917,6 +958,17 @@ const makeWithDefault = ( return op } +const makeWithFallbackConfig = ( + options: Options.Options, + config: Config.Config +): Options.Options => { + const op = Object.create(proto) + op._tag = "WithFallbackConfig" + op.options = options + op.config = config + return op +} + const modifySingle = (self: Instruction, f: (single: Single) => Single): Options.Options => { switch (self._tag) { case "Empty": { @@ -949,6 +1001,9 @@ const modifySingle = (self: Instruction, f: (single: Single) => Single): Options case "WithDefault": { return makeWithDefault(modifySingle(self.options as Instruction, f), self.fallback) } + case "WithFallbackConfig": { + return makeWithFallbackConfig(modifySingle(self.options as Instruction, f), self.config) + } } } @@ -967,7 +1022,8 @@ export const getNames = (self: Instruction): ReadonlyArray => { return loop(self.argumentOption as Instruction) } case "Map": - case "WithDefault": { + case "WithDefault": + case "WithFallbackConfig": { return loop(self.options as Instruction) } case "Both": @@ -1001,7 +1057,8 @@ const toParseableInstruction = (self: Instruction): ReadonlyArray Effect.succeed(self.fallback)) ) } + case "WithFallbackConfig": { + return parseInternal(self.options as Instruction, args, config).pipe( + Effect.catchTag( + "MissingValue", + (e) => Effect.mapError(Effect.config(self.config), () => e) + ) + ) + } } } @@ -1293,6 +1358,30 @@ const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect. ) ) } + case "WithFallbackConfig": { + if (isBoolInternal(self.options as Instruction)) { + return wizardInternal(self.options as Instruction, config) + } + const defaultHelp = InternalHelpDoc.p(`Try load this option from the environment?`) + const message = pipe( + getHelpInternal(self.options as Instruction), + InternalHelpDoc.sequence(defaultHelp) + ) + return InternalSelectPrompt.select({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + choices: [ + { title: `Use environment variables`, value: true }, + { title: "Custom", value: false } + ] + }).pipe( + Effect.zipLeft(Console.log()), + Effect.flatMap((useFallback) => + useFallback + ? Effect.succeed(ReadonlyArray.empty()) + : wizardInternal(self.options as Instruction, config) + ) + ) + } } } @@ -1730,7 +1819,8 @@ const getShortDescription = (self: Instruction): string => { return getShortDescription(self.argumentOption as Instruction) } case "Map": - case "WithDefault": { + case "WithDefault": + case "WithFallbackConfig": { return getShortDescription(self.options as Instruction) } } @@ -1760,7 +1850,8 @@ export const getBashCompletions = (self: Instruction): ReadonlyArray => return getBashCompletions(self.argumentOption as Instruction) } case "Map": - case "WithDefault": { + case "WithDefault": + case "WithFallbackConfig": { return getBashCompletions(self.options as Instruction) } case "Both": @@ -1803,7 +1894,8 @@ export const getFishCompletions = (self: Instruction): ReadonlyArray => return getFishCompletions(self.argumentOption as Instruction) } case "Map": - case "WithDefault": { + case "WithDefault": + case "WithFallbackConfig": { return getFishCompletions(self.options as Instruction) } case "Both": @@ -1849,7 +1941,8 @@ export const getZshCompletions = ( return getZshCompletions(self.argumentOption as Instruction, { ...state, multiple: true }) } case "Map": - case "WithDefault": { + case "WithDefault": + case "WithFallbackConfig": { return getZshCompletions(self.options as Instruction, state) } case "Both": { From fa84f60b8b8bb2adad3db43faa3931552dfdb46c Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Sat, 2 Dec 2023 14:21:40 +1300 Subject: [PATCH 2/6] traverse WithDefault --- examples/minigit.ts | 7 +++---- src/Options.ts | 1 - src/internal/args.ts | 16 ++++++++++++++-- src/internal/options.ts | 26 ++++++++++++++++---------- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/examples/minigit.ts b/examples/minigit.ts index e25b942..e5397f9 100644 --- a/examples/minigit.ts +++ b/examples/minigit.ts @@ -17,10 +17,9 @@ const minigit = Command.make("minigit", { configs }, ({ configs }) => // minigit add [-v | --verbose] [--] [...] const pathspec = Args.text({ name: "pathspec" }).pipe(Args.repeated) -const verbose = Options.boolean("verbose", { - fallbackConfig: Config.boolean("VERBOSE") -}).pipe( - Options.withAlias("v") +const verbose = Options.boolean("verbose").pipe( + Options.withAlias("v"), + Options.withFallbackConfig(Config.boolean("VERBOSE")) ) const minigitAdd = Command.make("add", { pathspec, verbose }, ({ pathspec, verbose }) => { const paths = ReadonlyArray.match(pathspec, { diff --git a/src/Options.ts b/src/Options.ts index 56118e4..d1e999a 100644 --- a/src/Options.ts +++ b/src/Options.ts @@ -57,7 +57,6 @@ export declare namespace Options { readonly ifPresent?: boolean readonly negationNames?: NonEmptyReadonlyArray readonly aliases?: NonEmptyReadonlyArray - readonly fallbackConfig?: Config } /** diff --git a/src/internal/args.ts b/src/internal/args.ts index 48cf8fd..d1b9018 100644 --- a/src/internal/args.ts +++ b/src/internal/args.ts @@ -344,10 +344,22 @@ export const withDefault = dual< >(2, (self, fallback) => makeWithDefault(self, fallback)) /** @internal */ -export const withFallbackConfig = dual< +export const withFallbackConfig: { + (config: Config.Config): (self: Args.Args) => Args.Args + (self: Args.Args, config: Config.Config): Args.Args +} = dual< (config: Config.Config) => (self: Args.Args) => Args.Args, (self: Args.Args, config: Config.Config) => Args.Args ->(2, (self, config) => makeWithFallbackConfig(self, config)) +>(2, (self, config) => { + if ((self as Instruction)._tag === "WithDefault") { + const withDefault = self as WithDefault + return makeWithDefault( + withFallbackConfig(withDefault.args, config), + withDefault.fallback as any + ) + } + return makeWithFallbackConfig(self, config) +}) /** @internal */ export const withDescription = dual< diff --git a/src/internal/options.ts b/src/internal/options.ts index 8effdc8..983e041 100644 --- a/src/internal/options.ts +++ b/src/internal/options.ts @@ -225,17 +225,11 @@ export const boolean = ( InternalPrimitive.boolean(Option.some(!ifPresent)) ) return withDefault( - orElse( - options.fallbackConfig ? withFallbackConfig(option, options.fallbackConfig) : option, - negationOption - ), + orElse(option, negationOption), !ifPresent ) } - return withDefault( - options.fallbackConfig ? withFallbackConfig(option, options.fallbackConfig) : option, - !ifPresent - ) + return withDefault(option, !ifPresent) } /** @internal */ @@ -539,10 +533,22 @@ export const withDefault = dual< >(2, (self, fallback) => makeWithDefault(self, fallback)) /** @internal */ -export const withFallbackConfig = dual< +export const withFallbackConfig: { + (config: Config.Config): (self: Options.Options) => Options.Options + (self: Options.Options, config: Config.Config): Options.Options +} = dual< (config: Config.Config) => (self: Options.Options) => Options.Options, (self: Options.Options, config: Config.Config) => Options.Options ->(2, (self, config) => makeWithFallbackConfig(self, config)) +>(2, (self, config) => { + if ((self as Instruction)._tag === "WithDefault") { + const withDefault = self as WithDefault + return makeWithDefault( + withFallbackConfig(withDefault.options, config), + withDefault.fallback as any + ) + } + return makeWithFallbackConfig(self, config) +}) /** @internal */ export const withDescription = dual< From 54015aedef62340d76afe508d7ceddc61c9e0d24 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 2 Dec 2023 16:10:42 +1300 Subject: [PATCH 3/6] update WithFallbackConfig messages --- src/internal/args.ts | 2 +- src/internal/options.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/internal/args.ts b/src/internal/args.ts index d1b9018..f69c739 100644 --- a/src/internal/args.ts +++ b/src/internal/args.ts @@ -468,7 +468,7 @@ const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => { InternalHelpDoc.sequence( block, InternalHelpDoc.p( - "This setting is optional. Defaults to loading from environment variables." + "This argument can be set from environment variables." ) ) ] diff --git a/src/internal/options.ts b/src/internal/options.ts index 983e041..c060a53 100644 --- a/src/internal/options.ts +++ b/src/internal/options.ts @@ -706,7 +706,7 @@ const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => { InternalHelpDoc.sequence( block, InternalHelpDoc.p( - "This setting is optional. Defaults to loading from environment variables." + "This option can be set from environment variables." ) ) ] From 12e4a85100508c8d2100f79a3c024a8671e65170 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 2 Dec 2023 20:36:45 +1300 Subject: [PATCH 4/6] add tests --- src/Args.ts | 10 +++++++++ src/internal/args.ts | 5 +++-- test/Command.test.ts | 53 +++++++++++++++++++++++++++++++++++--------- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/Args.ts b/src/Args.ts index b88cb97..b7f2508 100644 --- a/src/Args.ts +++ b/src/Args.ts @@ -3,6 +3,7 @@ */ import type { FileSystem } from "@effect/platform/FileSystem" import type { QuitException, Terminal } from "@effect/platform/Terminal" +import type { Config } from "effect/Config" import type { Effect } from "effect/Effect" import type { Either } from "effect/Either" import type { Option } from "effect/Option" @@ -353,6 +354,15 @@ export const withDefault: { (self: Args, fallback: B): Args } = InternalArgs.withDefault +/** + * @since 1.0.0 + * @category combinators + */ +export const withFallbackConfig: { + (config: Config): (self: Args) => Args + (self: Args, config: Config): Args +} = InternalArgs.withFallbackConfig + /** * @since 1.0.0 * @category combinators diff --git a/src/internal/args.ts b/src/internal/args.ts index f69c739..fcd88dc 100644 --- a/src/internal/args.ts +++ b/src/internal/args.ts @@ -351,6 +351,7 @@ export const withFallbackConfig: { (config: Config.Config) => (self: Args.Args) => Args.Args, (self: Args.Args, config: Config.Config) => Args.Args >(2, (self, config) => { + console.log(self) if ((self as Instruction)._tag === "WithDefault") { const withDefault = self as WithDefault return makeWithDefault( @@ -629,12 +630,12 @@ const makeWithDefault = ( } const makeWithFallbackConfig = ( - options: Args.Args, + args: Args.Args, config: Config.Config ): Args.Args => { const op = Object.create(proto) op._tag = "WithFallbackConfig" - op.options = options + op.args = args op.config = config return op } diff --git a/test/Command.test.ts b/test/Command.test.ts index 1f6eb2d..90a5412 100644 --- a/test/Command.test.ts +++ b/test/Command.test.ts @@ -1,14 +1,19 @@ import { Args, Command, Options } from "@effect/cli" import { NodeContext } from "@effect/platform-node" -import { Context, Effect, Layer } from "effect" +import { Config, ConfigProvider, Context, Effect, Layer } from "effect" import { assert, describe, it } from "vitest" const git = Command.make("git", { - verbose: Options.boolean("verbose").pipe(Options.withAlias("v")) + verbose: Options.boolean("verbose").pipe( + Options.withAlias("v"), + Options.withFallbackConfig(Config.boolean("VERBOSE")) + ) }).pipe(Command.withDescription("the stupid content tracker")) const clone = Command.make("clone", { - repository: Args.text({ name: "repository" }) + repository: Args.text({ name: "repository" }).pipe( + Args.withFallbackConfig(Config.string("REPOSITORY")) + ) }, ({ repository }) => Effect.gen(function*(_) { const { log } = yield* _(Messages) @@ -51,31 +56,57 @@ describe("Command", () => { assert.deepStrictEqual(yield* _(messages.messages), []) }).pipe(Effect.provide(EnvLive), Effect.runPromise)) - it.skip("add", () => + it("add", () => Effect.gen(function*(_) { const messages = yield* _(Messages) yield* _(run(["add", "file"])) yield* _(run(["--verbose", "add", "file"])) - yield* _(run(["add", "--verbose", "file"])) assert.deepStrictEqual(yield* _(messages.messages), [ "Adding", - "Adding file", - "Adding" // TODO: probably should be "Adding repo" + "Adding file" ]) }).pipe(Effect.provide(EnvLive), Effect.runPromise)) - it.skip("clone", () => + it("clone", () => Effect.gen(function*(_) { const messages = yield* _(Messages) yield* _(run(["clone", "repo"])) yield* _(run(["--verbose", "clone", "repo"])) - yield* _(run(["clone", "--verbose", "repo"])) assert.deepStrictEqual(yield* _(messages.messages), [ "Cloning", - "Cloning repo", - "Cloning" // TODO: probably should be "Cloning repo" + "Cloning repo" ]) }).pipe(Effect.provide(EnvLive), Effect.runPromise)) + + it("withFallbackConfig Options boolean", () => + Effect.gen(function*(_) { + const messages = yield* _(Messages) + yield* _(run(["clone", "repo"])) + assert.deepStrictEqual(yield* _(messages.messages), [ + "Cloning repo" + ]) + }).pipe( + Effect.withConfigProvider(ConfigProvider.fromMap( + new Map([["VERBOSE", "true"]]) + )), + Effect.provide(EnvLive), + Effect.runPromise + )) + + it("withFallbackConfig Args", () => + Effect.gen(function*(_) { + const messages = yield* _(Messages) + yield* _(run(["clone"])) + assert.deepStrictEqual(yield* _(messages.messages), [ + "Cloning repo" + ]) + }).pipe( + Effect.withConfigProvider(ConfigProvider.fromMap( + new Map([["VERBOSE", "true"], ["REPOSITORY", "repo"]]) + )), + Effect.provide(EnvLive), + Effect.runPromise + )) }) }) From 4ea06801ef1fe711aed6c588d21ce6c9e70f77b7 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 3 Dec 2023 07:28:06 +1300 Subject: [PATCH 5/6] Update src/internal/args.ts Co-authored-by: Maxwell Brown --- src/internal/args.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/internal/args.ts b/src/internal/args.ts index fcd88dc..3de3190 100644 --- a/src/internal/args.ts +++ b/src/internal/args.ts @@ -351,7 +351,6 @@ export const withFallbackConfig: { (config: Config.Config) => (self: Args.Args) => Args.Args, (self: Args.Args, config: Config.Config) => Args.Args >(2, (self, config) => { - console.log(self) if ((self as Instruction)._tag === "WithDefault") { const withDefault = self as WithDefault return makeWithDefault( From 378f94f0c8af19c98d5820ea2398d74300099fb2 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 3 Dec 2023 08:32:58 +1300 Subject: [PATCH 6/6] use predicates --- .prettierignore | 1 + src/internal/args.ts | 17 +++++++++++++---- src/internal/options.ts | 14 ++++++++++---- 3 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..6461dee --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +*.ts diff --git a/src/internal/args.ts b/src/internal/args.ts index 3de3190..9181829 100644 --- a/src/internal/args.ts +++ b/src/internal/args.ts @@ -116,6 +116,9 @@ export interface WithFallbackConfig extends export const isArgs = (u: unknown): u is Args.Args => typeof u === "object" && u != null && ArgsTypeId in u +/** @internal */ +export const isInstruction = <_>(self: Args.Args<_>): self is Instruction => self as any + /** @internal */ export const isEmpty = (self: Instruction): self is Empty => self._tag === "Empty" @@ -131,6 +134,13 @@ export const isMap = (self: Instruction): self is Map => self._tag === "Map" /** @internal */ export const isVariadic = (self: Instruction): self is Variadic => self._tag === "Variadic" +/** @internal */ +export const isWithDefault = (self: Instruction): self is WithDefault => self._tag === "WithDefault" + +/** @internal */ +export const isWithFallbackConfig = (self: Instruction): self is WithFallbackConfig => + self._tag === "WithFallbackConfig" + // ============================================================================= // Constructors // ============================================================================= @@ -351,11 +361,10 @@ export const withFallbackConfig: { (config: Config.Config) => (self: Args.Args) => Args.Args, (self: Args.Args, config: Config.Config) => Args.Args >(2, (self, config) => { - if ((self as Instruction)._tag === "WithDefault") { - const withDefault = self as WithDefault + if (isInstruction(self) && isWithDefault(self)) { return makeWithDefault( - withFallbackConfig(withDefault.args, config), - withDefault.fallback as any + withFallbackConfig(self.args, config), + self.fallback as any ) } return makeWithFallbackConfig(self, config) diff --git a/src/internal/options.ts b/src/internal/options.ts index c060a53..53b4ca8 100644 --- a/src/internal/options.ts +++ b/src/internal/options.ts @@ -144,6 +144,9 @@ export interface WithDefault extends export const isOptions = (u: unknown): u is Options.Options => typeof u === "object" && u != null && OptionsTypeId in u +/** @internal */ +export const isInstruction = <_>(self: Options.Options<_>): self is Instruction => self as any + /** @internal */ export const isEmpty = (self: Instruction): self is Empty => self._tag === "Empty" @@ -165,6 +168,10 @@ export const isOrElse = (self: Instruction): self is OrElse => self._tag === "Or /** @internal */ export const isWithDefault = (self: Instruction): self is WithDefault => self._tag === "WithDefault" +/** @internal */ +export const isWithFallbackConfig = (self: Instruction): self is WithFallbackConfig => + self._tag === "WithFallbackConfig" + // ============================================================================= // Constructors // ============================================================================= @@ -540,11 +547,10 @@ export const withFallbackConfig: { (config: Config.Config) => (self: Options.Options) => Options.Options, (self: Options.Options, config: Config.Config) => Options.Options >(2, (self, config) => { - if ((self as Instruction)._tag === "WithDefault") { - const withDefault = self as WithDefault + if (isInstruction(self) && isWithDefault(self)) { return makeWithDefault( - withFallbackConfig(withDefault.options, config), - withDefault.fallback as any + withFallbackConfig(self.options, config), + self.fallback as any ) } return makeWithFallbackConfig(self, config)