Skip to content

Commit

Permalink
Introduce Redacted<T> module (#2856)
Browse files Browse the repository at this point in the history
Co-authored-by: maksim.khramtsov <maksim.khramtsov@btsdigital.kz>
Co-authored-by: Tim <hello@timsmart.co>
  • Loading branch information
3 people authored and gcanti committed May 30, 2024
1 parent 98bb035 commit 73d03f1
Show file tree
Hide file tree
Showing 32 changed files with 640 additions and 77 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-zebras-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/cli": patch
---

add .redacted apis to /cli package
7 changes: 7 additions & 0 deletions .changeset/slow-paws-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"effect": minor
---

Introduced `Redacted<out T = string>` module - `Secret` generalization
`Secret extends Redacted`
The use of the `Redacted` has been replaced by the use of the `Redacted` in packages with version `0.*.*`
6 changes: 6 additions & 0 deletions .changeset/sweet-sheep-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@effect/schema": patch
---

Added two related schemas `Redacted` and `RedactedFromSelf`
`Secret` and `SecretFromSelf` marked as deprecated
11 changes: 11 additions & 0 deletions packages/cli/src/Args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Config } from "effect/Config"
import type { Effect } from "effect/Effect"
import type { Option } from "effect/Option"
import type { Pipeable } from "effect/Pipeable"
import type { Redacted } from "effect/Redacted"
import type { Secret } from "effect/Secret"
import type { CliConfig } from "./CliConfig.js"
import type { HelpDoc } from "./HelpDoc.js"
Expand Down Expand Up @@ -376,6 +377,16 @@ export const path: (config?: Args.PathArgsConfig) => Args<string> = InternalArgs
*/
export const repeated: <A>(self: Args<A>) => Args<Array<A>> = InternalArgs.repeated

/**
* Creates a text argument.
*
* Can optionally provide a custom argument name (defaults to `"redacted"`).
*
* @since 1.0.0
* @category constructors
*/
export const redacted: (config?: Args.BaseArgsConfig) => Args<Redacted> = InternalArgs.redacted

/**
* Creates a text argument.
*
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/Options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { Either } from "effect/Either"
import type { HashMap } from "effect/HashMap"
import type { Option } from "effect/Option"
import type { Pipeable } from "effect/Pipeable"
import type { Redacted } from "effect/Redacted"
import type { Secret } from "effect/Secret"
import type { CliConfig } from "./CliConfig.js"
import type { HelpDoc } from "./HelpDoc.js"
Expand Down Expand Up @@ -311,6 +312,13 @@ export const none: Options<void> = InternalOptions.none
* @since 1.0.0
* @category constructors
*/
export const redacted: (name: string) => Options<Redacted> = InternalOptions.redacted

/**
* @since 1.0.0
* @category constructors
* @deprecated
*/
export const secret: (name: string) => Options<Secret> = InternalOptions.secret

/**
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/Prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { QuitException, Terminal, UserInput } from "@effect/platform/Termin
import type { Effect } from "effect/Effect"
import type { Option } from "effect/Option"
import type { Pipeable } from "effect/Pipeable"
import type { Secret } from "effect/Secret"
import type { Redacted } from "effect/Redacted"
import * as InternalPrompt from "./internal/prompt.js"
import * as InternalConfirmPrompt from "./internal/prompt/confirm.js"
import * as InternalDatePrompt from "./internal/prompt/date.js"
Expand Down Expand Up @@ -423,7 +423,7 @@ export const float: (options: Prompt.FloatOptions) => Prompt<number> = InternalN
* @since 1.0.0
* @category constructors
*/
export const hidden: (options: Prompt.TextOptions) => Prompt<Secret> = InternalTextPrompt.hidden
export const hidden: (options: Prompt.TextOptions) => Prompt<Redacted> = InternalTextPrompt.hidden

/**
* @since 1.0.0
Expand All @@ -450,7 +450,7 @@ export const map: {
* @since 1.0.0
* @category constructors
*/
export const password: (options: Prompt.TextOptions) => Prompt<Secret> = InternalTextPrompt.password
export const password: (options: Prompt.TextOptions) => Prompt<Redacted> = InternalTextPrompt.password

/**
* Executes the specified `Prompt`.
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/internal/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as Either from "effect/Either"
import { dual, pipe } from "effect/Function"
import * as Option from "effect/Option"
import { pipeArguments } from "effect/Pipeable"
import type * as Redacted from "effect/Redacted"
import * as Ref from "effect/Ref"
import type * as Secret from "effect/Secret"
import type * as Args from "../Args.js"
Expand Down Expand Up @@ -266,6 +267,11 @@ export const path = (config?: Args.Args.PathArgsConfig): Args.Args<string> =>
InternalPrimitive.path("either", config?.exists || "either")
)

/** @internal */
export const redacted = (
config?: Args.Args.BaseArgsConfig
): Args.Args<Redacted.Redacted> => makeSingle(Option.fromNullable(config?.name), InternalPrimitive.redacted)

