218 lines
7.7 KiB
Go
218 lines
7.7 KiB
Go
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)
|
|
}
|