Skip to content
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/preserve-config-all-input-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Preserve sibling provider input evidence when `Config.all` evaluates a failing child.
35 changes: 28 additions & 7 deletions packages/effect/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -414,55 +415,75 @@ export function all<const Arg extends Iterable<Config<any>> | Record<string, Con
if (Array.isArray(configs)) {
return make((provider, pathPrefix) =>
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
}
}

const resolveArray = (
resolutions: ReadonlyArray<Resolution<any>>
results: ReadonlyArray<Result.Result<Resolution<any>, EvaluationFailure>>
): Effect.Effect<Resolution<Array<any>>, EvaluationFailure> => {
const values: Array<any> = []
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 {
values.push(resolution.value)
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)
}
return Effect.succeed(resolved(values, hasInput))
}

const resolveRecord = (
resolutions: Record<string, Resolution<any>>
results: Record<string, Result.Result<Resolution<any>, EvaluationFailure>>
): Effect.Effect<Resolution<Record<string, any>>, EvaluationFailure> => {
const values: Record<string, any> = {}
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 {
InternalRecord.assignProperty(values, key, resolution.value)
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)
}
Expand Down
28 changes: 28 additions & 0 deletions packages/effect/test/Config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" })
Expand Down
Loading