/** @internal */
export const secret = (
config?: Args.Args.BaseArgsConfig
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/internal/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as HashMap from "effect/HashMap"
import * as Option from "effect/Option"
import * as Order from "effect/Order"
import { pipeArguments } from "effect/Pipeable"
import type * as Redacted from "effect/Redacted"
import * as Ref from "effect/Ref"
import type * as Secret from "effect/Secret"
import type * as CliConfig from "../CliConfig.js"
Expand Down Expand Up @@ -375,6 +376,10 @@ export const none: Options.Options<void> = (() => {
return op
})()

/** @internal */
export const redacted = (name: string): Options.Options<Redacted.Redacted> =>
makeSingle(name, Arr.empty(), InternalPrimitive.redacted)

/** @internal */
export const secret = (name: string): Options.Options<Secret.Secret> =>
makeSingle(name, Arr.empty(), InternalPrimitive.secret)
Expand Down
36 changes: 34 additions & 2 deletions packages/cli/src/internal/primitive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as Effect from "effect/Effect"
import { dual, pipe } from "effect/Function"
import * as Option from "effect/Option"
import { pipeArguments } from "effect/Pipeable"
import * as EffectRedacted from "effect/Redacted"
import * as EffectSecret from "effect/Secret"
import type * as CliConfig from "../CliConfig.js"
import type * as HelpDoc from "../HelpDoc.js"
Expand Down Expand Up @@ -51,6 +52,7 @@ export type Instruction =
| Integer
| Path
| Secret
| Redacted
| Text

/** @internal */
Expand Down Expand Up @@ -84,6 +86,9 @@ export interface Path extends
}>
{}

/** @internal */
export interface Redacted extends Op<"Redacted", {}> {}

/** @internal */
export interface Secret extends Op<"Secret", {}> {}

Expand Down Expand Up @@ -192,6 +197,13 @@ export const path = (
return op
}

/** @internal */
export const redacted: Primitive.Primitive<EffectRedacted.Redacted> = (() => {
const op = Object.create(proto)
op._tag = "Redacted"
return op
})()

