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

add Args/Options.withFallbackConfig #403

Merged
merged 6 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/violet-berries-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/cli": patch
---

add Args/Options.withFallbackConfig
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.ts
13 changes: 10 additions & 3 deletions examples/minigit.ts
Original file line number Diff line number Diff line change
@@ -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 <name>=<value>]
const configs = Options.keyValueMap("c").pipe(Options.optional)
Expand All @@ -17,7 +17,10 @@ const minigit = Command.make("minigit", { configs }, ({ configs }) =>

// minigit add [-v | --verbose] [--] [<pathspec>...]
const pathspec = Args.text({ name: "pathspec" }).pipe(Args.repeated)
const verbose = Options.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, {
onEmpty: () => "",
Expand All @@ -29,7 +32,10 @@ const minigitAdd = Command.make("add", { pathspec, verbose }, ({ pathspec, verbo
// minigit clone [--depth <depth>] [--] <repository> [<directory>]
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 },
Expand Down Expand Up @@ -58,6 +64,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
)
10 changes: 10 additions & 0 deletions src/Args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -353,6 +354,15 @@ export const withDefault: {
<A, const B>(self: Args<A>, fallback: B): Args<A | B>
} = InternalArgs.withDefault

/**
* @since 1.0.0
* @category combinators
*/
export const withFallbackConfig: {
<B>(config: Config<B>): <A>(self: Args<A>) => Args<B | A>
<A, B>(self: Args<A>, config: Config<B>): Args<A | B>
} = InternalArgs.withFallbackConfig

/**
* @since 1.0.0
* @category combinators
Expand Down
10 changes: 10 additions & 0 deletions src/Options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -451,6 +452,15 @@ export const withDefault: {
<A, const B>(self: Options<A>, fallback: B): Options<A | B>
} = InternalOptions.withDefault

/**
* @since 1.0.0
* @category combinators
*/
export const withFallbackConfig: {
<B>(config: Config<B>): <A>(self: Options<A>) => Options<B | A>
<A, B>(self: Options<A>, config: Config<B>): Options<A | B>
} = InternalOptions.withFallbackConfig

/**
* @since 1.0.0
* @category combinators
Expand Down
119 changes: 112 additions & 7 deletions src/internal/args.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -51,6 +52,7 @@ export type Instruction =
| Both
| Variadic
| WithDefault
| WithFallbackConfig

/** @internal */
export interface Empty extends Op<"Empty", {}> {}
Expand Down Expand Up @@ -98,6 +100,14 @@ export interface WithDefault extends
}>
{}

/** @internal */
export interface WithFallbackConfig extends
Op<"WithFallbackConfig", {
readonly args: Args.Args<unknown>
readonly config: Config.Config<unknown>
}>
{}

// =============================================================================
// Refinements
// =============================================================================
Expand All @@ -106,6 +116,9 @@ export interface WithDefault extends
export const isArgs = (u: unknown): u is Args.Args<unknown> =>
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"

Expand All @@ -121,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
// =============================================================================
Expand Down Expand Up @@ -333,6 +353,23 @@ export const withDefault = dual<
<A, const B>(self: Args.Args<A>, fallback: B) => Args.Args<A | B>
>(2, (self, fallback) => makeWithDefault(self, fallback))

/** @internal */
export const withFallbackConfig: {
<B>(config: Config.Config<B>): <A>(self: Args.Args<A>) => Args.Args<B | A>
<A, B>(self: Args.Args<A>, config: Config.Config<B>): Args.Args<A | B>
} = dual<
<B>(config: Config.Config<B>) => <A>(self: Args.Args<A>) => Args.Args<A | B>,
<A, B>(self: Args.Args<A>, config: Config.Config<B>) => Args.Args<A | B>
>(2, (self, config) => {
if (isInstruction(self) && isWithDefault(self)) {
return makeWithDefault(
withFallbackConfig(self.args, config),
self.fallback as any
)
}
return makeWithFallbackConfig(self, config)
})

/** @internal */
export const withDescription = dual<
(description: string) => <A>(self: Args.Args<A>) => Args.Args<A>,
Expand Down Expand Up @@ -432,6 +469,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 argument can be set from environment variables."
)
)
]
)
}
}
}

Expand All @@ -445,7 +496,8 @@ const getIdentifierInternal = (self: Instruction): Option.Option<string> => {
}
case "Map":
case "Variadic":
case "WithDefault": {
case "WithDefault":
case "WithFallbackConfig": {
return getIdentifierInternal(self.args as Instruction)
}
case "Both": {
Expand All @@ -464,7 +516,8 @@ const getIdentifierInternal = (self: Instruction): Option.Option<string> => {
const getMinSizeInternal = (self: Instruction): number => {
switch (self._tag) {
case "Empty":
case "WithDefault": {
case "WithDefault":
case "WithFallbackConfig": {
return 0
}
case "Single": {
Expand Down Expand Up @@ -494,7 +547,8 @@ const getMaxSizeInternal = (self: Instruction): number => {
return 1
}
case "Map":
case "WithDefault": {
case "WithDefault":
case "WithFallbackConfig": {
return getMaxSizeInternal(self.args as Instruction)
}
case "Both": {
Expand Down Expand Up @@ -532,7 +586,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))
}
}
Expand Down Expand Up @@ -582,6 +637,17 @@ const makeWithDefault = <A, const B>(
return op
}

const makeWithFallbackConfig = <A, B>(
args: Args.Args<A>,
config: Config.Config<B>
): Args.Args<A | B> => {
const op = Object.create(proto)
op._tag = "WithFallbackConfig"
op.args = args
op.config = config
return op
}

const makeVariadic = <A>(
args: Args.Args<A>,
min: Option.Option<number>,
Expand Down Expand Up @@ -699,6 +765,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<string>, any]
))
)
}
}
}

Expand Down Expand Up @@ -733,6 +808,12 @@ const withDescriptionInternal = (self: Instruction, description: string): Args.A
self.fallback
)
}
case "WithFallbackConfig": {
return makeWithFallbackConfig(
withDescriptionInternal(self.args as Instruction, description),
self.config
)
}
}
}

Expand Down Expand Up @@ -816,6 +897,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)
)
)
}
}
}

Expand All @@ -834,7 +936,8 @@ const getShortDescription = (self: Instruction): string => {
}
case "Map":
case "Variadic":
case "WithDefault": {
case "WithDefault":
case "WithFallbackConfig": {
return getShortDescription(self.args as Instruction)
}
}
Expand Down Expand Up @@ -869,7 +972,8 @@ export const getFishCompletions = (self: Instruction): ReadonlyArray<string> =>
}
case "Map":
case "Variadic":
case "WithDefault": {
case "WithDefault":
case "WithFallbackConfig": {
return getFishCompletions(self.args as Instruction)
}
}
Expand Down Expand Up @@ -913,7 +1017,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 })
}
}
Expand Down
Loading
Loading