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

Commit

Permalink
implement withDefault for Args
Browse files Browse the repository at this point in the history
  • Loading branch information
IMax153 committed Nov 20, 2023
1 parent 714fe74 commit 0807e32
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 66 deletions.
5 changes: 5 additions & 0 deletions .changeset/rude-lies-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/cli": patch
---

implement withDefault for Args
9 changes: 9 additions & 0 deletions src/Args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,15 @@ export const validate: {
): Effect<FileSystem, ValidationError, [ReadonlyArray<string>, A]>
} = InternalArgs.validate

/**
* @since 1.0.0
* @category combinators
*/
export const withDefault: {
<A>(fallback: A): (self: Args<A>) => Args<A>
<A>(self: Args<A>, fallback: A): Args<A>
} = InternalArgs.withDefault

/**
* @since 1.0.0
* @category combinators
Expand Down
154 changes: 88 additions & 66 deletions src/internal/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import * as InternalHelpDoc from "./helpDoc.js"
import * as InternalSpan from "./helpDoc/span.js"
import * as InternalPrimitive from "./primitive.js"
import * as InternalNumberPrompt from "./prompt/number.js"
import * as InternalTogglePrompt from "./prompt/toggle.js"
import * as InternalSelectPrompt from "./prompt/select.js"
import * as InternalRegularLanguage from "./regularLanguage.js"
import * as InternalUsage from "./usage.js"
import * as InternalValidationError from "./validationError.js"
Expand Down Expand Up @@ -51,8 +51,8 @@ export type Instruction =
| Single
| Map
| Both
| Optional
| Variadic
| WithDefault

/** @internal */
export interface Empty extends Op<"Empty", {}> {}
Expand Down Expand Up @@ -84,18 +84,19 @@ export interface Both extends
{}

/** @internal */
export interface Optional extends
Op<"Optional", {
export interface Variadic extends
Op<"Variadic", {
readonly args: Args.Args<unknown>
readonly min: Option.Option<number>
readonly max: Option.Option<number>
}>
{}

/** @internal */
export interface Variadic extends
Op<"Variadic", {
export interface WithDefault extends
Op<"WithDefault", {
readonly args: Args.Args<unknown>
readonly min: Option.Option<number>
readonly max: Option.Option<number>
readonly fallback: unknown
}>
{}

Expand Down Expand Up @@ -301,7 +302,7 @@ export const mapTryCatch = dual<

/** @internal */
export const optional = <A>(self: Args.Args<A>): Args.Args<Option.Option<A>> =>
makeOptional(self as Instruction)
makeWithDefault(self as Instruction, Option.none())

/** @internal */
export const repeated = <A>(self: Args.Args<A>): Args.Args<ReadonlyArray<A>> =>
Expand Down Expand Up @@ -333,6 +334,12 @@ export const validate = dual<
>
>(3, (self, args, config) => validateInternal(self as Instruction, args, config))

/** @internal */
export const withDefault = dual<
<A>(fallback: A) => (self: Args.Args<A>) => Args.Args<A>,
<A>(self: Args.Args<A>, fallback: A) => Args.Args<A>
>(2, (self, fallback) => makeWithDefault(self, fallback))

/** @internal */
export const withDescription = dual<
(description: string) => <A>(self: Args.Args<A>) => Args.Args<A>,
Expand Down Expand Up @@ -399,15 +406,6 @@ const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => {
getHelpInternal(self.right as Instruction)
)
}
case "Optional": {
return InternalHelpDoc.mapDescriptionList(
getHelpInternal(self.args as Instruction),
(span, block) => {
const optionalDescription = InternalHelpDoc.p("This setting is optional.")
return [span, InternalHelpDoc.sequence(block, optionalDescription)]
}
)
}
case "Variadic": {
const help = getHelpInternal(self.args as Instruction)
return InternalHelpDoc.mapDescriptionList(help, (oldSpan, oldBlock) => {
Expand All @@ -426,6 +424,21 @@ const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => {
return [InternalSpan.concat(oldSpan, newSpan), InternalHelpDoc.sequence(oldBlock, newBlock)]
})
}
case "WithDefault": {
return InternalHelpDoc.mapDescriptionList(
getHelpInternal(self.args as Instruction),
(span, block) => {
const optionalDescription = Option.isOption(self.fallback)
? Option.match(self.fallback, {
onNone: () => InternalHelpDoc.p("This setting is optional."),
onSome: () =>
InternalHelpDoc.p(`This setting is optional. Defaults to: ${self.fallback}`)
})
: InternalHelpDoc.p("This setting is optional.")
return [span, InternalHelpDoc.sequence(block, optionalDescription)]
}
)
}
}
}