/** @internal */
export const secret: Primitive.Primitive<EffectSecret.Secret> = (() => {
const op = Object.create(proto)
Expand Down Expand Up @@ -269,6 +281,7 @@ const getChoicesInternal = (self: Instruction): Option.Option<string> => {
case "Float":
case "Integer":
case "Path":
case "Redacted":
case "Secret":
case "Text": {
return Option.none()
Expand Down Expand Up @@ -332,7 +345,8 @@ const getHelpInternal = (self: Instruction): Span.Span => {
`('${self.pathType}') and path existence ('${self.pathExists}')`
)
}
case "Secret": {
case "Secret":
case "Redacted": {
return InternalSpan.text("A user-defined piece of text that is confidential.")
}
case "Text": {
Expand Down Expand Up @@ -364,6 +378,9 @@ const getTypeNameInternal = (self: Instruction): string => {
}
return self.pathType
}
case "Redacted": {
return "redacted"
}
case "Secret": {
return "secret"
}
Expand Down Expand Up @@ -444,6 +461,11 @@ const validateInternal = (
)
})
}
case "Redacted": {
return attempt(value, getTypeNameInternal(self), Schema.decodeUnknown(Schema.String)).pipe(
Effect.map((value) => EffectRedacted.make(value))
)
}
case "Secret": {
return attempt(value, getTypeNameInternal(self), Schema.decodeUnknown(Schema.String)).pipe(
Effect.map((value) => EffectSecret.fromString(value))
Expand Down Expand Up @@ -570,8 +592,15 @@ const wizardInternal = (self: Instruction, help: HelpDoc.HelpDoc): Prompt.Prompt
message: InternalHelpDoc.toAnsiText(message).trimEnd()
})
}
case "Redacted": {
const primitiveHelp = InternalHelpDoc.p("Enter some text (value will be redacted)")
const message = InternalHelpDoc.sequence(help, primitiveHelp)
return InternalTextPrompt.hidden({
message: InternalHelpDoc.toAnsiText(message).trimEnd()
})
}
case "Secret": {
const primitiveHelp = InternalHelpDoc.p("Enter some text (value will be hidden)")
const primitiveHelp = InternalHelpDoc.p("Enter some text (value will be redacted)")
const message = InternalHelpDoc.sequence(help, primitiveHelp)
return InternalTextPrompt.hidden({
message: InternalHelpDoc.toAnsiText(message).trimEnd()
Expand Down Expand Up @@ -601,6 +630,7 @@ export const getBashCompletions = (self: Instruction): string => {
case "Float":
case "Integer":
case "Secret":
case "Redacted":
case "Text": {
return "$(compgen -f \"${cur}\")"
}
Expand Down Expand Up @@ -642,6 +672,7 @@ export const getFishCompletions = (self: Instruction): Array<string> => {
case "DateTime":
case "Float":
case "Integer":
case "Redacted":
case "Secret":
case "Text": {
return Arr.make("-r", "-f")
Expand Down Expand Up @@ -721,6 +752,7 @@ export const getZshCompletions = (self: Instruction): string => {
}
}
}
case "Redacted":
case "Secret":
case "Text": {
return ""
Expand Down
10 changes: 5 additions & 5 deletions packages/cli/src/internal/prompt/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as Arr from "effect/Array"
import * as Effect from "effect/Effect"
import { pipe } from "effect/Function"
import * as Option from "effect/Option"
import * as Secret from "effect/Secret"
import * as Redacted from "effect/Redacted"
import type * as Prompt from "../../Prompt.js"
import * as InternalPrompt from "../prompt.js"
import * as InternalPromptAction from "./action.js"
Expand Down Expand Up @@ -297,12 +297,12 @@ const basePrompt = (
}

/** @internal */
export const hidden = (options: Prompt.Prompt.TextOptions): Prompt.Prompt<Secret.Secret> =>
basePrompt(options, "hidden").pipe(InternalPrompt.map(Secret.fromString))
export const hidden = (options: Prompt.Prompt.TextOptions): Prompt.Prompt<Redacted.Redacted> =>
basePrompt(options, "hidden").pipe(InternalPrompt.map(Redacted.make))

/** @internal */
export const password = (options: Prompt.Prompt.TextOptions): Prompt.Prompt<Secret.Secret> =>
basePrompt(options, "password").pipe(InternalPrompt.map(Secret.fromString))
export const password = (options: Prompt.Prompt.TextOptions): Prompt.Prompt<Redacted.Redacted> =>
basePrompt(options, "password").pipe(InternalPrompt.map(Redacted.make))

/** @internal */
export const text = (options: Prompt.Prompt.TextOptions): Prompt.Prompt<string> => basePrompt(options, "text")
10 changes: 10 additions & 0 deletions packages/effect/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as internal from "./internal/config.js"
import type * as LogLevel from "./LogLevel.js"
import type * as Option from "./Option.js"
import type { Predicate, Refinement } from "./Predicate.js"
import type * as Redacted from "./Redacted.js"
import type * as Secret from "./Secret.js"
import type * as Types from "./Types.js"

Expand Down Expand Up @@ -332,9 +333,18 @@ export const repeat: <A>(self: Config<A>) => Config<Array<A>> = internal.repeat
*
* @since 2.0.0
* @category constructors
* @deprecated
*/
export const secret: (name?: string) => Config<Secret.Secret> = internal.secret

/**
* Constructs a config for a redacted value.
*
* @since 2.0.0
* @category constructors
*/
export const redacted: (name?: string) => Config<Redacted.Redacted> = internal.redacted

/**
* Constructs a config for a sequence of values.
*
Expand Down
79 changes: 79 additions & 0 deletions packages/effect/src/Redacted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @since 3.3.0
*/
import type * as Equal from "./Equal.js"
import * as Equivalence from "./Equivalence.js"
import * as redacted_ from "./internal/redacted.js"
import type { Pipeable } from "./Pipeable.js"
import type { Covariant } from "./Types.js"

/**
* @since 3.3.0
* @category symbols
*/
export const RedactedTypeId: unique symbol = redacted_.RedactedTypeId

/**
* @since 3.3.0
* @category symbols
*/
export type RedactedTypeId = typeof RedactedTypeId

/**
* @since 3.3.0
* @category models
*/
export interface Redacted<out A = string> extends Redacted.Variance<A>, Equal.Equal, Pipeable {
}

/**
* @since 3.3.0
*/
export declare namespace Redacted {
/**
* @since 3.3.0
* @category models
*/
export interface Variance<out A> {
readonly [RedactedTypeId]: {
readonly _A: Covariant<A>
}
}

/**
* @since 3.3.0
* @category type-level
*/
export type Value<T extends Redacted<any>> = [T] extends [Redacted<infer _A>] ? _A : never
}

/**
* @since 3.3.0
* @category refinements
*/
export const isRedacted: (u: unknown) => u is Redacted<unknown> = redacted_.isRedacted

/**
* @since 3.3.0
* @category constructors
*/
export const make: <A>(value: A) => Redacted<A> = redacted_.make

/**
* @since 3.3.0
* @category getters
*/
export const value: <A>(self: Redacted<A>) => A = redacted_.value

/**
* @since 3.3.0
* @category unsafe
*/
export const unsafeWipe: <A>(self: Redacted<A>) => boolean = redacted_.unsafeWipe

/**
* @category equivalence
* @since 3.3.0
*/
export const getEquivalence = <A>(isEquivalent: Equivalence.Equivalence<A>): Equivalence.Equivalence<Redacted<A>> =>
Equivalence.make((x, y) => x === y || (isEquivalent(value(x), value(y))))
Loading

0 comments on commit 73d03f1

Please sign in to comment.