diff --git a/.changeset/preserve-config-all-input-evidence.md b/.changeset/preserve-config-all-input-evidence.md new file mode 100644 index 00000000000..bd8b92ad8b2 --- /dev/null +++ b/.changeset/preserve-config-all-input-evidence.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve sibling provider input evidence when `Config.all` evaluates a failing child. diff --git a/packages/effect/src/Config.ts b/packages/effect/src/Config.ts index 2d39dd3261c..5caddba3a43 100644 --- a/packages/effect/src/Config.ts +++ b/packages/effect/src/Config.ts @@ -17,6 +17,7 @@ import * as LogLevel_ from "./LogLevel.ts" import * as Option from "./Option.ts" import * as Predicate from "./Predicate.ts" import * as Rec from "./Record.ts" +import * as Result from "./Result.ts" import * as Schema from "./Schema.ts" import * as SchemaAST from "./SchemaAST.ts" import * as SchemaGetter from "./SchemaGetter.ts" @@ -414,14 +415,14 @@ export function all> | Record Effect.flatMapEager( - Effect.all(configs.map((config) => evaluateAt(config, provider, pathPrefix))), + Effect.all(configs.map((config) => Effect.result(evaluateAt(config, provider, pathPrefix)))), resolveArray ) ) as any } else { return make((provider, pathPrefix) => Effect.flatMapEager( - Effect.all(Rec.map(configs, (config) => evaluateAt(config, provider, pathPrefix))), + Effect.all(Rec.map(configs, (config) => Effect.result(evaluateAt(config, provider, pathPrefix)))), resolveRecord ) ) as any @@ -429,12 +430,19 @@ export function all> | Record> + results: ReadonlyArray, EvaluationFailure>> ): Effect.Effect>, EvaluationFailure> => { const values: Array = [] + let firstFailure: EvaluationFailure | undefined let firstAbsent: Absent | undefined let hasInput = false - for (const resolution of resolutions) { + for (const result of results) { + if (Result.isFailure(result)) { + firstFailure ??= result.failure + hasInput = hasInput || result.failure.hasInput + continue + } + const resolution = result.success if (resolution._tag === "Absent") { firstAbsent ??= resolution } else { @@ -442,6 +450,9 @@ const resolveArray = ( hasInput = hasInput || resolution.hasInput } } + if (firstFailure !== undefined) { + return Effect.fail(evaluationFailure(firstFailure.error, hasInput)) + } if (firstAbsent !== undefined) { return hasInput ? Effect.fail(evaluationFailure(firstAbsent.error, true)) : Effect.succeed(firstAbsent) } @@ -449,13 +460,20 @@ const resolveArray = ( } const resolveRecord = ( - resolutions: Record> + results: Record, EvaluationFailure>> ): Effect.Effect>, EvaluationFailure> => { const values: Record = {} + let firstFailure: EvaluationFailure | undefined let firstAbsent: Absent | undefined let hasInput = false - for (const key in resolutions) { - const resolution = resolutions[key] + for (const key in results) { + const result = results[key] + if (Result.isFailure(result)) { + firstFailure ??= result.failure + hasInput = hasInput || result.failure.hasInput + continue + } + const resolution = result.success if (resolution._tag === "Absent") { firstAbsent ??= resolution } else { @@ -463,6 +481,9 @@ const resolveRecord = ( hasInput = hasInput || resolution.hasInput } } + if (firstFailure !== undefined) { + return Effect.fail(evaluationFailure(firstFailure.error, hasInput)) + } if (firstAbsent !== undefined) { return hasInput ? Effect.fail(evaluationFailure(firstAbsent.error, true)) : Effect.succeed(firstAbsent) } diff --git a/packages/effect/test/Config.test.ts b/packages/effect/test/Config.test.ts index f48bfd1b75e..ddccd151da0 100644 --- a/packages/effect/test/Config.test.ts +++ b/packages/effect/test/Config.test.ts @@ -846,6 +846,34 @@ Expected "Infinity" | "-Infinity" | "NaN" ) })) + it.effect("preserves sibling input evidence when recovering an all failure", () => + Effect.gen(function*() { + const sourceError = new ConfigProvider.SourceError({ message: "source unavailable" }) + const provider = ConfigProvider.make((path) => { + if (path[0] === "failed") return Effect.fail(sourceError) + if (path[0] === "present") return Effect.succeed(ConfigProvider.makeValue("value")) + return Effect.succeed(undefined) + }) + const recovered = Config.all({ + failed: Config.string("failed"), + present: Config.string("present") + }).pipe(Config.orElse(() => Config.succeed({ failed: "recovered", present: "recovered" }))) + const config = Config.all({ + recovered, + required: Config.string("required") + }).pipe(Config.withDefault({ + recovered: { failed: "default", present: "default" }, + required: "default" + })) + const error = yield* config.parse(provider).pipe(Effect.flip) + + assert.strictEqual( + error.cause.message, + `Expected string + at ["required"]` + ) + })) + it.effect("does not invent provider input evidence after an initial source failure", () => Effect.gen(function*() { const sourceError = new ConfigProvider.SourceError({ message: "source unavailable" })