Expand All @@ -438,8 +451,8 @@ const getIdentifierInternal = (self: Instruction): Option.Option<string> => {
return Option.some(self.name)
}
case "Map":
case "Optional":
case "Variadic": {
case "Variadic":
case "WithDefault": {
return getIdentifierInternal(self.args as Instruction)
}
case "Both": {
Expand All @@ -458,7 +471,7 @@ const getIdentifierInternal = (self: Instruction): Option.Option<string> => {
const getMinSizeInternal = (self: Instruction): number => {
switch (self._tag) {
case "Empty":
case "Optional": {
case "WithDefault": {
return 0
}
case "Single": {
Expand Down Expand Up @@ -488,7 +501,7 @@ const getMaxSizeInternal = (self: Instruction): number => {
return 1
}
case "Map":
case "Optional": {
case "WithDefault": {
return getMaxSizeInternal(self.args as Instruction)
}
case "Both": {
Expand Down Expand Up @@ -523,12 +536,12 @@ const getUsageInternal = (self: Instruction): Usage.Usage => {
getUsageInternal(self.right as Instruction)
)
}
case "Optional": {
return InternalUsage.optional(getUsageInternal(self.args as Instruction))
}
case "Variadic": {
return InternalUsage.repeated(getUsageInternal(self.args as Instruction))
}
case "WithDefault": {
return InternalUsage.optional(getUsageInternal(self.args as Instruction))
}
}
}

Expand Down Expand Up @@ -565,10 +578,14 @@ const makeBoth = <A, B>(left: Args.Args<A>, right: Args.Args<B>): Args.Args<[A,
return op
}

const makeOptional = <A>(self: Args.Args<A>): Args.Args<Option.Option<A>> => {
const makeWithDefault = <A>(
self: Args.Args<A>,
fallback: A
): Args.Args<A> => {
const op = Object.create(proto)
op._tag = "Optional"
op._tag = "WithDefault"
op.args = self
op.fallback = fallback
return op
}

Expand Down Expand Up @@ -602,15 +619,17 @@ const toRegularLanguageInternal = (self: Instruction): RegularLanguage.RegularLa
toRegularLanguageInternal(self.right as Instruction)
)
}
case "Optional": {
return InternalRegularLanguage.optional(toRegularLanguageInternal(self.args as Instruction))
}
case "Variadic": {
return InternalRegularLanguage.repeated(toRegularLanguageInternal(self.args as Instruction), {
min: Option.getOrUndefined(self.min),
max: Option.getOrUndefined(self.max)
})
}
case "WithDefault": {
return InternalRegularLanguage.optional(
toRegularLanguageInternal(self.args as Instruction)
)
}
}
}

Expand Down Expand Up @@ -681,16 +700,6 @@ const validateInternal = (
)
)
}
case "Optional": {
return validateInternal(self.args as Instruction, args, config).pipe(
Effect.map(([args, value]) => [args, Option.some(value)] as [ReadonlyArray<string>, any]),
Effect.catchTag("MissingValue", () =>
Effect.succeed<[ReadonlyArray<string>, any]>([
args,
Option.none()
]))
)
}
case "Variadic": {
const min1 = Option.getOrElse(self.min, () => 0)
const max1 = Option.getOrElse(self.max, () => Number.MAX_SAFE_INTEGER)
Expand All @@ -717,6 +726,15 @@ const validateInternal = (
Effect.map(([args, acc]) => [args, acc])
)
}
case "WithDefault": {
return validateInternal(self.args as Instruction, args, config).pipe(
Effect.catchTag("MissingValue", () =>
Effect.succeed<[ReadonlyArray<string>, any]>([
args,
self.fallback
]))
)
}
}
}

