254 lines
7.1 KiB
Go
254 lines
7.1 KiB
Go
package result_test
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"gitea.djmil.dev/go/template/pkg/result"
|
|
)
|
|
|
|
// parseHost is an example of a simple utility function, that validates a hostname.
|
|
func parseHost(s string) result.Expect[string] {
|
|
if s == "" {
|
|
return result.Errf[string]("host must not be empty")
|
|
}
|
|
return result.Ok(s)
|
|
}
|
|
|
|
// parsePort wraps strconv.Atoi so callers can use the happy-path style.
|
|
func parsePort(s string) result.Expect[int] {
|
|
port := result.Of(strconv.Atoi(s))
|
|
if port.Err() != nil {
|
|
return result.Errw[int](port.Err(), "result.Of")
|
|
}
|
|
if port.Value() < 1 || port.Value() > 65535 {
|
|
return result.Errf[int]("%d out of range", port.Value())
|
|
}
|
|
return port
|
|
}
|
|
|
|
// getURL is an example of business logic implementation with emphasis on happy-path.
|
|
func getURL(host, port string) string {
|
|
mHost := parseHost(host).Expect("parse host")
|
|
mPort := parsePort(port).Expect("parse port")
|
|
return fmt.Sprintf("http://%s:%d", mHost, mPort)
|
|
}
|
|
|
|
// Example_errCheck shows checking the error without panicking — useful at the
|
|
// outermost boundary where you want a normal error return.
|
|
func Example_errCheck() {
|
|
r := parsePort("not-a-number")
|
|
if r.Err() != nil {
|
|
fmt.Println("failed:", r.Err())
|
|
}
|
|
// Output:
|
|
// failed: example_test.go:22: result.Of
|
|
// strconv.Atoi: parsing "not-a-number": invalid syntax
|
|
}
|
|
|
|
// Example_happyPath shows the basic happy-path pattern: call Expect at each
|
|
// step; if anything fails the panic unwinds to the nearest Catch.
|
|
func Example_happyPath() {
|
|
host := parseHost("localhost").Expect("read host")
|
|
fmt.Println(host)
|
|
// Output:
|
|
// localhost
|
|
}
|
|
|
|
// Example_catch shows the failure path: when Expect exits the goroutine,
|
|
// defer Catch at the entry point converts it into a normal error return.
|
|
func Example_catch() {
|
|
loadHost := func() (err error) {
|
|
defer result.Catch(&err)
|
|
host := parseHost("").Expect("read host")
|
|
fmt.Println(host)
|
|
return
|
|
}
|
|
|
|
if err := loadHost(); err != nil {
|
|
fmt.Println("caught:", err)
|
|
// NOTE: you can have stack trace of an error as well
|
|
// fmt.Println("stacktrace:", result.StackTrace(err))
|
|
}
|
|
// Output:
|
|
// caught: read host
|
|
// example_test.go:13: host must not be empty
|
|
}
|
|
|
|
// Example_run shows result.Run as a lightweight synchronous boundary — a concise
|
|
// alternative to defer Catch when no named return is needed. Expect calls may be
|
|
// chained freely inside.
|
|
func Example_run() {
|
|
err := result.Run(func() {
|
|
url := getURL("localhost", "8080")
|
|
fmt.Println(url)
|
|
})
|
|
|
|
if err != nil {
|
|
fmt.Println("failed:", err)
|
|
}
|
|
// Output:
|
|
// http://localhost:8080
|
|
}
|
|
|
|
// Example_runtimeError shows that Expectf annotates the error message with the
|
|
// formatted context, just like Expect does.
|
|
//
|
|
// NOTE: Genuine runtime panics (nil-deref, index out of bounds) are NOT collected,
|
|
// they propagate and crash the program, as they should.
|
|
func Example_runtimeError() {
|
|
err := result.Run(func() {
|
|
_ = parsePort("99999").Expectf("arg %d port value", 2)
|
|
})
|
|
|
|
if err != nil {
|
|
fmt.Println("caught:", err)
|
|
}
|
|
// Output:
|
|
// caught: arg 2 port value
|
|
// example_test.go:25: 99999 out of range
|
|
}
|
|
|
|
// Example_go is like result.Run but returns a typed Expect[T] so the computed
|
|
// value can be retrieved. Any failure is collected and surfaced on that Expect.
|
|
func Example_go() {
|
|
url := result.Go(func() string {
|
|
return getURL("localhost", "8080")
|
|
}).Must()
|
|
|
|
fmt.Println(url)
|
|
// Output:
|
|
// http://localhost:8080
|
|
}
|
|
|
|
// Example_goError shows result.Go collecting an error when one of the chained
|
|
// Expect calls inside the goroutine fails.
|
|
func Example_goError() {
|
|
url := result.Go(func() string {
|
|
return getURL("localhost", "99999")
|
|
})
|
|
|
|
if err := url.Err(); err != nil {
|
|
fmt.Println("failed:", err)
|
|
}
|
|
// Output:
|
|
// failed: parse port
|
|
// example_test.go:25: 99999 out of range
|
|
}
|
|
|
|
// Example_unwrap shows re-joining the normal Go (value, error) world at a
|
|
// boundary where both values are needed separately.
|
|
func Example_unwrap() {
|
|
port, err := parsePort("443").Unwrap()
|
|
if err != nil {
|
|
fmt.Println("error:", err)
|
|
return
|
|
}
|
|
fmt.Println(port)
|
|
// Output:
|
|
// 443
|
|
}
|
|
|
|
// Example_all shows result.All collecting values from concurrently running
|
|
// goroutines. All goroutines are started before any result is read.
|
|
func Example_all() {
|
|
aCh := result.Async(func() int { return parsePort("80").Must() })
|
|
bCh := result.Async(func() int { return parsePort("443").Must() })
|
|
cCh := result.Async(func() int { return parsePort("8080").Must() })
|
|
|
|
ports := result.All(aCh, bCh, cCh).Must()
|
|
fmt.Println(ports)
|
|
// Output:
|
|
// [80 443 8080]
|
|
}
|
|
|
|
// Example_allError shows result.All returning on the first error (in channel
|
|
// order). Here bCh fails, so its error is returned and cCh is never read.
|
|
func Example_allError() {
|
|
aCh := result.Async(func() int { return parsePort("80").Must() })
|
|
bCh := result.Async(func() int { return parsePort("99999").Must() })
|
|
cCh := result.Async(func() int { return parsePort("8080").Must() })
|
|
|
|
if err := result.All(aCh, bCh, cCh).Err(); err != nil {
|
|
fmt.Println("failed:", err)
|
|
}
|
|
// Output:
|
|
// failed: example_test.go:25: 99999 out of range
|
|
}
|
|
|
|
// Example_map shows result.Map running a function concurrently over a slice.
|
|
// All goroutines complete and all errors are collected.
|
|
func Example_map() {
|
|
ports, err := result.Map(
|
|
[]string{"80", "443", "8080"},
|
|
func(s string) int { return parsePort(s).Must() },
|
|
).Unwrap()
|
|
|
|
if err != nil {
|
|
fmt.Println("failed:", err)
|
|
return
|
|
}
|
|
fmt.Println(ports)
|
|
// Output:
|
|
// [80 443 8080]
|
|
}
|
|
|
|
// Example_mapError shows result.Map collecting all errors when multiple inputs
|
|
// fail, rather than stopping at the first.
|
|
func Example_mapError() {
|
|
_, err := result.Map(
|
|
[]string{"80", "bad", "99999"},
|
|
func(s string) int { return parsePort(s).Must() },
|
|
).Unwrap()
|
|
|
|
if err != nil {
|
|
fmt.Println("failed:", err)
|
|
}
|
|
// Output:
|
|
// failed: example_test.go:22: result.Of
|
|
// strconv.Atoi: parsing "bad": invalid syntax
|
|
// example_test.go:25: 99999 out of range
|
|
}
|
|
|
|
// Example_asyncOf shows result.AsyncOf running library functions (which return
|
|
// Expect[T]) concurrently. Only one .Expect() per goroutine is needed — at the
|
|
// collection site inside the boundary.
|
|
func Example_asyncOf() {
|
|
hostCh := result.AsyncOf(result.Bind(parseHost, "localhost"))
|
|
portCh := result.AsyncOf(func() result.Expect[int] { return parsePort("8080") })
|
|
|
|
url, err := result.Go(func() string {
|
|
host := (<-hostCh).Expect("parse host")
|
|
port := (<-portCh).Expect("parse port")
|
|
return fmt.Sprintf("http://%s:%d", host, port)
|
|
}).Unwrap()
|
|
|
|
if err != nil {
|
|
fmt.Println("failed:", err)
|
|
return
|
|
}
|
|
fmt.Println(url)
|
|
// Output:
|
|
// http://localhost:8080
|
|
}
|
|
|
|
// Example_asyncOfError shows result.AsyncOf propagating a failure through the
|
|
// channel — the error surfaces at the .Expect() call inside the boundary.
|
|
func Example_asyncOfError() {
|
|
hostCh := result.AsyncOf(func() result.Expect[string] { return parseHost("") })
|
|
portCh := result.AsyncOf(func() result.Expect[int] { return parsePort("8080") })
|
|
|
|
_, err := result.Go(func() string {
|
|
host := (<-hostCh).Expect("parse host")
|
|
port := (<-portCh).Expect("parse port")
|
|
return fmt.Sprintf("http://%s:%d", host, port)
|
|
}).Unwrap()
|
|
|
|
if err != nil {
|
|
fmt.Println("failed:", err)
|
|
}
|
|
// Output:
|
|
// failed: parse host
|
|
// example_test.go:13: host must not be empty
|
|
}
|