package result import ( "fmt" "path/filepath" "runtime" ) // Combinators for composing Expect-returning calls into a single expression. // // Every combinator takes an Expect and returns one, so failure propagates // untouched from step to step and no intermediate code inspects it. A failed // Expect skips every transform that follows — no closure runs, no zero value is // carried onward as if it were real — until something explicitly handles it: // [Expect.Or], [Expect.OrElse], [Expect.UnwrapOr], [Expect.UnwrapOrElse], or // the boundary itself via [Expect.Expect]. // // None of these exit the goroutine, so unlike [Expect.Expect] and [Expect.Must] // they are safe inside pkg/ library code. // // Readability is the entire point. Name a step and declare it before the chain // when it is reused, or when its body would otherwise bury the pipeline in the // middle of the call: // // port := parsePort(raw).Filter(unprivileged, "privileged port").UnwrapOr(defaultPort) // // A one-shot closure with a single call site stays where it is used — hoisting // it out only forces the reader to jump elsewhere. See example_flow_test.go. // // A combinator can only be a method when it preserves T, because Go has no type // parameters on methods. The package-level [Map] and [AndThen] are the // type-changing forms; a chain breaks at the crossing and resumes on the result. // Map applies f to a successful value and passes a failure through untouched. // f is not called on a failed Expect. // // func readFile(path string) result.Expect[string] // contents, or a failure // // raw := readFile(path).Map(strings.TrimSpace).Map(strings.ToLower) // // This is the chaining form and requires f to preserve the type; when the // transform produces a different type, use the package-level [Map]. func (r Expect[T]) Map(f func(T) T) Expect[T] { if r.err != nil { return Expect[T]{err: r.err} } return Expect[T]{value: f(r.value)} } // AndThen chains a step that is itself fallible, flattening the result so // nested Expects never appear. f is not called on a failed Expect. // // port := parsePort(raw).AndThen(rejectPrivileged) // // The chaining counterpart to [Expect.Map] for fallible steps, with the same // same-type restriction; the package-level [AndThen] handles a type change. func (r Expect[T]) AndThen(f func(T) Expect[T]) Expect[T] { if r.err != nil { return Expect[T]{err: r.err} } return f(r.value) } // Filter turns a successful value into a failure when pred rejects it, building // the error from a formatted message with the caller's file and line prepended, // exactly as [Failf] does. pred is not called on an already-failed Expect. // // port := parsePort(raw).Filter(unprivileged, "port must be >= 1024") // // The message can name the rejected value wherever the Expect has a binding to // read it from. The arg is evaluated eagerly, which does no harm: the message is // formatted only when pred rejects, and [Expect.Value] is exactly what pred was // handed. // // r := parsePort(raw) // port := r.Filter(unprivileged, "port %d is privileged", r.Value()) // // Mid-chain there is no such binding, so reach for [Expect.AndThen] and return // a [Failf] instead: // // func rejectPrivileged(p int) result.Expect[int] { // if p < 1024 { // return result.Failf[int]("port %d is privileged", p) // } // return result.Ok(p) // } // // port := parsePort(raw).AndThen(rejectPrivileged) func (r Expect[T]) Filter(pred func(T) bool, format string, args ...any) Expect[T] { if r.err != nil { return Expect[T]{err: r.err} } if pred(r.value) { return r } _, file, line, _ := runtime.Caller(1) loc := fmt.Sprintf("%s:%d", filepath.Base(file), line) return Expect[T]{err: fmt.Errorf(loc+": "+format, args...)} } // MapErr replaces the error of a failed Expect with f(err), leaving a success // untouched. Use it to add context mid-chain without breaking it; wrap with %w // to preserve the errors.Is/As chain. // // func describeAsConfigFailure(err error) error { // return fmt.Errorf("load config: %w", err) // } // // cfg := readFile(path).MapErr(describeAsConfigFailure) // // f is expected to return a non-nil error. Returning nil produces a successful // Expect holding the zero value, which is almost always a bug. func (r Expect[T]) MapErr(f func(error) error) Expect[T] { if r.err == nil { return r } return Expect[T]{err: f(r.err)} } // Or returns r when it succeeded, otherwise alt — discarding r's error. Chain // it to express a precedence order over several sources: // // port := fromFlag.Or(fromEnv).Or(fromFile).UnwrapOr(8080) // // Every alternative is evaluated before the chain runs; use [Expect.OrElse] // when producing one has a cost or a side effect. If alt is itself a failure // the chain stays failed, carrying alt's error. func (r Expect[T]) Or(alt Expect[T]) Expect[T] { if r.err != nil { return alt } return r } // OrElse returns r when it succeeded, otherwise f(err). The lazy counterpart to // [Expect.Or]: f runs only on failure, and receives the error so it can decide // whether to recover from this particular one. // // func rereadFromBackup(error) result.Expect[string] { return readFile(backupPath) } // // cfg := readFile(primaryPath).OrElse(rereadFromBackup) func (r Expect[T]) OrElse(f func(error) Expect[T]) Expect[T] { if r.err != nil { return f(r.err) } return r } // UnwrapOr leaves the chain with a value in hand, returning def on failure. // The error is discarded; when it should reach the caller instead, end the // chain with [Expect.Expect] or [Expect.Unwrap]. func (r Expect[T]) UnwrapOr(def T) T { if r.err != nil { return def } return r.value } // UnwrapOrElse leaves the chain with a value in hand, computing the fallback // from the error on failure. The lazy counterpart to [Expect.UnwrapOr]. // // func warnAndUseDefaultPort(err error) int { // log.Warn("bad port, using default", "err", err) // return 8080 // } // // port := parsePort(raw).UnwrapOrElse(warnAndUseDefaultPort) func (r Expect[T]) UnwrapOrElse(f func(error) T) T { if r.err != nil { return f(r.err) } return r.value } // Map is the type-changing form of [Expect.Map]: it applies f to a successful // value, producing an Expect of the new type, and passes a failure through // untouched. f is not called on a failed Expect. // // It is a function rather than a method because Go has no type parameters on // methods. A chain therefore breaks at each type change and resumes on the // result, with the crossing written inside-out: // // func listenAddr(p int) string { return fmt.Sprintf("localhost:%d", p) } // // addr := result.Map(parsePort(raw), listenAddr).Map(strings.ToLower) // // When the chain continues past the crossing, give the crossing a name of its // own. The caller's site is then an unbroken chain again, and the named step can // layer on context that Map cannot — Map carries the error verbatim: // // func describeAsPortFailure(err error) error { // return fmt.Errorf("resolve listen port: %w", err) // } // // func listenAddrOf(raw string) result.Expect[string] { // return result.Map(parsePort(raw), listenAddr).MapErr(describeAsPortFailure) // } // // addr := listenAddrOf(raw).Filter(isLocal, "not a local address").UnwrapOr(defaultAddr) func Map[T, U any](r Expect[T], f func(T) U) Expect[U] { if r.err != nil { return Expect[U]{err: r.err} } return Expect[U]{value: f(r.value)} } // AndThen is the type-changing form of [Expect.AndThen]: it chains a fallible // step that produces a different type, flattening the result so nested Expects // never appear. f is not called on a failed Expect. // // port := result.AndThen(readFile(path), parsePort).Filter(unprivileged, "privileged port") func AndThen[T, U any](r Expect[T], f func(T) Expect[U]) Expect[U] { if r.err != nil { return Expect[U]{err: r.err} } return f(r.value) }