diff --git a/.changeset/tidy-trainers-serve.md b/.changeset/tidy-trainers-serve.md new file mode 100644 index 0000000000..0b51b413ee --- /dev/null +++ b/.changeset/tidy-trainers-serve.md @@ -0,0 +1,22 @@ +--- +"effect": minor +--- + +`Config.redacted` has been made more flexible and can now wrap any other config. This allows to transform or validate config values before it’s hidden. + +```ts +import { Config } from "effect" + +Effect.gen(function* () { + // can be any string including empty + const pass1 = yield* Config.redacted("PASSWORD") + // ^? Redacted + + // can't be empty string + const pass2 = yield* Config.redacted(Config.nonEmptyString("PASSWORD")) + // ^? Redacted + + const pass2 = yield* Config.redacted(Config.number("SECRET_NUMBER")) + // ^? Redacted +}) +``` diff --git a/packages/effect/src/Config.ts b/packages/effect/src/Config.ts index a58fdb048f..74fed9321b 100644 --- a/packages/effect/src/Config.ts +++ b/packages/effect/src/Config.ts @@ -357,7 +357,10 @@ export const secret: (name?: string) => Config = internal.secret * @since 2.0.0 * @category constructors */ -export const redacted: (name?: string) => Config = internal.redacted +export const redacted: { + (name?: string): Config + (config: Config): Config> +} = internal.redacted /** * Constructs a config for a sequence of values. diff --git a/packages/effect/src/internal/config.ts b/packages/effect/src/internal/config.ts index ed051c8b8f..dc5e509057 100644 --- a/packages/effect/src/internal/config.ts +++ b/packages/effect/src/internal/config.ts @@ -437,12 +437,11 @@ export const secret = (name?: string): Config.Config => { } /** @internal */ -export const redacted = (name?: string): Config.Config => { - const config = primitive( - "a redacted property", - (text) => Either.right(redacted_.make(text)) - ) - return name === undefined ? config : nested(config, name) +export const redacted = ( + nameOrConfig?: string | Config.Config +): Config.Config> => { + const config: Config.Config = isConfig(nameOrConfig) ? nameOrConfig : string(nameOrConfig) + return map(config, redacted_.make) } /** @internal */ diff --git a/packages/effect/test/Config.test.ts b/packages/effect/test/Config.test.ts index d7e485a328..0b01bc8250 100644 --- a/packages/effect/test/Config.test.ts +++ b/packages/effect/test/Config.test.ts @@ -34,6 +34,16 @@ const assertSuccess = ( expect(result).toStrictEqual(Exit.succeed(a)) } +const assertEqualSuccess = ( + config: Config.Config, + map: ReadonlyArray, + a: A +) => { + const configProvider = ConfigProvider.fromMap(new Map(map)) + const result = Effect.runSync(Effect.exit(configProvider.load(config))) + expect(Equal.equals(Exit.succeed(a), result)).toBe(true) +} + describe("Config", () => { describe("boolean", () => { it("name = undefined", () => { @@ -534,7 +544,12 @@ describe("Config", () => { it("name != undefined", () => { const config = Config.redacted("SECRET") - assertSuccess(config, [["SECRET", "a"]], Redacted.make("a")) + assertEqualSuccess(config, [["SECRET", "a"]], Redacted.make("a")) + }) + + it("can wrap generic Config", () => { + const config = Config.redacted(Config.integer("NUM")) + assertEqualSuccess(config, [["NUM", "2"]], Redacted.make(2)) }) }) @@ -547,7 +562,7 @@ describe("Config", () => { it("name != undefined", () => { const config = Config.secret("SECRET") - assertSuccess(config, [["SECRET", "a"]], Secret.fromString("a")) + assertEqualSuccess(config, [["SECRET", "a"]], Secret.fromString("a")) }) })