Expand All @@ -738,16 +756,19 @@ const withDescriptionInternal = (self: Instruction, description: string): Args.A
withDescriptionInternal(self.right as Instruction, description)
)
}
case "Optional": {
return makeOptional(withDescriptionInternal(self.args as Instruction, description))
}
case "Variadic": {
return makeVariadic(
withDescriptionInternal(self.args as Instruction, description),
self.min,
self.max
)
}
case "WithDefault": {
return makeWithDefault(
withDescriptionInternal(self.args as Instruction, description),
self.fallback
)
}
}
}

Expand Down Expand Up @@ -785,29 +806,6 @@ const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect.
(left, right) => ReadonlyArray.appendAll(left, right)
).pipe(Effect.tap((args) => validateInternal(self, args, config)))
}
case "Optional": {
const defaultHelp = InternalHelpDoc.p(`This argument is optional - specify a value?`)
const message = pipe(
wizardHeader,
InternalHelpDoc.sequence(getHelpInternal(self.args as Instruction)),
InternalHelpDoc.sequence(defaultHelp)
)
return Console.log().pipe(
Effect.zipRight(
InternalTogglePrompt.toggle({
message: InternalHelpDoc.toAnsiText(message).trimEnd(),
initial: true,
active: "yes",
inactive: "no"
})
),
Effect.flatMap((specifyValue) =>
specifyValue
? wizardInternal(self.args as Instruction, config)
: Effect.succeed(ReadonlyArray.empty())
)
)
}
case "Variadic": {
const repeatHelp = InternalHelpDoc.p(
"How many times should this argument should be repeated?"
Expand Down Expand Up @@ -837,5 +835,29 @@ const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect.
)
)
}
case "WithDefault": {
const defaultHelp = InternalHelpDoc.p(`This argument is optional - use the default?`)
const message = pipe(
wizardHeader,
InternalHelpDoc.sequence(getHelpInternal(self.args as Instruction)),
InternalHelpDoc.sequence(defaultHelp)
)
return Console.log().pipe(
Effect.zipRight(
InternalSelectPrompt.select({
message: InternalHelpDoc.toAnsiText(message).trimEnd(),
choices: [
{ title: `Default ['${JSON.stringify(self.fallback)}']`, value: true },
{ title: "Custom", value: false }
]
})
),
Effect.flatMap((useFallback) =>
useFallback
? Effect.succeed(ReadonlyArray.empty())
: wizardInternal(self.args as Instruction, config)
)
)
}
}
}
18 changes: 18 additions & 0 deletions test/Args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ const runEffect = <E, A>(
): Promise<A> => Effect.provide(self, NodeContext.layer).pipe(Effect.runPromise)

describe("Args", () => {
it("validates an valid argument with a default", () =>
Effect.gen(function*(_) {
const args = Args.integer().pipe(Args.withDefault(0))
const result = yield* _(
Args.validate(args, ReadonlyArray.empty(), CliConfig.defaultConfig)
)
expect(result).toEqual([ReadonlyArray.empty(), 0])
}).pipe(runEffect))

it("does not validate an invalid argument even when there is a default", () =>
Effect.gen(function*(_) {
const args = Args.integer().pipe(Args.withDefault(0))
const result = yield* _(Effect.flip(
Args.validate(args, ReadonlyArray.of("abc"), CliConfig.defaultConfig)
))
expect(result).toEqual(ValidationError.invalidArgument(HelpDoc.p("'abc' is not a integer")))
}).pipe(runEffect))

it("should validate an existing file that is expected to exist", () =>
Effect.gen(function*(_) {
const path = yield* _(Path.Path)
Expand Down

0 comments on commit 0807e32

Please sign in to comment.