├── types └── types.go ├── net ├── http │ ├── charset.go │ ├── accept_encodings.go │ ├── quality_value.go │ ├── form.go │ ├── quality_value_test.go │ ├── helpers_test_go1.18.go │ ├── retryable_test.go │ ├── mime_types.go │ ├── helpers_go1.18.go │ ├── headers.go │ ├── retrier_test.go │ ├── retryable.go │ ├── retrier.go │ ├── helpers.go │ └── helpers_test.go └── url │ ├── helpers.go │ └── helpers_test.go ├── go.mod ├── values ├── values.go ├── result │ ├── result.go │ └── result_test.go └── option │ ├── option_common.go │ ├── option_sql_go1.22.go │ ├── option_sql.go │ └── option_test.go ├── Makefile ├── strings ├── join_test.go └── join.go ├── time ├── nanotime.go ├── time.go ├── instant_test.go ├── nanotime_test.go └── instant.go ├── .gitignore ├── math ├── max_go121.go ├── min_go121.go ├── max.go ├── min.go ├── max_test.go └── min_test.go ├── go.sum ├── unsafe ├── conversions_test.go ├── conversions_go121.go └── conversions.go ├── map ├── map.go └── map_test.go ├── ascii └── helpers.go ├── bytes └── size.go ├── context ├── context_test.go └── context.go ├── strconv └── bool.go ├── database └── sql │ └── transaction.go ├── constraints └── constraints.go ├── LICENSE-MIT ├── .github └── workflows │ └── go.yml ├── runtime ├── stack_test.go └── stack.go ├── sync ├── mutex_test.go ├── mutex2_test.go ├── mutex2.go └── mutex.go ├── io ├── limit_reader.go └── limit_reader_test.go ├── errors ├── do.go ├── retryable.go ├── retrier_test.go └── retrier.go ├── _examples └── net │ └── http │ └── retrier │ └── main.go ├── README.md ├── app ├── context_test.go └── context.go ├── slice ├── slice_test.go └── slice.go ├── CHANGELOG.md ├── container └── list │ ├── doubly_linked.go │ └── doubly_linked_test.go └── LICENSE-APACHE /types/types.go: -------------------------------------------------------------------------------- 1 | package typesext 2 | 3 | // Nothing indicates the absence of a value and is an alias to struct{} 4 | type Nothing = struct{} 5 | -------------------------------------------------------------------------------- /net/http/charset.go: -------------------------------------------------------------------------------- 1 | package httpext 2 | 3 | // Charset values 4 | const ( 5 | UTF8 string = "utf-8" 6 | ISO88591 string = "iso-8859-1" 7 | ) 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-playground/pkg/v5 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/go-playground/assert/v2 v2.2.0 7 | github.com/go-playground/form/v4 v4.2.1 8 | ) 9 | -------------------------------------------------------------------------------- /values/values.go: -------------------------------------------------------------------------------- 1 | package valuesext 2 | 3 | import typesext "github.com/go-playground/pkg/v5/types" 4 | 5 | // Nothing is an instantiated value of type typesext.Nothing. 6 | var Nothing = typesext.Nothing{} 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOCMD=GO111MODULE=on go 2 | 3 | test: 4 | $(GOCMD) test -cover -race ./... 5 | 6 | bench: 7 | $(GOCMD) test -run=NONE -bench=. -benchmem ./... 8 | 9 | lint: 10 | golangci-lint run 11 | 12 | .PHONY: lint test bench -------------------------------------------------------------------------------- /net/http/accept_encodings.go: -------------------------------------------------------------------------------- 1 | package httpext 2 | 3 | // Accept-Encoding values 4 | const ( 5 | Gzip string = "gzip" 6 | Compress string = "compress" 7 | Deflate string = "deflate" 8 | Br string = "br" 9 | Identity string = "identity" 10 | Any string = "*" 11 | ) 12 | -------------------------------------------------------------------------------- /strings/join_test.go: -------------------------------------------------------------------------------- 1 | package stringsext 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestJoin(t *testing.T) { 9 | s1, s2, s3 := "a", "b", "c" 10 | arr := []string{s1, s2, s3} 11 | if strings.Join(arr, ",") != Join(",", s1, s2, s3) { 12 | t.Errorf("Join failed") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /time/nanotime.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package timeext 5 | 6 | import ( 7 | "time" 8 | ) 9 | 10 | var base = time.Now() 11 | 12 | // NanoTime returns a monotonically increasing time in nanoseconds. 13 | func NanoTime() int64 { 14 | return int64(time.Since(base)) 15 | } 16 | -------------------------------------------------------------------------------- /time/time.go: -------------------------------------------------------------------------------- 1 | package timeext 2 | 3 | const ( 4 | // RFC3339Nano is a correct replacement to Go's current time.RFC3339Nano which is NOT sortable and 5 | // have no intention of fixing https://github.com/golang/go/issues/19635; this format fixes that. 6 | RFC3339Nano = "2006-01-02T15:04:05.000000000Z07:00" 7 | ) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | .idea 16 | -------------------------------------------------------------------------------- /net/url/helpers.go: -------------------------------------------------------------------------------- 1 | package urlext 2 | 3 | import ( 4 | "net/url" 5 | 6 | httpext "github.com/go-playground/pkg/v5/net/http" 7 | ) 8 | 9 | // EncodeToURLValues encodes a struct or field into a set of url.Values 10 | func EncodeToURLValues(v interface{}) (url.Values, error) { 11 | return httpext.DefaultFormEncoder.Encode(v) 12 | } 13 | -------------------------------------------------------------------------------- /strings/join.go: -------------------------------------------------------------------------------- 1 | package stringsext 2 | 3 | import "strings" 4 | 5 | // Join is a wrapper around strings.Join with a more ergonomic interface when you don't already have a slice of strings. 6 | // 7 | // Join concatenates the variadic elements placing the separator string between each element. 8 | func Join(sep string, s ...string) string { 9 | return strings.Join(s, sep) 10 | } 11 | -------------------------------------------------------------------------------- /math/max_go121.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | 3 | package mathext 4 | 5 | import ( 6 | constraintsext "github.com/go-playground/pkg/v5/constraints" 7 | ) 8 | 9 | // Max returns the larger value. 10 | // 11 | // NOTE: this function does not check for difference in floats of 0/zero vs -0/negative zero using Signbit. 12 | // 13 | // Deprecated: use the new std library `max` instead. 14 | func Max[N constraintsext.Number](x, y N) N { 15 | return max(x, y) 16 | } 17 | -------------------------------------------------------------------------------- /math/min_go121.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | 3 | package mathext 4 | 5 | import ( 6 | constraintsext "github.com/go-playground/pkg/v5/constraints" 7 | ) 8 | 9 | // Min returns the smaller value. 10 | // 11 | // NOTE: this function does not check for difference in floats of 0/zero vs -0/negative zero using Signbit. 12 | // 13 | // Deprecated: use the new std library `max` instead. 14 | func Min[N constraintsext.Number](x, y N) N { 15 | return min(x, y) 16 | } 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 2 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 3 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 4 | github.com/go-playground/form/v4 v4.2.1 h1:HjdRDKO0fftVMU5epjPW2SOREcZ6/wLUzEobqUGJuPw= 5 | github.com/go-playground/form/v4 v4.2.1/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U= 6 | -------------------------------------------------------------------------------- /time/instant_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package timeext 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestInstant(t *testing.T) { 11 | i := NewInstant() 12 | if i.Elapsed() < 0 { 13 | t.Fatalf("elapsed time should be always be monotonically increasing") 14 | } 15 | i2 := NewInstant() 16 | if i2.Since(i) < 0 { 17 | t.Fatalf("time since instant should always be after") 18 | } 19 | if i.Since(i2) != 0 { 20 | t.Fatalf("time since instant should be zero") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /net/url/helpers_test.go: -------------------------------------------------------------------------------- 1 | package urlext 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/go-playground/assert/v2" 7 | ) 8 | 9 | func TestEncodeToURLValues(t *testing.T) { 10 | type Test struct { 11 | Domain string `form:"domain"` 12 | Next string `form:"next"` 13 | } 14 | 15 | s := Test{Domain: "company.org", Next: "NIDEJ89#(@#NWJK"} 16 | values, err := EncodeToURLValues(s) 17 | Equal(t, err, nil) 18 | Equal(t, len(values), 2) 19 | Equal(t, values.Encode(), "domain=company.org&next=NIDEJ89%23%28%40%23NWJK") 20 | } 21 | -------------------------------------------------------------------------------- /unsafe/conversions_test.go: -------------------------------------------------------------------------------- 1 | package unsafeext 2 | 3 | import "testing" 4 | 5 | func TestBytesToString(t *testing.T) { 6 | b := []byte{'g', 'o', '-', 'p', 'l', 'a', 'y', 'g', 'r', 'o', 'u', 'n', 'd'} 7 | s := BytesToString(b) 8 | expected := string(b) 9 | if s != expected { 10 | t.Fatalf("expected '%s' got '%s'", expected, s) 11 | } 12 | } 13 | 14 | func TestStringToBytes(t *testing.T) { 15 | s := "go-playground" 16 | b := StringToBytes(s) 17 | 18 | if string(b) != s { 19 | t.Fatalf("expected '%s' got '%s'", s, string(b)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /net/http/quality_value.go: -------------------------------------------------------------------------------- 1 | package httpext 2 | 3 | import "fmt" 4 | 5 | const ( 6 | // QualityValueFormat is a format string helper for Quality Values 7 | QualityValueFormat = "%s;q=%1.3g" 8 | ) 9 | 10 | // QualityValue accepts a values to add/concatenate a quality values to and 11 | // the quality values itself. 12 | func QualityValue(v string, qv float32) string { 13 | if qv > 1 { 14 | qv = 1 // highest possible values 15 | } 16 | if qv < 0.001 { 17 | qv = 0.001 // lowest possible values 18 | } 19 | return fmt.Sprintf(QualityValueFormat, v, qv) 20 | } 21 | -------------------------------------------------------------------------------- /map/map.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package mapext 5 | 6 | // Retain retains only the elements specified by the function and removes others. 7 | func Retain[K comparable, V any](m map[K]V, fn func(key K, value V) bool) { 8 | for k, v := range m { 9 | if fn(k, v) { 10 | continue 11 | } 12 | delete(m, k) 13 | } 14 | } 15 | 16 | // Map allows mapping of a map[K]V -> U. 17 | func Map[K comparable, V any, U any](m map[K]V, init U, fn func(accum U, key K, value V) U) U { 18 | accum := init 19 | for k, v := range m { 20 | accum = fn(accum, k, v) 21 | } 22 | return accum 23 | } 24 | -------------------------------------------------------------------------------- /math/max.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 && !go1.21 2 | // +build go1.18,!go1.21 3 | 4 | package mathext 5 | 6 | import ( 7 | "math" 8 | 9 | constraintsext "github.com/go-playground/pkg/v5/constraints" 10 | ) 11 | 12 | // Max returns the larger value. 13 | // 14 | // NOTE: this function does not check for difference in floats of 0/zero vs -0/negative zero using Signbit. 15 | func Max[N constraintsext.Number](x, y N) N { 16 | // special case for floats 17 | // IEEE 754 says that only NaNs satisfy f != f. 18 | if x != x || y != y { 19 | return N(math.NaN()) 20 | } 21 | 22 | if x > y { 23 | return x 24 | } 25 | return y 26 | } 27 | -------------------------------------------------------------------------------- /math/min.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 && !go1.21 2 | // +build go1.18,!go1.21 3 | 4 | package mathext 5 | 6 | import ( 7 | "math" 8 | 9 | constraintsext "github.com/go-playground/pkg/v5/constraints" 10 | ) 11 | 12 | // Min returns the smaller value. 13 | // 14 | // NOTE: this function does not check for difference in floats of 0/zero vs -0/negative zero using Signbit. 15 | func Min[N constraintsext.Number](x, y N) N { 16 | // special case for float 17 | // IEEE 754 says that only NaNs satisfy f != f. 18 | if x != x || y != y { 19 | return N(math.NaN()) 20 | } 21 | 22 | if x < y { 23 | return x 24 | } 25 | return y 26 | } 27 | -------------------------------------------------------------------------------- /ascii/helpers.go: -------------------------------------------------------------------------------- 1 | package asciiext 2 | 3 | // IsAlphanumeric returns true if the byte is an ASCII letter or digit. 4 | func IsAlphanumeric(c byte) bool { 5 | return IsLower(c) || IsUpper(c) || IsDigit(c) 6 | } 7 | 8 | // IsUpper returns true if the byte is an ASCII uppercase letter. 9 | func IsUpper(c byte) bool { 10 | return c >= 'A' && c <= 'Z' 11 | } 12 | 13 | // IsLower returns true if the byte is an ASCII lowercase letter. 14 | func IsLower(c byte) bool { 15 | return c >= 'a' && c <= 'z' 16 | } 17 | 18 | // IsDigit returns true if the byte is an ASCII digit. 19 | func IsDigit(c byte) bool { 20 | return c >= '0' && c <= '9' 21 | } 22 | -------------------------------------------------------------------------------- /time/nanotime_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package timeext 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestNanoTime(t *testing.T) { 12 | t1 := NanoTime() 13 | time.Sleep(time.Second) 14 | t2 := NanoTime() 15 | if t2-t1 < int64(time.Second) { 16 | t.Fatalf("nanotime failed to monotonically increase, t1: %d t2: %d", t1, t2) 17 | } 18 | } 19 | 20 | func BenchmarkNanoTime(b *testing.B) { 21 | for i := 0; i < b.N; i++ { 22 | _ = NanoTime() 23 | } 24 | } 25 | 26 | func BenchmarkNanoTimeUsingUnixNano(b *testing.B) { 27 | for i := 0; i < b.N; i++ { 28 | _ = time.Now().UnixNano() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bytes/size.go: -------------------------------------------------------------------------------- 1 | package bytesext 2 | 3 | // Bytes is a type alias to int64 in order to better express the desired data type. 4 | type Bytes = int64 5 | 6 | // Common byte unit sizes 7 | const ( 8 | BYTE = 1 9 | 10 | // Decimal (Powers of 10 for Humans) 11 | KB = 1000 * BYTE 12 | MB = 1000 * KB 13 | GB = 1000 * MB 14 | TB = 1000 * GB 15 | PB = 1000 * TB 16 | EB = 1000 * PB 17 | ZB = 1000 * EB 18 | YB = 1000 * ZB 19 | 20 | // Binary (Powers of 2 for Computers) 21 | KiB = 1024 * BYTE 22 | MiB = 1024 * KiB 23 | GiB = 1024 * MiB 24 | TiB = 1024 * GiB 25 | PiB = 1024 * TiB 26 | EiB = 1024 * PiB 27 | ZiB = 1024 * EiB 28 | YiB = 1024 * ZiB 29 | ) 30 | -------------------------------------------------------------------------------- /net/http/form.go: -------------------------------------------------------------------------------- 1 | package httpext 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/go-playground/form/v4" 7 | ) 8 | 9 | // FormDecoder is the type used for decoding a form for use 10 | type FormDecoder interface { 11 | Decode(interface{}, url.Values) error 12 | } 13 | 14 | // FormEncoder is the type used for encoding form data 15 | type FormEncoder interface { 16 | Encode(interface{}) (url.Values, error) 17 | } 18 | 19 | var ( 20 | // DefaultFormDecoder of this package, which is configurable 21 | DefaultFormDecoder FormDecoder = form.NewDecoder() 22 | 23 | // DefaultFormEncoder of this package, which is configurable 24 | DefaultFormEncoder FormEncoder = form.NewEncoder() 25 | ) 26 | -------------------------------------------------------------------------------- /unsafe/conversions_go121.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | 3 | package unsafeext 4 | 5 | import ( 6 | "unsafe" 7 | ) 8 | 9 | // BytesToString converts an array of bytes into a string without allocating. 10 | // The byte slice passed to this function is not to be used after this call as it's unsafe; you have been warned. 11 | func BytesToString(b []byte) string { 12 | return unsafe.String(unsafe.SliceData(b), len(b)) 13 | } 14 | 15 | // StringToBytes converts an existing string into an []byte without allocating. 16 | // The string passed to these functions is not to be used again after this call as it's unsafe; you have been warned. 17 | func StringToBytes(s string) (b []byte) { 18 | d := unsafe.StringData(s) 19 | b = unsafe.Slice(d, len(s)) 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /context/context_test.go: -------------------------------------------------------------------------------- 1 | package contextext 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestDetach(t *testing.T) { 10 | 11 | key := &struct{ name string }{name: "key"} 12 | ctx := context.WithValue(context.Background(), key, 13) 13 | ctx, cancel := context.WithTimeout(ctx, time.Nanosecond) 14 | cancel() // cancel ensuring context has been canceled 15 | 16 | select { 17 | case <-ctx.Done(): 18 | default: 19 | t.Fatal("expected context to be cancelled") 20 | } 21 | 22 | ctx = Detach(ctx) 23 | 24 | select { 25 | case <-ctx.Done(): 26 | t.Fatal("expected context to be detached from parents cancellation") 27 | default: 28 | rawValue := ctx.Value(key) 29 | n, ok := rawValue.(int) 30 | if !ok || n != 13 { 31 | t.Fatalf("expected integer woth value of 13 but got %v", rawValue) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /strconv/bool.go: -------------------------------------------------------------------------------- 1 | package strconvext 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | // ParseBool returns the boolean value represented by the string. It extends the std library parse bool with a few more 8 | // valid options. 9 | // 10 | // It accepts 1, t, T, true, TRUE, True, on, yes, ok as true values and 0, f, F, false, FALSE, False, off, no as false. 11 | func ParseBool(str string) (bool, error) { 12 | switch str { 13 | case "1", "t", "T", "true", "TRUE", "True", "on", "yes", "ok": 14 | return true, nil 15 | case "", "0", "f", "F", "false", "FALSE", "False", "off", "no": 16 | return false, nil 17 | } 18 | // strconv.NumError mimicking exactly the strconv.ParseBool(..) error and type 19 | // to ensure compatibility with std library. 20 | return false, &strconv.NumError{Func: "ParseBool", Num: string([]byte(str)), Err: strconv.ErrSyntax} 21 | } 22 | -------------------------------------------------------------------------------- /database/sql/transaction.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package sqlext 5 | 6 | import ( 7 | "context" 8 | "database/sql" 9 | resultext "github.com/go-playground/pkg/v5/values/result" 10 | ) 11 | 12 | // DoTransaction is a helper function that abstracts some complexities of dealing with a transaction and rolling it back. 13 | func DoTransaction[T any](ctx context.Context, opts *sql.TxOptions, conn *sql.DB, fn func(context.Context, *sql.Tx) resultext.Result[T, error]) resultext.Result[T, error] { 14 | tx, err := conn.BeginTx(ctx, opts) 15 | if err != nil { 16 | return resultext.Err[T, error](err) 17 | } 18 | result := fn(ctx, tx) 19 | if result.IsErr() { 20 | _ = tx.Rollback() 21 | return result 22 | } 23 | err = tx.Commit() 24 | if err != nil { 25 | return resultext.Err[T, error](err) 26 | } 27 | return result 28 | } 29 | -------------------------------------------------------------------------------- /time/instant.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package timeext 5 | 6 | import "time" 7 | 8 | // Instant represents a monotonic instant in time. 9 | // 10 | // Instants are opaque types that can only be compared with one another and allows measuring of duration. 11 | type Instant int64 12 | 13 | // NewInstant returns a new Instant. 14 | func NewInstant() Instant { 15 | return Instant(NanoTime()) 16 | } 17 | 18 | // Elapsed returns the duration since the instant was created. 19 | func (i Instant) Elapsed() time.Duration { 20 | return time.Duration(NewInstant() - i) 21 | } 22 | 23 | // Since returns the duration elapsed from another Instant, or zero is that Instant is later than this one. 24 | func (i Instant) Since(instant Instant) time.Duration { 25 | if instant > i { 26 | return 0 27 | } 28 | return time.Duration(i - instant) 29 | } 30 | -------------------------------------------------------------------------------- /constraints/constraints.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package constraintsext 5 | 6 | // Number represents any non-complex number eg. Integer and Float. 7 | type Number interface { 8 | Integer | Float 9 | } 10 | 11 | // Integer represents any integer type both signed and unsigned. 12 | type Integer interface { 13 | Signed | Unsigned 14 | } 15 | 16 | // Float represents any float type. 17 | type Float interface { 18 | ~float32 | ~float64 19 | } 20 | 21 | // Signed represents any signed integer. 22 | type Signed interface { 23 | ~int | ~int8 | ~int16 | ~int32 | ~int64 24 | } 25 | 26 | // Unsigned represents any unsigned integer. 27 | type Unsigned interface { 28 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr 29 | } 30 | 31 | // Complex represents any complex number type. 32 | type Complex interface { 33 | ~complex64 | ~complex128 34 | } 35 | -------------------------------------------------------------------------------- /unsafe/conversions.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.21 2 | // +build !go1.21 3 | 4 | package unsafeext 5 | 6 | import ( 7 | "reflect" 8 | "unsafe" 9 | ) 10 | 11 | // BytesToString converts an array of bytes into a string without allocating. 12 | // The byte slice passed to this function is not to be used after this call as it's unsafe; you have been warned. 13 | func BytesToString(b []byte) string { 14 | return *(*string)(unsafe.Pointer(&b)) 15 | } 16 | 17 | // StringToBytes converts an existing string into an []byte without allocating. 18 | // The string passed to this functions is not to be used again after this call as it's unsafe; you have been warned. 19 | func StringToBytes(s string) (b []byte) { 20 | strHdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) 21 | sliceHdr := (*reflect.SliceHeader)(unsafe.Pointer(&b)) 22 | sliceHdr.Data = strHdr.Data 23 | sliceHdr.Cap = strHdr.Len 24 | sliceHdr.Len = strHdr.Len 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /net/http/quality_value_test.go: -------------------------------------------------------------------------------- 1 | package httpext 2 | 3 | import "testing" 4 | 5 | func TestQualityValue(t *testing.T) { 6 | type args struct { 7 | v string 8 | qv float32 9 | } 10 | tests := []struct { 11 | name string 12 | args args 13 | want string 14 | }{ 15 | { 16 | name: "in-range", 17 | args: args{v: "test", qv: 0.5}, 18 | want: "test;q=0.5", 19 | }, 20 | { 21 | name: "in-range-trailing-zeros", 22 | args: args{v: "test", qv: 0.500}, 23 | want: "test;q=0.5", 24 | }, 25 | { 26 | name: "greater-than-range", 27 | args: args{v: "test", qv: 1.500}, 28 | want: "test;q=1", 29 | }, 30 | { 31 | name: "less-than-range", 32 | args: args{v: "test", qv: 0.0000001}, 33 | want: "test;q=0.001", 34 | }, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | if got := QualityValue(tt.args.v, tt.args.qv); got != tt.want { 39 | t.Errorf("QualityValue() = %v, want %v", got, tt.want) 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /math/max_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package mathext 5 | 6 | import ( 7 | "math" 8 | "testing" 9 | 10 | . "github.com/go-playground/assert/v2" 11 | ) 12 | 13 | func TestMax(t *testing.T) { 14 | Equal(t, true, math.IsNaN(Max(math.NaN(), 1))) 15 | Equal(t, true, math.IsNaN(Max(1, math.NaN()))) 16 | Equal(t, math.Inf(0), Max(math.Inf(0), math.Inf(-1))) 17 | Equal(t, math.Inf(0), Max(math.Inf(-1), math.Inf(0))) 18 | Equal(t, 1.333, Max(1.333, 1.0)) 19 | Equal(t, 1.333, Max(1.0, 1.333)) 20 | Equal(t, 3, Max(3, 1)) 21 | Equal(t, 3, Max(1, 3)) 22 | Equal(t, 0, Max(0, -0)) 23 | Equal(t, 0, Max(-0, 0)) 24 | } 25 | 26 | func BenchmarkMaxInf(b *testing.B) { 27 | n1 := math.Inf(0) 28 | n2 := math.Inf(-1) 29 | 30 | for i := 0; i < b.N; i++ { 31 | _ = Max(n1, n2) 32 | } 33 | } 34 | 35 | func BenchmarkMaxNaN(b *testing.B) { 36 | n1 := math.Inf(0) 37 | n2 := math.NaN() 38 | 39 | for i := 0; i < b.N; i++ { 40 | _ = Max(n1, n2) 41 | } 42 | } 43 | 44 | func BenchmarkMaxNumber(b *testing.B) { 45 | n1 := 1 46 | n2 := 3 47 | 48 | for i := 0; i < b.N; i++ { 49 | _ = Max(n1, n2) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /math/min_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package mathext 5 | 6 | import ( 7 | "math" 8 | "testing" 9 | 10 | . "github.com/go-playground/assert/v2" 11 | ) 12 | 13 | func TestMin(t *testing.T) { 14 | Equal(t, true, math.IsNaN(Min(math.NaN(), 1))) 15 | Equal(t, true, math.IsNaN(Min(1, math.NaN()))) 16 | Equal(t, math.Inf(-1), Min(math.Inf(0), math.Inf(-1))) 17 | Equal(t, math.Inf(-1), Min(math.Inf(-1), math.Inf(0))) 18 | Equal(t, 1.0, Min(1.333, 1.0)) 19 | Equal(t, 1.0, Min(1.0, 1.333)) 20 | Equal(t, 1, Min(3, 1)) 21 | Equal(t, 1, Min(1, 3)) 22 | Equal(t, -0, Min(0, -0)) 23 | Equal(t, -0, Min(-0, 0)) 24 | } 25 | 26 | func BenchmarkMinInf(b *testing.B) { 27 | n1 := math.Inf(0) 28 | n2 := math.Inf(-1) 29 | 30 | for i := 0; i < b.N; i++ { 31 | _ = Min(n1, n2) 32 | } 33 | } 34 | 35 | func BenchmarkMinNaN(b *testing.B) { 36 | n1 := math.Inf(0) 37 | n2 := math.NaN() 38 | 39 | for i := 0; i < b.N; i++ { 40 | _ = Min(n1, n2) 41 | } 42 | } 43 | 44 | func BenchmarkMinNumber(b *testing.B) { 45 | n1 := 1 46 | n2 := 3 47 | 48 | for i := 0; i < b.N; i++ { 49 | _ = Min(n1, n2) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /map/map_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package mapext 5 | 6 | import ( 7 | . "github.com/go-playground/assert/v2" 8 | "sort" 9 | "testing" 10 | ) 11 | 12 | func TestRetain(t *testing.T) { 13 | m := map[string]int{ 14 | "0": 0, 15 | "1": 1, 16 | "2": 2, 17 | "3": 3, 18 | } 19 | Retain(m, func(key string, value int) bool { 20 | return value < 1 || value > 2 21 | }) 22 | Equal(t, len(m), 2) 23 | Equal(t, m["0"], 0) 24 | Equal(t, m["3"], 3) 25 | } 26 | 27 | func TestMap(t *testing.T) { 28 | // Test Map to slice 29 | m := map[string]int{ 30 | "0": 0, 31 | "1": 1, 32 | } 33 | slice := Map(m, make([]int, 0, len(m)), func(accum []int, key string, value int) []int { 34 | return append(accum, value) 35 | }) 36 | sort.SliceStable(slice, func(i, j int) bool { 37 | return i < j 38 | }) 39 | Equal(t, len(slice), 2) 40 | 41 | // Test Map to Map of different type 42 | inverted := Map(m, make(map[int]string, len(m)), func(accum map[int]string, key string, value int) map[int]string { 43 | accum[value] = key 44 | return accum 45 | }) 46 | Equal(t, len(inverted), 2) 47 | Equal(t, inverted[0], "0") 48 | Equal(t, inverted[1], "1") 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | types: [opened, edited, reopened, synchronize] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | test: 15 | strategy: 16 | matrix: 17 | go-version: [1.22.x,1.21.x,1.20.x,1.19.x,1.18.x,1.17.x] 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v4 23 | 24 | - name: Install Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | 29 | - name: Test 30 | run: go test -race -cover ./... 31 | 32 | golangci: 33 | name: lint 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: actions/setup-go@v5 38 | with: 39 | go-version: stable 40 | - name: golangci-lint 41 | uses: golangci/golangci-lint-action@v4 42 | with: 43 | version: latest -------------------------------------------------------------------------------- /runtime/stack_test.go: -------------------------------------------------------------------------------- 1 | package runtimeext 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func nested(level int) Frame { 8 | return StackLevel(level) 9 | } 10 | 11 | func TestStack(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | frame Frame 15 | file string 16 | line int 17 | function string 18 | }{ 19 | { 20 | name: "stack", 21 | frame: Stack(), 22 | file: "stack_test.go", 23 | line: 21, 24 | function: "TestStack", 25 | }, 26 | { 27 | name: "stack-level1", 28 | frame: nested(1), 29 | file: "stack_test.go", 30 | line: 28, 31 | function: "TestStack", 32 | }, 33 | { 34 | name: "stack-level0", 35 | frame: nested(0), 36 | file: "stack_test.go", 37 | line: 8, 38 | function: "nested", 39 | }, 40 | } 41 | for _, tt := range tests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | if tt.frame.File() != tt.file { 44 | t.Errorf("TestStack File() = %s, want %s", tt.frame.File(), tt.file) 45 | } 46 | if tt.frame.Line() != tt.line { 47 | t.Errorf("TestStack Line() = %d, want %d", tt.frame.Line(), tt.line) 48 | } 49 | if tt.frame.Function() != tt.function { 50 | t.Errorf("TestStack Function() = %s, want %s", tt.frame.Function(), tt.function) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /sync/mutex_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package syncext 5 | 6 | import ( 7 | optionext "github.com/go-playground/pkg/v5/values/option" 8 | "testing" 9 | 10 | . "github.com/go-playground/assert/v2" 11 | ) 12 | 13 | func TestMutex(t *testing.T) { 14 | m := NewMutex(make(map[string]int)) 15 | m.Lock()["foo"] = 1 16 | m.Unlock(optionext.None[map[string]int]()) 17 | 18 | err := m.PerformMut(func(m map[string]int) (map[string]int, error) { 19 | m["boo"] = 1 20 | return m, nil 21 | }) 22 | Equal(t, err, nil) 23 | 24 | myMap := m.Lock() 25 | Equal(t, 2, len(myMap)) 26 | m.Unlock(optionext.None[map[string]int]()) 27 | } 28 | 29 | func TestRWMutex(t *testing.T) { 30 | m := NewRWMutex(make(map[string]int)) 31 | m.Lock()["foo"] = 1 32 | Equal(t, m.TryLock().IsOk(), false) 33 | Equal(t, m.TryRLock().IsOk(), false) 34 | m.Unlock(optionext.None[map[string]int]()) 35 | 36 | err := m.PerformMut(func(m map[string]int) (map[string]int, error) { 37 | m["boo"] = 2 38 | return m, nil 39 | }) 40 | Equal(t, err, nil) 41 | 42 | myMap := m.RLock() 43 | Equal(t, len(myMap), 2) 44 | Equal(t, m.TryRLock().IsOk(), true) 45 | m.RUnlock() 46 | 47 | err = m.Perform(func(m map[string]int) error { 48 | Equal(t, 1, m["foo"]) 49 | Equal(t, 2, m["boo"]) 50 | return nil 51 | }) 52 | Equal(t, err, nil) 53 | } 54 | -------------------------------------------------------------------------------- /runtime/stack.go: -------------------------------------------------------------------------------- 1 | package runtimeext 2 | 3 | import ( 4 | "runtime" 5 | "strings" 6 | ) 7 | 8 | // Frame wraps a runtime.Frame to provide some helper functions while still allowing access to 9 | // the original runtime.Frame 10 | type Frame struct { 11 | runtime.Frame 12 | } 13 | 14 | // File is the runtime.Frame.File stripped down to just the filename 15 | func (f Frame) File() string { 16 | name := f.Frame.File 17 | i := strings.LastIndexByte(name, '/') 18 | return name[i+1:] 19 | } 20 | 21 | // Line is the line of the runtime.Frame and exposed for convenience. 22 | func (f Frame) Line() int { 23 | return f.Frame.Line 24 | } 25 | 26 | // Function is the runtime.Frame.Function stripped down to just the function name 27 | func (f Frame) Function() string { 28 | name := f.Frame.Function 29 | i := strings.LastIndexByte(name, '.') 30 | return name[i+1:] 31 | } 32 | 33 | // Stack returns a stack Frame 34 | func Stack() Frame { 35 | return StackLevel(1) 36 | } 37 | 38 | // StackLevel returns a stack Frame skipping the number of supplied frames. 39 | // This is primarily used by other libraries who use this package 40 | // internally as the additional. 41 | func StackLevel(skip int) (f Frame) { 42 | var frame [3]uintptr 43 | runtime.Callers(skip+2, frame[:]) 44 | frames := runtime.CallersFrames(frame[:]) 45 | f.Frame, _ = frames.Next() 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /context/context.go: -------------------------------------------------------------------------------- 1 | package contextext 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | var _ context.Context = (*detachedContext)(nil) 10 | 11 | type detachedContext struct { 12 | parent context.Context 13 | } 14 | 15 | // Detach returns a new context which continues to have access to its parent values but 16 | // is no longer bound/attached to the parents timeouts nor deadlines. 17 | // 18 | // If a nil context is passed in a new Background context will be used as the parent. 19 | // 20 | // This is useful for when you wish to pass along values such as span or logging information but do not want the 21 | // current operation to be cancelled despite what upstream code/callers think. 22 | func Detach(parent context.Context) detachedContext { 23 | if parent == nil { 24 | return detachedContext{parent: context.Background()} 25 | } 26 | return detachedContext{parent: parent} 27 | } 28 | 29 | func (c detachedContext) Deadline() (deadline time.Time, ok bool) { 30 | return 31 | } 32 | 33 | func (c detachedContext) Done() <-chan struct{} { 34 | return nil 35 | } 36 | 37 | func (c detachedContext) Err() error { 38 | return nil 39 | } 40 | 41 | func (c detachedContext) Value(key interface{}) interface{} { 42 | return c.parent.Value(key) 43 | } 44 | 45 | func (c detachedContext) String() string { 46 | return fmt.Sprintf("%s.Detached", c.parent) 47 | } 48 | -------------------------------------------------------------------------------- /io/limit_reader.go: -------------------------------------------------------------------------------- 1 | package ioext 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | ) 7 | 8 | var ( 9 | // ErrLimitedReaderEOF is an error returned by LimitedReader to give feedback to the fact that we did not hit an 10 | // EOF of the Reader but hit the limit imposed by the LimitedReader. 11 | ErrLimitedReaderEOF = errors.New("LimitedReader EOF: limit reached") 12 | ) 13 | 14 | // LimitReader returns a LimitedReader that reads from r 15 | // but stops with ErrLimitedReaderEOF after n bytes. 16 | func LimitReader(r io.Reader, n int64) *LimitedReader { 17 | return &LimitedReader{R: r, N: n} 18 | } 19 | 20 | // A LimitedReader reads from R but limits the amount of 21 | // data returned to just N bytes. Each call to Read 22 | // updates N to reflect the new amount remaining. 23 | // Read returns ErrLimitedReaderEOF when N <= 0 or when the underlying R returns EOF. 24 | // Unlike the std io.LimitedReader this provides feedback 25 | // that the limit was reached through the returned error. 26 | type LimitedReader struct { 27 | R io.Reader 28 | N int64 // bytes allotted 29 | } 30 | 31 | func (l *LimitedReader) Read(p []byte) (n int, err error) { 32 | if int64(len(p)) > l.N { 33 | p = p[0 : l.N+1] 34 | } 35 | n, err = l.R.Read(p) 36 | l.N -= int64(n) 37 | if err != nil { 38 | return 39 | } 40 | if l.N < 0 { 41 | return n, ErrLimitedReaderEOF 42 | } 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /net/http/helpers_test_go1.18.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package httpext 5 | 6 | import ( 7 | . "github.com/go-playground/assert/v2" 8 | bytesext "github.com/go-playground/pkg/v5/bytes" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | ) 13 | 14 | func TestDecodeResponse(t *testing.T) { 15 | 16 | type result struct { 17 | ID int `json:"id" xml:"id"` 18 | } 19 | 20 | tests := []struct { 21 | name string 22 | handler http.HandlerFunc 23 | expected result 24 | }{ 25 | { 26 | name: "Test JSON", 27 | handler: func(w http.ResponseWriter, r *http.Request) { 28 | Equal(t, JSON(w, http.StatusOK, result{ID: 3}), nil) 29 | }, 30 | expected: result{ID: 3}, 31 | }, 32 | { 33 | name: "Test XML", 34 | handler: func(w http.ResponseWriter, r *http.Request) { 35 | Equal(t, XML(w, http.StatusOK, result{ID: 5}), nil) 36 | }, 37 | expected: result{ID: 5}, 38 | }, 39 | } 40 | 41 | for _, tc := range tests { 42 | tc := tc 43 | t.Run(tc.name, func(t *testing.T) { 44 | mux := http.NewServeMux() 45 | mux.HandleFunc("/", tc.handler) 46 | 47 | server := httptest.NewServer(mux) 48 | defer server.Close() 49 | 50 | req, err := http.NewRequest(http.MethodGet, server.URL, nil) 51 | Equal(t, err, nil) 52 | 53 | resp, err := http.DefaultClient.Do(req) 54 | Equal(t, err, nil) 55 | Equal(t, resp.StatusCode, http.StatusOK) 56 | 57 | res, err := DecodeResponse[result](resp, bytesext.MiB) 58 | Equal(t, err, nil) 59 | Equal(t, tc.expected.ID, res.ID) 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /net/http/retryable_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package httpext 5 | 6 | import ( 7 | "context" 8 | . "github.com/go-playground/assert/v2" 9 | bytesext "github.com/go-playground/pkg/v5/bytes" 10 | errorsext "github.com/go-playground/pkg/v5/errors" 11 | optionext "github.com/go-playground/pkg/v5/values/option" 12 | "net/http" 13 | "net/http/httptest" 14 | "testing" 15 | ) 16 | 17 | func TestDoRetryable(t *testing.T) { 18 | 19 | ctx := context.Background() 20 | type response struct { 21 | Name string `json:"name"` 22 | } 23 | expected := "Joey Bloggs" 24 | var requests int 25 | 26 | mux := http.NewServeMux() 27 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 28 | requests++ 29 | if requests < 3 { 30 | w.WriteHeader(http.StatusServiceUnavailable) 31 | return 32 | } 33 | Equal(t, JSON(w, http.StatusOK, response{Name: expected}), nil) 34 | }) 35 | 36 | server := httptest.NewServer(mux) 37 | defer server.Close() 38 | 39 | fn := func(ctx context.Context) (*http.Request, error) { 40 | return http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil) 41 | } 42 | retryCount := 0 43 | dummyOnRetryFn := func(ctx context.Context, origErr error, reason string, attempt int) optionext.Option[error] { 44 | retryCount++ 45 | return optionext.None[error]() 46 | } 47 | 48 | result := DoRetryable[response](ctx, errorsext.IsRetryableHTTP, dummyOnRetryFn, IsRetryableStatusCode, nil, http.StatusOK, bytesext.MiB, fn) 49 | Equal(t, result.IsErr(), false) 50 | Equal(t, result.Unwrap().Name, expected) 51 | Equal(t, retryCount, 2) 52 | } 53 | -------------------------------------------------------------------------------- /errors/do.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package errorsext 5 | 6 | import ( 7 | "context" 8 | 9 | optionext "github.com/go-playground/pkg/v5/values/option" 10 | resultext "github.com/go-playground/pkg/v5/values/result" 11 | ) 12 | 13 | // RetryableFn is a function that can be retried. 14 | type RetryableFn[T, E any] func(ctx context.Context) resultext.Result[T, E] 15 | 16 | // IsRetryableFn is called to determine if the error is retryable and optionally returns the reason for logging and metrics. 17 | type IsRetryableFn[E any] func(err E) (reason string, isRetryable bool) 18 | 19 | // OnRetryFn is called after IsRetryableFn returns true and before the retry is attempted. 20 | // 21 | // this allows for interception, short-circuiting and adding of backoff strategies. 22 | type OnRetryFn[E any] func(ctx context.Context, originalErr E, reason string, attempt int) optionext.Option[E] 23 | 24 | // DoRetryable will execute the provided functions code and automatically retry using the provided retry function. 25 | // 26 | // Deprecated: use `errorsext.Retrier` instead which corrects design issues with the current implementation. 27 | func DoRetryable[T, E any](ctx context.Context, isRetryFn IsRetryableFn[E], onRetryFn OnRetryFn[E], fn RetryableFn[T, E]) resultext.Result[T, E] { 28 | var attempt int 29 | for { 30 | result := fn(ctx) 31 | if result.IsErr() { 32 | err := result.Err() 33 | if reason, isRetryable := isRetryFn(err); isRetryable { 34 | if opt := onRetryFn(ctx, err, reason, attempt); opt.IsSome() { 35 | return resultext.Err[T, E](opt.Unwrap()) 36 | } 37 | attempt++ 38 | continue 39 | } 40 | } 41 | return result 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /net/http/mime_types.go: -------------------------------------------------------------------------------- 1 | package httpext 2 | 3 | const ( 4 | charsetUTF8 = "; charset=" + UTF8 5 | ) 6 | 7 | // Mime Type values for the Content-Type HTTP header 8 | const ( 9 | ApplicationJSONNoCharset string = "application/json" 10 | ApplicationJSON string = ApplicationJSONNoCharset + charsetUTF8 11 | ApplicationJavaScript string = "application/javascript" 12 | ApplicationXMLNoCharset string = "application/xml" 13 | ApplicationXML string = ApplicationXMLNoCharset + charsetUTF8 14 | ApplicationForm string = "application/x-www-form-urlencoded" 15 | ApplicationProtobuf string = "application/protobuf" 16 | ApplicationMsgpack string = "application/msgpack" 17 | ApplicationWasm string = "application/wasm" 18 | ApplicationPDF string = "application/pdf" 19 | ApplicationOctetStream string = "application/octet-stream" 20 | TextHTMLNoCharset = "text/html" 21 | TextHTML string = TextHTMLNoCharset + charsetUTF8 22 | TextPlainNoCharset = "text/plain" 23 | TextPlain string = TextPlainNoCharset + charsetUTF8 24 | TextMarkdownNoCharset string = "text/markdown" 25 | TextMarkdown string = TextMarkdownNoCharset + charsetUTF8 26 | TextCSSNoCharset string = "text/css" 27 | TextCSS string = TextCSSNoCharset + charsetUTF8 28 | TextCSV string = "text/csv" 29 | ImagePNG string = "image/png" 30 | ImageGIF string = "image/gif" 31 | ImageSVG string = "image/svg+xml" 32 | ImageJPEG string = "image/jpeg" 33 | MultipartForm string = "multipart/form-data" 34 | ) 35 | -------------------------------------------------------------------------------- /sync/mutex2_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package syncext 5 | 6 | import ( 7 | resultext "github.com/go-playground/pkg/v5/values/result" 8 | "sync" 9 | "testing" 10 | 11 | . "github.com/go-playground/assert/v2" 12 | ) 13 | 14 | func TestMutex2(t *testing.T) { 15 | m := NewMutex2(make(map[string]int)) 16 | guard := m.Lock() 17 | guard.T["foo"] = 1 18 | guard.Unlock() 19 | 20 | m.PerformMut(func(m map[string]int) { 21 | m["boo"] = 1 22 | }) 23 | guard = m.Lock() 24 | myMap := guard.T 25 | Equal(t, 2, len(myMap)) 26 | Equal(t, myMap["foo"], 1) 27 | Equal(t, myMap["boo"], 1) 28 | Equal(t, m.TryLock(), resultext.Err[MutexGuard[map[string]int, *sync.Mutex]](struct{}{})) 29 | guard.Unlock() 30 | 31 | result := m.TryLock() 32 | Equal(t, result.IsOk(), true) 33 | result.Unwrap().Unlock() 34 | } 35 | 36 | func TestRWMutex2(t *testing.T) { 37 | m := NewRWMutex2(make(map[string]int)) 38 | guard := m.Lock() 39 | guard.T["foo"] = 1 40 | Equal(t, m.TryLock().IsOk(), false) 41 | Equal(t, m.TryRLock().IsOk(), false) 42 | guard.Unlock() 43 | 44 | m.PerformMut(func(m map[string]int) { 45 | m["boo"] = 2 46 | }) 47 | guard = m.Lock() 48 | mp := guard.T 49 | Equal(t, mp["foo"], 1) 50 | Equal(t, mp["boo"], 2) 51 | guard.Unlock() 52 | 53 | rguard := m.RLock() 54 | myMap := rguard.T 55 | Equal(t, len(myMap), 2) 56 | Equal(t, m.TryRLock().IsOk(), true) 57 | rguard.RUnlock() 58 | 59 | m.Perform(func(m map[string]int) { 60 | Equal(t, 1, m["foo"]) 61 | Equal(t, 2, m["boo"]) 62 | }) 63 | rguard = m.RLock() 64 | myMap = rguard.T 65 | Equal(t, len(myMap), 2) 66 | Equal(t, myMap["foo"], 1) 67 | Equal(t, myMap["boo"], 2) 68 | rguard.RUnlock() 69 | } 70 | -------------------------------------------------------------------------------- /net/http/helpers_go1.18.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package httpext 5 | 6 | import ( 7 | "errors" 8 | "net/http" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | asciiext "github.com/go-playground/pkg/v5/ascii" 14 | bytesext "github.com/go-playground/pkg/v5/bytes" 15 | . "github.com/go-playground/pkg/v5/values/option" 16 | ) 17 | 18 | // DecodeResponse takes the response and attempts to discover its content type via 19 | // the http headers and then decode the request body into the provided type. 20 | // 21 | // Example if header was "application/json" would decode using 22 | // json.NewDecoder(ioext.LimitReader(r.Body, maxBytes)).Decode(v). 23 | func DecodeResponse[T any](r *http.Response, maxMemory bytesext.Bytes) (result T, err error) { 24 | typ := r.Header.Get(ContentType) 25 | if idx := strings.Index(typ, ";"); idx != -1 { 26 | typ = typ[:idx] 27 | } 28 | switch typ { 29 | case nakedApplicationJSON: 30 | err = decodeJSON(r.Header, r.Body, NoQueryParams, nil, maxMemory, &result) 31 | case nakedApplicationXML: 32 | err = decodeXML(r.Header, r.Body, NoQueryParams, nil, maxMemory, &result) 33 | default: 34 | err = errors.New("unsupported content type") 35 | } 36 | return 37 | } 38 | 39 | // HasRetryAfter parses the Retry-After header and returns the duration if possible. 40 | func HasRetryAfter(headers http.Header) Option[time.Duration] { 41 | if ra := headers.Get(RetryAfter); ra != "" { 42 | if asciiext.IsDigit(ra[0]) { 43 | if n, err := strconv.ParseInt(ra, 10, 64); err == nil { 44 | return Some(time.Duration(n) * time.Second) 45 | } 46 | } else { 47 | // not a number so must be a date in the future 48 | if t, err := http.ParseTime(ra); err == nil { 49 | return Some(time.Until(t)) 50 | } 51 | } 52 | } 53 | return None[time.Duration]() 54 | } 55 | -------------------------------------------------------------------------------- /_examples/net/http/retrier/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "time" 9 | 10 | appext "github.com/go-playground/pkg/v5/app" 11 | errorsext "github.com/go-playground/pkg/v5/errors" 12 | httpext "github.com/go-playground/pkg/v5/net/http" 13 | . "github.com/go-playground/pkg/v5/values/result" 14 | ) 15 | 16 | // customize as desired to meet your needs including custom retryable status codes, errors etc. 17 | var retrier = httpext.NewRetryer() 18 | 19 | func main() { 20 | ctx := appext.Context().Build() 21 | 22 | type Test struct { 23 | Date time.Time 24 | } 25 | var count int 26 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | if count < 2 { 28 | count++ 29 | http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests) 30 | return 31 | } 32 | _ = httpext.JSON(w, http.StatusOK, Test{Date: time.Now().UTC()}) 33 | })) 34 | defer server.Close() 35 | 36 | // fetch response 37 | fn := func(ctx context.Context) Result[*http.Request, error] { 38 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil) 39 | if err != nil { 40 | return Err[*http.Request, error](err) 41 | } 42 | return Ok[*http.Request, error](req) 43 | } 44 | 45 | var result Test 46 | err := retrier.Do(ctx, fn, &result, http.StatusOK) 47 | if err != nil { 48 | panic(err) 49 | } 50 | fmt.Printf("Response: %+v\n", result) 51 | 52 | // `Retrier` configuration is copy and so the base `Retrier` can be used and even customized for one-off requests. 53 | // eg for this request we change the max attempts from the default configuration. 54 | err = retrier.MaxAttempts(errorsext.MaxAttempts, 2).Do(ctx, fn, &result, http.StatusOK) 55 | if err != nil { 56 | panic(err) 57 | } 58 | fmt.Printf("Response: %+v\n", result) 59 | } 60 | -------------------------------------------------------------------------------- /io/limit_reader_test.go: -------------------------------------------------------------------------------- 1 | package ioext 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestLimitedReader_Read(t *testing.T) { 9 | eofLimited := LimitReader(strings.NewReader("all"), 5) 10 | 11 | type args struct { 12 | p []byte 13 | } 14 | tests := []struct { 15 | name string 16 | l *LimitedReader 17 | args args 18 | wantN int 19 | wantErr bool 20 | }{ 21 | { 22 | name: "not-limited", 23 | l: LimitReader(strings.NewReader("all"), 3), 24 | args: args{p: make([]byte, 4)}, 25 | wantN: 3, 26 | wantErr: false, 27 | }, 28 | { 29 | name: "not-limited-exact", 30 | l: LimitReader(strings.NewReader("all"), 3), 31 | args: args{p: make([]byte, 3)}, 32 | wantN: 3, 33 | wantErr: false, 34 | }, 35 | { 36 | name: "not-limited-EOF-OK", 37 | l: eofLimited, 38 | args: args{p: make([]byte, 4)}, 39 | wantN: 3, 40 | wantErr: false, 41 | }, 42 | { 43 | name: "not-limited-EOF", 44 | l: eofLimited, 45 | args: args{p: make([]byte, 4)}, 46 | wantN: 0, 47 | wantErr: true, 48 | }, 49 | { 50 | name: "limited", 51 | l: LimitReader(strings.NewReader("limited"), 1), 52 | args: args{p: make([]byte, 3)}, 53 | wantN: 2, // need to read one past to know we're past 54 | wantErr: true, 55 | }, 56 | { 57 | name: "limited-buff-under-N", 58 | l: LimitReader(strings.NewReader("limited"), 0), 59 | args: args{p: make([]byte, 1)}, 60 | wantN: 1, 61 | wantErr: true, 62 | }, 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | gotN, err := tt.l.Read(tt.args.p) 67 | if (err != nil) != tt.wantErr { 68 | t.Errorf("LimitedReader.Read() error = %v, wantErr %v", err, tt.wantErr) 69 | return 70 | } 71 | if gotN != tt.wantN { 72 | t.Errorf("LimitedReader.Read() = %v, want %v", gotN, tt.wantN) 73 | } 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pkg 2 | 3 | ![Project status](https://img.shields.io/badge/version-5.30.0-green.svg) 4 | [![Lint & Test](https://github.com/go-playground/pkg/actions/workflows/go.yml/badge.svg)](https://github.com/go-playground/pkg/actions/workflows/go.yml) 5 | [![Coverage Status](https://coveralls.io/repos/github/go-playground/pkg/badge.svg?branch=master)](https://coveralls.io/github/go-playground/pkg?branch=master) 6 | [![GoDoc](https://godoc.org/github.com/go-playground/pkg?status.svg)](https://pkg.go.dev/mod/github.com/go-playground/pkg/v5) 7 | ![License](https://img.shields.io/dub/l/vibe-d.svg) 8 | 9 | pkg extends the core Go packages with missing or additional functionality built in. All packages correspond to the std go package name with an additional suffix of `ext` to avoid naming conflicts. 10 | 11 | ## Motivation 12 | 13 | This is a place to put common reusable code that is not quite a library but extends upon the core library, or it's failings. 14 | 15 | ## Install 16 | 17 | `go get -u github.com/go-playground/pkg/v5` 18 | 19 | 20 | ## Highlights 21 | - Generic Doubly Linked List. 22 | - Result & Option types 23 | - Generic Mutex and RWMutex. 24 | - Bytes helper placeholders units eg. MB, MiB, GB, ... 25 | - Detachable context. 26 | - Retrier for helping with any fallible operation. 27 | - Proper RFC3339Nano definition. 28 | - unsafe []byte->string & string->[]byte helper functions. 29 | - HTTP helper functions and constant placeholders. 30 | - And much, much more. 31 | 32 | ## How to Contribute 33 | 34 | Make a pull request... can't guarantee it will be added, going to strictly vet what goes in. 35 | 36 | ## License 37 | 38 | 39 | Licensed under either of Apache License, Version 40 | 2.0 or MIT license at your option. 41 | 42 | 43 |
44 | 45 | 46 | Unless you explicitly state otherwise, any contribution intentionally submitted 47 | for inclusion in this package by you, as defined in the Apache-2.0 license, shall be 48 | dual licensed as above, without any additional terms or conditions. 49 | 50 | -------------------------------------------------------------------------------- /app/context_test.go: -------------------------------------------------------------------------------- 1 | package appext 2 | 3 | import ( 4 | "context" 5 | . "github.com/go-playground/assert/v2" 6 | "os" 7 | "os/signal" 8 | "sync" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestForceExitWithNoTimeout(t *testing.T) { 14 | var wg sync.WaitGroup 15 | wg.Add(1) 16 | exitFn := func(code int) { 17 | defer wg.Done() 18 | Equal(t, 1, code) 19 | } 20 | 21 | c := Context().Timeout(0).ExitFn(exitFn) 22 | 23 | // copy of Build for testing 24 | var sig = make(chan os.Signal, 1) 25 | signal.Notify(sig, c.signals...) 26 | 27 | ctx, cancel := context.WithCancel(context.Background()) 28 | 29 | go listen(sig, cancel, c.exitFn, c.timeout, c.forceExit) 30 | 31 | sig <- os.Interrupt 32 | sig <- os.Interrupt 33 | wg.Wait() 34 | Equal(t, context.Canceled, ctx.Err()) 35 | } 36 | 37 | func TestForceExitWithTimeout(t *testing.T) { 38 | var wg sync.WaitGroup 39 | wg.Add(1) 40 | exitFn := func(code int) { 41 | defer wg.Done() 42 | Equal(t, 1, code) 43 | } 44 | 45 | c := Context().Timeout(time.Hour).ExitFn(exitFn) 46 | 47 | // copy of Build for testing 48 | var sig = make(chan os.Signal, 1) 49 | signal.Notify(sig, c.signals...) 50 | 51 | ctx, cancel := context.WithCancel(context.Background()) 52 | 53 | go listen(sig, cancel, c.exitFn, c.timeout, c.forceExit) 54 | 55 | sig <- os.Interrupt 56 | sig <- os.Interrupt 57 | wg.Wait() 58 | Equal(t, context.Canceled, ctx.Err()) 59 | } 60 | 61 | func TestTimeoutWithNoForceExit(t *testing.T) { 62 | var wg sync.WaitGroup 63 | wg.Add(1) 64 | exitFn := func(code int) { 65 | defer wg.Done() 66 | Equal(t, 1, code) 67 | } 68 | 69 | c := Context().Timeout(time.Millisecond * 200).ForceExit(false).ExitFn(exitFn) 70 | 71 | // copy of Build for testing 72 | var sig = make(chan os.Signal, 1) 73 | signal.Notify(sig, c.signals...) 74 | 75 | ctx, cancel := context.WithCancel(context.Background()) 76 | 77 | go listen(sig, cancel, c.exitFn, c.timeout, c.forceExit) 78 | 79 | // only sending one, timeout must be reached for test to finish 80 | sig <- os.Interrupt 81 | wg.Wait() 82 | Equal(t, context.Canceled, ctx.Err()) 83 | } 84 | -------------------------------------------------------------------------------- /slice/slice_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package sliceext 5 | 6 | import ( 7 | . "github.com/go-playground/assert/v2" 8 | optionext "github.com/go-playground/pkg/v5/values/option" 9 | "strconv" 10 | "testing" 11 | ) 12 | 13 | func TestFilter(t *testing.T) { 14 | s := Filter([]int{0, 1, 2, 3}, func(v int) bool { 15 | return v > 0 && v < 3 16 | }) 17 | Equal(t, len(s), 2) 18 | Equal(t, s[0], 0) 19 | Equal(t, s[1], 3) 20 | 21 | } 22 | 23 | func TestRetain(t *testing.T) { 24 | s := Retain([]int{0, 1, 2, 3}, func(v int) bool { 25 | return v > 0 && v < 3 26 | }) 27 | Equal(t, len(s), 2) 28 | Equal(t, s[0], 1) 29 | Equal(t, s[1], 2) 30 | } 31 | 32 | func TestMap(t *testing.T) { 33 | s := Map[int, []string]([]int{0, 1, 2, 3}, make([]string, 0, 4), func(accum []string, v int) []string { 34 | return append(accum, strconv.Itoa(v)) 35 | }) 36 | Equal(t, len(s), 4) 37 | Equal(t, s[0], "0") 38 | Equal(t, s[1], "1") 39 | Equal(t, s[2], "2") 40 | Equal(t, s[3], "3") 41 | 42 | // Test Map empty slice 43 | s2 := Map[int, []string](nil, nil, func(accum []string, v int) []string { 44 | return append(accum, strconv.Itoa(v)) 45 | }) 46 | Equal(t, len(s2), 0) 47 | } 48 | 49 | func TestSort(t *testing.T) { 50 | s := []int{0, 1, 2} 51 | Sort(s, func(i int, j int) bool { 52 | return i > j 53 | }) 54 | Equal(t, s[0], 2) 55 | Equal(t, s[1], 1) 56 | Equal(t, s[2], 0) 57 | } 58 | 59 | func TestSortStable(t *testing.T) { 60 | s := []int{0, 1, 1, 2} 61 | SortStable(s, func(i int, j int) bool { 62 | return i > j 63 | }) 64 | Equal(t, s[0], 2) 65 | Equal(t, s[1], 1) 66 | Equal(t, s[2], 1) 67 | Equal(t, s[3], 0) 68 | } 69 | 70 | func TestReduce(t *testing.T) { 71 | result := Reduce([]int{0, 1, 2}, func(accum int, current int) int { 72 | return accum + current 73 | }) 74 | Equal(t, result, optionext.Some(3)) 75 | 76 | // Test Reduce empty slice 77 | result = Reduce([]int{}, func(accum int, current int) int { 78 | return accum + current 79 | }) 80 | Equal(t, result, optionext.None[int]()) 81 | } 82 | 83 | func TestReverse(t *testing.T) { 84 | s := []int{1, 2} 85 | Reverse(s) 86 | Equal(t, []int{2, 1}, s) 87 | 88 | s = []int{1, 2, 3} 89 | Reverse(s) 90 | Equal(t, []int{3, 2, 1}, s) 91 | } 92 | 93 | func BenchmarkReverse(b *testing.B) { 94 | s := make([]int, 0, 1000) 95 | for i := 0; i < 1000; i++ { 96 | s = append(s, i) 97 | } 98 | b.ResetTimer() 99 | for i := 0; i < b.N; i++ { 100 | Reverse(s) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /slice/slice.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package sliceext 5 | 6 | import ( 7 | "sort" 8 | 9 | optionext "github.com/go-playground/pkg/v5/values/option" 10 | ) 11 | 12 | // Retain retains only the elements specified by the function. 13 | // 14 | // This returns a new slice with references to the underlying data instead of shuffling. 15 | func Retain[T any](slice []T, fn func(v T) bool) []T { 16 | results := make([]T, 0, len(slice)) 17 | for _, v := range slice { 18 | v := v 19 | if fn(v) { 20 | results = append(results, v) 21 | } 22 | } 23 | return results 24 | } 25 | 26 | // Filter filters out the elements specified by the function. 27 | // 28 | // This returns a new slice with references to the underlying data instead of shuffling. 29 | func Filter[T any](slice []T, fn func(v T) bool) []T { 30 | results := make([]T, 0, len(slice)) 31 | for _, v := range slice { 32 | v := v 33 | if fn(v) { 34 | continue 35 | } 36 | results = append(results, v) 37 | } 38 | return results 39 | } 40 | 41 | // Map maps a slice of []T -> []U using the map function. 42 | func Map[T, U any](slice []T, init U, fn func(accum U, v T) U) U { 43 | if len(slice) == 0 { 44 | return init 45 | } 46 | accum := init 47 | for _, v := range slice { 48 | accum = fn(accum, v) 49 | } 50 | return accum 51 | } 52 | 53 | // Sort sorts the sliceWrapper x given the provided less function. 54 | // 55 | // The sort is not guaranteed to be stable: equal elements 56 | // may be reversed from their original order. 57 | // 58 | // For a stable sort, use SortStable. 59 | func Sort[T any](slice []T, less func(i T, j T) bool) { 60 | sort.Slice(slice, func(j, k int) bool { 61 | return less(slice[j], slice[k]) 62 | }) 63 | } 64 | 65 | // SortStable sorts the sliceWrapper x using the provided less 66 | // function, keeping equal elements in their original order. 67 | func SortStable[T any](slice []T, less func(i T, j T) bool) { 68 | sort.SliceStable(slice, func(j, k int) bool { 69 | return less(slice[j], slice[k]) 70 | }) 71 | } 72 | 73 | // Reduce reduces the elements to a single one, by repeatedly applying a reducing function. 74 | func Reduce[T any](slice []T, fn func(accum T, current T) T) optionext.Option[T] { 75 | if len(slice) == 0 { 76 | return optionext.None[T]() 77 | } 78 | accum := slice[0] 79 | for _, v := range slice { 80 | accum = fn(accum, v) 81 | } 82 | return optionext.Some(accum) 83 | } 84 | 85 | // Reverse reverses the slice contents. 86 | func Reverse[T any](slice []T) { 87 | for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 { 88 | slice[i], slice[j] = slice[j], slice[i] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /values/result/result.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package resultext 5 | 6 | // Result represents the result of an operation that is successful or not. 7 | type Result[T, E any] struct { 8 | ok T 9 | err E 10 | isOk bool 11 | } 12 | 13 | // Ok returns a Result that contains the given values. 14 | func Ok[T, E any](value T) Result[T, E] { 15 | return Result[T, E]{ok: value, isOk: true} 16 | } 17 | 18 | // Err returns a Result that contains the given error. 19 | func Err[T, E any](err E) Result[T, E] { 20 | return Result[T, E]{err: err} 21 | } 22 | 23 | // IsOk returns true if the result is successful with no error. 24 | func (r Result[T, E]) IsOk() bool { 25 | return r.isOk 26 | } 27 | 28 | // IsErr returns true if the result is not successful and has an error. 29 | func (r Result[T, E]) IsErr() bool { 30 | return !r.isOk 31 | } 32 | 33 | // Unwrap returns the values of the result. It panics if there is no result due to not checking for errors. 34 | func (r Result[T, E]) Unwrap() T { 35 | if r.isOk { 36 | return r.ok 37 | } 38 | panic("Result.Unwrap(): result is Err") 39 | } 40 | 41 | // UnwrapOr returns the contained Ok value or a provided default. 42 | // 43 | // Arguments passed to UnwrapOr are eagerly evaluated; if you are passing the result of a function call, 44 | // look to use `UnwrapOrElse`, which can be lazily evaluated. 45 | func (r Result[T, E]) UnwrapOr(value T) T { 46 | if r.isOk { 47 | return r.ok 48 | } 49 | return value 50 | } 51 | 52 | // UnwrapOrElse returns the contained Ok value or computes it from a provided function. 53 | func (r Result[T, E]) UnwrapOrElse(fn func() T) T { 54 | if r.isOk { 55 | return r.ok 56 | } 57 | return fn() 58 | } 59 | 60 | // UnwrapOrDefault returns the contained Ok value or the default value of the type T. 61 | func (r Result[T, E]) UnwrapOrDefault() T { 62 | return r.ok 63 | } 64 | 65 | // And calls the provided function with the contained value if the result is Ok, returns the Result value otherwise. 66 | func (r Result[T, E]) And(fn func(T) T) Result[T, E] { 67 | if r.isOk { 68 | r.ok = fn(r.ok) 69 | } 70 | return r 71 | } 72 | 73 | // AndThen calls the provided function with the contained value if the result is Ok, returns the Result value otherwise. 74 | // 75 | // This differs from `And` in that the provided function returns a Result[T, E] allowing changing of the Option value 76 | // itself. 77 | func (r Result[T, E]) AndThen(fn func(T) Result[T, E]) Result[T, E] { 78 | if r.isOk { 79 | return fn(r.ok) 80 | } 81 | return r 82 | } 83 | 84 | // Err returns the error of the result. To be used after calling IsOK() 85 | func (r Result[T, E]) Err() E { 86 | return r.err 87 | } 88 | -------------------------------------------------------------------------------- /values/result/result_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package resultext 5 | 6 | import ( 7 | "errors" 8 | "io" 9 | "testing" 10 | 11 | . "github.com/go-playground/assert/v2" 12 | ) 13 | 14 | type myStruct struct{} 15 | 16 | func TestAndXXX(t *testing.T) { 17 | ok := Ok[int, error](1) 18 | Equal(t, Ok[int, error](3), ok.And(func(int) int { return 3 })) 19 | Equal(t, Ok[int, error](3), ok.AndThen(func(int) Result[int, error] { return Ok[int, error](3) })) 20 | Equal(t, Err[int, error](io.EOF), ok.AndThen(func(int) Result[int, error] { return Err[int, error](io.EOF) })) 21 | 22 | err := Err[int, error](io.EOF) 23 | Equal(t, Err[int, error](io.EOF), err.And(func(int) int { return 3 })) 24 | Equal(t, Err[int, error](io.EOF), err.AndThen(func(int) Result[int, error] { return Ok[int, error](3) })) 25 | Equal(t, Err[int, error](io.EOF), err.AndThen(func(int) Result[int, error] { return Err[int, error](io.ErrUnexpectedEOF) })) 26 | Equal(t, Err[int, error](io.ErrUnexpectedEOF), ok.AndThen(func(int) Result[int, error] { return Err[int, error](io.ErrUnexpectedEOF) })) 27 | } 28 | 29 | func TestUnwrap(t *testing.T) { 30 | er := Err[int, error](io.EOF) 31 | PanicMatches(t, func() { er.Unwrap() }, "Result.Unwrap(): result is Err") 32 | 33 | v := er.UnwrapOr(3) 34 | Equal(t, 3, v) 35 | 36 | v = er.UnwrapOrElse(func() int { return 2 }) 37 | Equal(t, 2, v) 38 | 39 | v = er.UnwrapOrDefault() 40 | Equal(t, 0, v) 41 | } 42 | 43 | func TestResult(t *testing.T) { 44 | result := returnOk() 45 | Equal(t, true, result.IsOk()) 46 | Equal(t, false, result.IsErr()) 47 | Equal(t, true, result.Err() == nil) 48 | Equal(t, myStruct{}, result.Unwrap()) 49 | 50 | result = returnErr() 51 | Equal(t, false, result.IsOk()) 52 | Equal(t, true, result.IsErr()) 53 | Equal(t, false, result.Err() == nil) 54 | PanicMatches(t, func() { 55 | result.Unwrap() 56 | }, "Result.Unwrap(): result is Err") 57 | } 58 | 59 | func returnOk() Result[myStruct, error] { 60 | return Ok[myStruct, error](myStruct{}) 61 | } 62 | 63 | func returnErr() Result[myStruct, error] { 64 | return Err[myStruct, error](errors.New("bad")) 65 | } 66 | 67 | func BenchmarkResultOk(b *testing.B) { 68 | for i := 0; i < b.N; i++ { 69 | res := returnOk() 70 | if res.IsOk() { 71 | _ = res.Unwrap() 72 | } 73 | } 74 | } 75 | 76 | func BenchmarkResultErr(b *testing.B) { 77 | for i := 0; i < b.N; i++ { 78 | res := returnErr() 79 | if res.IsOk() { 80 | _ = res.Unwrap() 81 | } 82 | } 83 | } 84 | 85 | func BenchmarkNoResultOk(b *testing.B) { 86 | for i := 0; i < b.N; i++ { 87 | res, err := returnOkNoResult() 88 | if err != nil { 89 | _ = res 90 | } 91 | } 92 | } 93 | 94 | func BenchmarkNoResultErr(b *testing.B) { 95 | for i := 0; i < b.N; i++ { 96 | res, err := returnErrNoResult() 97 | if err != nil { 98 | _ = res 99 | } 100 | } 101 | } 102 | 103 | func returnOkNoResult() (myStruct, error) { 104 | return myStruct{}, nil 105 | } 106 | 107 | func returnErrNoResult() (myStruct, error) { 108 | return myStruct{}, errors.New("bad") 109 | } 110 | -------------------------------------------------------------------------------- /app/context.go: -------------------------------------------------------------------------------- 1 | package appext 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | ) 11 | 12 | type contextBuilder struct { 13 | signals []os.Signal 14 | timeout time.Duration 15 | exitFn func(int) 16 | forceExit bool 17 | } 18 | 19 | // Context returns a new context builder, with sane defaults, that can be overridden. Calling `Build()` finalizes 20 | // the new desired context and returns the configured `context.Context`. 21 | func Context() *contextBuilder { 22 | return &contextBuilder{ 23 | signals: []os.Signal{ 24 | os.Interrupt, 25 | syscall.SIGTERM, 26 | syscall.SIGQUIT, 27 | }, 28 | timeout: 30 * time.Second, 29 | forceExit: true, 30 | exitFn: os.Exit, 31 | } 32 | } 33 | 34 | // Signals sets the signals to listen for. Defaults to `os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT`. 35 | func (c *contextBuilder) Signals(signals ...os.Signal) *contextBuilder { 36 | c.signals = signals 37 | return c 38 | } 39 | 40 | // Timeout sets the timeout for graceful shutdown before forcing the issue exiting with exit code 1. 41 | // Defaults to 30 seconds. 42 | // 43 | // A timeout of <= 0, not recommended, disables the timeout and will wait forever for a seconds signal or application 44 | // shuts down. 45 | func (c *contextBuilder) Timeout(timeout time.Duration) *contextBuilder { 46 | c.timeout = timeout 47 | return c 48 | } 49 | 50 | // ForceExit sets whether to force terminate ungracefully upon receipt of a second signal. Defaults to true. 51 | func (c *contextBuilder) ForceExit(forceExit bool) *contextBuilder { 52 | c.forceExit = forceExit 53 | return c 54 | } 55 | 56 | // ExitFn sets the exit function to use. Defaults to `os.Exit`. 57 | // 58 | // This is used in the unit tests but can be used to intercept the exit call and do something else as needed also. 59 | func (c *contextBuilder) ExitFn(exitFn func(int)) *contextBuilder { 60 | c.exitFn = exitFn 61 | return c 62 | } 63 | 64 | // Build finalizes the context builder and returns the configured `context.Context`. 65 | // 66 | // This will spawn another goroutine listening for the configured signals and will cancel the context when received with 67 | // the configured settings. 68 | func (c *contextBuilder) Build() context.Context { 69 | var sig = make(chan os.Signal, 1) 70 | signal.Notify(sig, c.signals...) 71 | 72 | ctx, cancel := context.WithCancel(context.Background()) 73 | 74 | go listen(sig, cancel, c.exitFn, c.timeout, c.forceExit) 75 | 76 | return ctx 77 | } 78 | 79 | func listen(sig <-chan os.Signal, cancel context.CancelFunc, exitFn func(int), timeout time.Duration, forceExit bool) { 80 | s := <-sig 81 | cancel() 82 | log.Printf("received shutdown signal %q\n", s) 83 | 84 | if timeout > 0 { 85 | select { 86 | case s := <-sig: 87 | if forceExit { 88 | log.Printf("received second shutdown signal %q, forcing exit\n", s) 89 | exitFn(1) 90 | } 91 | case <-time.After(timeout): 92 | log.Printf("timeout of %s reached, forcing exit\n", timeout) 93 | exitFn(1) 94 | } 95 | } else { 96 | s = <-sig 97 | if forceExit { 98 | log.Printf("received second shutdown signal %q, forcing exit\n", s) 99 | exitFn(1) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /values/option/option_common.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package optionext 5 | 6 | import ( 7 | "encoding/json" 8 | ) 9 | 10 | // Option represents a values that represents a values existence. 11 | // 12 | // nil is usually used on Go however this has two problems: 13 | // 1. Checking if the return values is nil is NOT enforced and can lead to panics. 14 | // 2. Using nil is not good enough when nil itself is a valid value. 15 | // 16 | // This implements the sql.Scanner interface and can be used as a sql value for reading and writing. It supports: 17 | // - String 18 | // - Bool 19 | // - Uint8 20 | // - Float64 21 | // - Int16 22 | // - Int32 23 | // - Int64 24 | // - interface{}/any 25 | // - time.Time 26 | // - Struct - when type is convertable to []byte and assumes JSON. 27 | // - Slice - when type is convertable to []byte and assumes JSON. 28 | // - Map types - when type is convertable to []byte and assumes JSON. 29 | // 30 | // This also implements the `json.Marshaler` and `json.Unmarshaler` interfaces. The only caveat is a None value will result 31 | // in a JSON `null` value. there is no way to hook into the std library to make `omitempty` not produce any value at 32 | // this time. 33 | type Option[T any] struct { 34 | value T 35 | isSome bool 36 | } 37 | 38 | // IsSome returns true if the option is not empty. 39 | func (o Option[T]) IsSome() bool { 40 | return o.isSome 41 | } 42 | 43 | // IsNone returns true if the option is empty. 44 | func (o Option[T]) IsNone() bool { 45 | return !o.isSome 46 | } 47 | 48 | // Unwrap returns the values if the option is not empty or panics. 49 | func (o Option[T]) Unwrap() T { 50 | if o.isSome { 51 | return o.value 52 | } 53 | panic("Option.Unwrap: option is None") 54 | } 55 | 56 | // UnwrapOr returns the contained `Some` value or provided default value. 57 | // 58 | // Arguments passed to `UnwrapOr` are eagerly evaluated; if you are passing the result of a function call, 59 | // look to use `UnwrapOrElse`, which can be lazily evaluated. 60 | func (o Option[T]) UnwrapOr(value T) T { 61 | if o.isSome { 62 | return o.value 63 | } 64 | return value 65 | } 66 | 67 | // UnwrapOrElse returns the contained `Some` value or computes it from a provided function. 68 | func (o Option[T]) UnwrapOrElse(fn func() T) T { 69 | if o.isSome { 70 | return o.value 71 | } 72 | return fn() 73 | } 74 | 75 | // UnwrapOrDefault returns the contained `Some` value or the default value of the type T. 76 | func (o Option[T]) UnwrapOrDefault() T { 77 | return o.value 78 | } 79 | 80 | // And calls the provided function with the contained value if the option is Some, returns the None value otherwise. 81 | func (o Option[T]) And(fn func(T) T) Option[T] { 82 | if o.isSome { 83 | o.value = fn(o.value) 84 | } 85 | return o 86 | } 87 | 88 | // AndThen calls the provided function with the contained value if the option is Some, returns the None value otherwise. 89 | // 90 | // This differs from `And` in that the provided function returns an Option[T] allowing changing of the Option value 91 | // itself. 92 | func (o Option[T]) AndThen(fn func(T) Option[T]) Option[T] { 93 | if o.isSome { 94 | return fn(o.value) 95 | } 96 | return o 97 | } 98 | 99 | // Some creates a new Option with the given values. 100 | func Some[T any](value T) Option[T] { 101 | return Option[T]{value, true} 102 | } 103 | 104 | // None creates an empty Option that represents no values. 105 | func None[T any]() Option[T] { 106 | return Option[T]{} 107 | } 108 | 109 | // MarshalJSON implements the `json.Marshaler` interface. 110 | func (o Option[T]) MarshalJSON() ([]byte, error) { 111 | if o.isSome { 112 | return json.Marshal(o.value) 113 | } 114 | return []byte("null"), nil 115 | } 116 | 117 | // UnmarshalJSON implements the `json.Unmarshaler` interface. 118 | func (o *Option[T]) UnmarshalJSON(data []byte) error { 119 | if len(data) == 4 && string(data[:4]) == "null" { 120 | *o = None[T]() 121 | return nil 122 | } 123 | var v T 124 | err := json.Unmarshal(data, &v) 125 | if err != nil { 126 | return err 127 | } 128 | *o = Some(v) 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /errors/retryable.go: -------------------------------------------------------------------------------- 1 | package errorsext 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "syscall" 7 | ) 8 | 9 | var ( 10 | // ErrMaxAttemptsReached is a placeholder error to use when some retryable even has reached its maximum number of 11 | // attempts. 12 | ErrMaxAttemptsReached = errors.New("max attempts reached") 13 | ) 14 | 15 | // IsRetryableHTTP returns if the provided error is considered retryable HTTP error. It also returns the 16 | // type, in string form, for optional logging and metrics use. 17 | func IsRetryableHTTP(err error) (retryType string, isRetryable bool) { 18 | if retryType, isRetryable = IsRetryableNetwork(err); isRetryable { 19 | return 20 | } 21 | 22 | errStr := err.Error() 23 | 24 | if strings.Contains(errStr, "http2: server sent GOAWAY") { 25 | return "goaway", true 26 | } 27 | 28 | // errServerClosedIdle is not seen by users for idempotent HTTP requests, but may be 29 | // seen by a user if the server shuts down an idle connection and sends its FIN 30 | // in flight with already-written POST body bytes from the client. 31 | // See https://github.com/golang/go/issues/19943#issuecomment-355607646 32 | // 33 | // This will possibly get fixed in the upstream SDK's based on the ability to set an HTTP error in the future 34 | // https://go-review.googlesource.com/c/go/+/191779/ but until then we should retry these. 35 | // 36 | if strings.Contains(errStr, "http: server closed idle connection") { 37 | return "server_close_idle_connection", true 38 | } 39 | return "", false 40 | } 41 | 42 | // IsRetryableNetwork returns if the provided error is a retryable network related error. It also returns the 43 | // type, in string form, for optional logging and metrics use. 44 | func IsRetryableNetwork(err error) (retryType string, isRetryable bool) { 45 | if IsRetryable(err) { 46 | return "retryable", true 47 | } 48 | if IsTemporary(err) { 49 | return "temporary", true 50 | } 51 | if IsTimeout(err) { 52 | return "timeout", true 53 | } 54 | return IsTemporaryConnection(err) 55 | } 56 | 57 | // IsRetryable returns true if the provided error is considered retryable by testing if it 58 | // complies with an interface implementing `Retryable() bool` or `IsRetryable bool` and calling the function. 59 | func IsRetryable(err error) bool { 60 | var t interface{ IsRetryable() bool } 61 | if errors.As(err, &t) && t.IsRetryable() { 62 | return true 63 | } 64 | 65 | var t2 interface{ Retryable() bool } 66 | return errors.As(err, &t2) && t2.Retryable() 67 | } 68 | 69 | // IsTemporary returns true if the provided error is considered retryable temporary error by testing if it 70 | // complies with an interface implementing `Temporary() bool` and calling the function. 71 | func IsTemporary(err error) bool { 72 | var t interface{ Temporary() bool } 73 | return errors.As(err, &t) && t.Temporary() 74 | } 75 | 76 | // IsTimeout returns true if the provided error is considered a retryable timeout error by testing if it 77 | // complies with an interface implementing `Timeout() bool` and calling the function. 78 | func IsTimeout(err error) bool { 79 | var t interface{ Timeout() bool } 80 | return errors.As(err, &t) && t.Timeout() 81 | } 82 | 83 | // IsTemporaryConnection returns if the provided error was a low level retryable connection error. It also returns the 84 | // type, in string form, for optional logging and metrics use. 85 | func IsTemporaryConnection(err error) (retryType string, isRetryable bool) { 86 | if err != nil { 87 | if errors.Is(err, syscall.ECONNRESET) { 88 | return "econnreset", true 89 | } 90 | if errors.Is(err, syscall.ECONNABORTED) { 91 | return "econnaborted", true 92 | } 93 | if errors.Is(err, syscall.ENOTCONN) { 94 | return "enotconn", true 95 | } 96 | if errors.Is(err, syscall.EWOULDBLOCK) { 97 | return "ewouldblock", true 98 | } 99 | if errors.Is(err, syscall.EAGAIN) { 100 | return "eagain", true 101 | } 102 | if errors.Is(err, syscall.ETIMEDOUT) { 103 | return "etimedout", true 104 | } 105 | if errors.Is(err, syscall.EINTR) { 106 | return "eintr", true 107 | } 108 | if errors.Is(err, syscall.EPIPE) { 109 | return "epipe", true 110 | } 111 | } 112 | return "", false 113 | } 114 | -------------------------------------------------------------------------------- /net/http/headers.go: -------------------------------------------------------------------------------- 1 | package httpext 2 | 3 | // HTTP Header keys 4 | const ( 5 | Age string = "Age" 6 | AltSCV string = "Alt-Svc" 7 | Accept string = "Accept" 8 | AcceptCharset string = "Accept-Charset" 9 | AcceptPatch string = "Accept-Patch" 10 | AcceptRanges string = "Accept-Ranges" 11 | AcceptedLanguage string = "Accept-Language" 12 | AcceptEncoding string = "Accept-Encoding" 13 | Authorization string = "Authorization" 14 | CrossOriginResourcePolicy string = "Cross-Origin-Resource-Policy" 15 | CacheControl string = "Cache-Control" 16 | Connection string = "Connection" 17 | ContentDisposition string = "Content-Disposition" 18 | ContentEncoding string = "Content-Encoding" 19 | ContentLength string = "Content-Length" 20 | ContentType string = "Content-Type" 21 | ContentLanguage string = "Content-Language" 22 | ContentLocation string = "Content-Location" 23 | ContentRange string = "Content-Range" 24 | Date string = "Date" 25 | DeltaBase string = "Delta-Base" 26 | ETag string = "ETag" 27 | Expires string = "Expires" 28 | Host string = "Host" 29 | IM string = "IM" 30 | IfMatch string = "If-Match" 31 | IfModifiedSince string = "If-Modified-Since" 32 | IfNoneMatch string = "If-None-Match" 33 | IfRange string = "If-Range" 34 | IfUnmodifiedSince string = "If-Unmodified-Since" 35 | KeepAlive string = "Keep-Alive" 36 | LastModified string = "Last-Modified" 37 | Link string = "Link" 38 | Pragma string = "Pragma" 39 | ProxyAuthenticate string = "Proxy-Authenticate" 40 | ProxyAuthorization string = "Proxy-Authorization" 41 | PublicKeyPins string = "Public-Key-Pins" 42 | RetryAfter string = "Retry-After" 43 | Referer string = "Referer" 44 | Server string = "Server" 45 | SetCookie string = "Set-Cookie" 46 | StrictTransportSecurity string = "Strict-Transport-Security" 47 | Trailer string = "Trailer" 48 | TK string = "Tk" 49 | TransferEncoding string = "Transfer-Encoding" 50 | Location string = "Location" 51 | Upgrade string = "Upgrade" 52 | Vary string = "Vary" 53 | Via string = "Via" 54 | Warning string = "Warning" 55 | WWWAuthenticate string = "WWW-Authenticate" 56 | XForwardedFor string = "X-Forwarded-For" 57 | XForwardedHost string = "X-Forwarded-Host" 58 | XForwardedProto string = "X-Forwarded-Proto" 59 | XRealIP string = "X-Real-Ip" 60 | XContentTypeOptions string = "X-Content-Type-Options" 61 | XFrameOptions string = "X-Frame-Options" 62 | XXSSProtection string = "X-XSS-Protection" 63 | XDNSPrefetchControl string = "X-DNS-Prefetch-Control" 64 | Allow string = "Allow" 65 | Origin string = "Origin" 66 | AccessControlAllowOrigin string = "Access-Control-Allow-Origin" 67 | AccessControlAllowCredentials string = "Access-Control-Allow-Credentials" 68 | AccessControlAllowHeaders string = "Access-Control-Allow-Headers" 69 | AccessControlAllowMethods string = "Access-Control-Allow-Methods" 70 | AccessControlExposeHeaders string = "Access-Control-Expose-Headers" 71 | AccessControlMaxAge string = "Access-Control-Max-Age" 72 | AccessControlRequestHeaders string = "Access-Control-Request-Headers" 73 | AccessControlRequestMethod string = "Access-Control-Request-Method" 74 | TimingAllowOrigin string = "Timing-Allow-Origin" 75 | UserAgent string = "User-Agent" 76 | ) 77 | -------------------------------------------------------------------------------- /errors/retrier_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package errorsext 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "io" 10 | "testing" 11 | "time" 12 | 13 | . "github.com/go-playground/assert/v2" 14 | . "github.com/go-playground/pkg/v5/values/result" 15 | ) 16 | 17 | // TODO: Add IsRetryable and Retryable to helper functions. 18 | 19 | func TestRetrierMaxAttempts(t *testing.T) { 20 | var i, j int 21 | result := NewRetryer[int, error]().Backoff(func(ctx context.Context, attempt int, _ error) { 22 | j++ 23 | }).MaxAttempts(MaxAttempts, 3).Do(context.Background(), func(ctx context.Context) Result[int, error] { 24 | i++ 25 | if i > 50 { 26 | panic("infinite loop") 27 | } 28 | return Err[int, error](io.EOF) 29 | }) 30 | Equal(t, result.IsErr(), true) 31 | Equal(t, result.Err(), io.EOF) 32 | Equal(t, i, 3) 33 | Equal(t, j, 2) 34 | } 35 | 36 | func TestRetrierMaxAttemptsNonRetryable(t *testing.T) { 37 | var i, j int 38 | returnErr := io.ErrUnexpectedEOF 39 | result := NewRetryer[int, error]().IsRetryableFn(func(_ context.Context, e error) (isRetryable bool) { 40 | if returnErr == io.EOF { 41 | return false 42 | } else { 43 | return true 44 | } 45 | }).Backoff(func(ctx context.Context, attempt int, _ error) { 46 | j++ 47 | if j == 10 { 48 | returnErr = io.EOF 49 | } 50 | }).MaxAttempts(MaxAttemptsNonRetryable, 3).Do(context.Background(), func(ctx context.Context) Result[int, error] { 51 | i++ 52 | if i > 50 { 53 | panic("infinite loop") 54 | } 55 | return Err[int, error](returnErr) 56 | }) 57 | Equal(t, result.IsErr(), true) 58 | Equal(t, result.Err(), io.EOF) 59 | Equal(t, i, 13) 60 | Equal(t, j, 12) 61 | } 62 | 63 | func TestRetrierMaxAttemptsNonRetryableReset(t *testing.T) { 64 | var i, j int 65 | returnErr := io.EOF 66 | result := NewRetryer[int, error]().IsRetryableFn(func(_ context.Context, e error) (isRetryable bool) { 67 | if returnErr == io.EOF { 68 | return false 69 | } else { 70 | return true 71 | } 72 | }).Backoff(func(ctx context.Context, attempt int, _ error) { 73 | j++ 74 | if j == 2 { 75 | returnErr = io.ErrUnexpectedEOF 76 | } else if j == 10 { 77 | returnErr = io.EOF 78 | } 79 | }).MaxAttempts(MaxAttemptsNonRetryableReset, 3).Do(context.Background(), func(ctx context.Context) Result[int, error] { 80 | i++ 81 | if i > 50 { 82 | panic("infinite loop") 83 | } 84 | return Err[int, error](returnErr) 85 | }) 86 | Equal(t, result.IsErr(), true) 87 | Equal(t, result.Err(), io.EOF) 88 | Equal(t, i, 13) 89 | Equal(t, j, 12) 90 | } 91 | 92 | func TestRetrierMaxAttemptsUnlimited(t *testing.T) { 93 | var i, j int 94 | r := NewRetryer[int, error]().Backoff(func(ctx context.Context, attempt int, _ error) { 95 | j++ 96 | }).MaxAttempts(MaxAttemptsUnlimited, 0) 97 | 98 | PanicMatches(t, func() { 99 | r.Do(context.Background(), func(ctx context.Context) Result[int, error] { 100 | i++ 101 | if i > 50 { 102 | panic("infinite loop") 103 | } 104 | return Err[int, error](io.EOF) 105 | }) 106 | }, "infinite loop") 107 | } 108 | 109 | func TestRetrierMaxAttemptsTimeout(t *testing.T) { 110 | result := NewRetryer[int, error]().Backoff(func(ctx context.Context, attempt int, _ error) { 111 | }).MaxAttempts(MaxAttempts, 1).Timeout(time.Second). 112 | Do(context.Background(), func(ctx context.Context) Result[int, error] { 113 | select { 114 | case <-ctx.Done(): 115 | return Err[int, error](ctx.Err()) 116 | case <-time.After(time.Second * 3): 117 | return Err[int, error](io.EOF) 118 | } 119 | }) 120 | Equal(t, result.IsErr(), true) 121 | Equal(t, result.Err(), context.DeadlineExceeded) 122 | } 123 | 124 | func TestRetrierEarlyReturn(t *testing.T) { 125 | var earlyReturnCount int 126 | 127 | r := NewRetryer[int, error]().Backoff(func(ctx context.Context, attempt int, _ error) { 128 | }).MaxAttempts(MaxAttempts, 5).Timeout(time.Second). 129 | IsEarlyReturnFn(func(ctx context.Context, err error) bool { 130 | earlyReturnCount++ 131 | return errors.Is(err, io.EOF) 132 | }).Backoff(nil) 133 | 134 | result := r.Do(context.Background(), func(ctx context.Context) Result[int, error] { 135 | return Err[int, error](io.EOF) 136 | }) 137 | Equal(t, result.IsErr(), true) 138 | Equal(t, result.Err(), io.EOF) 139 | Equal(t, earlyReturnCount, 1) 140 | 141 | // now let try with retryable overriding early return TL;DR retryable should take precedence over early return 142 | earlyReturnCount = 0 143 | isRetryableCount := 0 144 | result = r.IsRetryableFn(func(ctx context.Context, err error) (isRetryable bool) { 145 | isRetryableCount++ 146 | return errors.Is(err, io.EOF) 147 | }).Do(context.Background(), func(ctx context.Context) Result[int, error] { 148 | return Err[int, error](io.EOF) 149 | }) 150 | Equal(t, result.IsErr(), true) 151 | Equal(t, result.Err(), io.EOF) 152 | Equal(t, earlyReturnCount, 0) 153 | Equal(t, isRetryableCount, 5) 154 | 155 | // while here let's check the first test case again, `Retrier` should be a copy and original still intact. 156 | isRetryableCount = 0 157 | result = r.Do(context.Background(), func(ctx context.Context) Result[int, error] { 158 | return Err[int, error](io.EOF) 159 | }) 160 | Equal(t, result.IsErr(), true) 161 | Equal(t, result.Err(), io.EOF) 162 | Equal(t, earlyReturnCount, 1) 163 | Equal(t, isRetryableCount, 0) 164 | } 165 | -------------------------------------------------------------------------------- /values/option/option_sql_go1.22.go: -------------------------------------------------------------------------------- 1 | //go:build go1.22 2 | 3 | package optionext 4 | 5 | import ( 6 | "database/sql" 7 | "database/sql/driver" 8 | "encoding/json" 9 | "fmt" 10 | "math" 11 | "reflect" 12 | "time" 13 | ) 14 | 15 | var ( 16 | scanType = reflect.TypeFor[sql.Scanner]() 17 | valuerType = reflect.TypeFor[driver.Valuer]() 18 | byteSliceType = reflect.TypeFor[[]byte]() 19 | timeType = reflect.TypeFor[time.Time]() 20 | stringType = reflect.TypeFor[string]() 21 | int64Type = reflect.TypeFor[int64]() 22 | float64Type = reflect.TypeFor[float64]() 23 | boolType = reflect.TypeFor[bool]() 24 | ) 25 | 26 | // Value implements the driver.Valuer interface. 27 | // 28 | // This honours the `driver.Valuer` interface if the value implements it. 29 | // It also supports custom types of the std types and treats all else as []byte 30 | func (o Option[T]) Value() (driver.Value, error) { 31 | if o.IsNone() { 32 | return nil, nil 33 | } 34 | val := reflect.ValueOf(o.value) 35 | 36 | if val.Type().Implements(valuerType) { 37 | return val.Interface().(driver.Valuer).Value() 38 | } 39 | switch val.Kind() { 40 | case reflect.String: 41 | return val.Convert(stringType).Interface(), nil 42 | case reflect.Bool: 43 | return val.Convert(boolType).Interface(), nil 44 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 45 | return val.Convert(int64Type).Interface(), nil 46 | case reflect.Float64: 47 | return val.Convert(float64Type).Interface(), nil 48 | case reflect.Slice, reflect.Array: 49 | if val.Type().ConvertibleTo(byteSliceType) { 50 | return val.Convert(byteSliceType).Interface(), nil 51 | } 52 | return json.Marshal(val.Interface()) 53 | case reflect.Struct: 54 | if val.CanConvert(timeType) { 55 | return val.Convert(timeType).Interface(), nil 56 | } 57 | return json.Marshal(val.Interface()) 58 | case reflect.Map: 59 | return json.Marshal(val.Interface()) 60 | default: 61 | return o.value, nil 62 | } 63 | } 64 | 65 | // Scan implements the sql.Scanner interface. 66 | func (o *Option[T]) Scan(value any) error { 67 | 68 | if value == nil { 69 | *o = None[T]() 70 | return nil 71 | } 72 | 73 | val := reflect.ValueOf(&o.value) 74 | 75 | if val.Type().Implements(scanType) { 76 | err := val.Interface().(sql.Scanner).Scan(value) 77 | if err != nil { 78 | return err 79 | } 80 | o.isSome = true 81 | return nil 82 | } 83 | 84 | val = val.Elem() 85 | 86 | switch val.Kind() { 87 | case reflect.String, reflect.Bool, reflect.Uint8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float64: 88 | var v sql.Null[T] 89 | if err := v.Scan(value); err != nil { 90 | return err 91 | } 92 | *o = Some(reflect.ValueOf(v.V).Convert(val.Type()).Interface().(T)) 93 | case reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: 94 | v := reflect.ValueOf(value) 95 | if v.Type().ConvertibleTo(val.Type()) { 96 | *o = Some(reflect.ValueOf(v.Convert(val.Type()).Interface()).Interface().(T)) 97 | } else { 98 | return fmt.Errorf("value %T not convertable to %T", value, o.value) 99 | } 100 | case reflect.Float32: 101 | var v sql.Null[float64] 102 | if err := v.Scan(value); err != nil { 103 | return err 104 | } 105 | *o = Some(reflect.ValueOf(v.V).Convert(val.Type()).Interface().(T)) 106 | case reflect.Int: 107 | var v sql.Null[int64] 108 | if err := v.Scan(value); err != nil { 109 | return err 110 | } 111 | if v.V > math.MaxInt || v.V < math.MinInt { 112 | return fmt.Errorf("value %d out of range for int", v.V) 113 | } 114 | *o = Some(reflect.ValueOf(v.V).Convert(val.Type()).Interface().(T)) 115 | case reflect.Int8: 116 | var v sql.Null[int64] 117 | if err := v.Scan(value); err != nil { 118 | return err 119 | } 120 | if v.V > math.MaxInt8 || v.V < math.MinInt8 { 121 | return fmt.Errorf("value %d out of range for int8", v.V) 122 | } 123 | *o = Some(reflect.ValueOf(v.V).Convert(val.Type()).Interface().(T)) 124 | case reflect.Interface: 125 | *o = Some(reflect.ValueOf(value).Convert(val.Type()).Interface().(T)) 126 | case reflect.Struct: 127 | if val.CanConvert(timeType) { 128 | switch t := value.(type) { 129 | case string: 130 | tm, err := time.Parse(time.RFC3339Nano, t) 131 | if err != nil { 132 | return err 133 | } 134 | *o = Some(reflect.ValueOf(tm).Convert(val.Type()).Interface().(T)) 135 | 136 | case []byte: 137 | tm, err := time.Parse(time.RFC3339Nano, string(t)) 138 | if err != nil { 139 | return err 140 | } 141 | *o = Some(reflect.ValueOf(tm).Convert(val.Type()).Interface().(T)) 142 | 143 | default: 144 | var v sql.Null[time.Time] 145 | if err := v.Scan(value); err != nil { 146 | return err 147 | } 148 | *o = Some(reflect.ValueOf(v.V).Convert(val.Type()).Interface().(T)) 149 | } 150 | return nil 151 | } 152 | fallthrough 153 | 154 | default: 155 | switch val.Kind() { 156 | case reflect.Struct, reflect.Slice, reflect.Map: 157 | v := reflect.ValueOf(value) 158 | 159 | if v.Type().ConvertibleTo(byteSliceType) { 160 | if val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8 { 161 | *o = Some(reflect.ValueOf(v.Convert(val.Type()).Interface()).Interface().(T)) 162 | } else { 163 | if err := json.Unmarshal(v.Convert(byteSliceType).Interface().([]byte), &o.value); err != nil { 164 | return err 165 | } 166 | } 167 | o.isSome = true 168 | return nil 169 | } 170 | } 171 | return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", value, o.value) 172 | } 173 | return nil 174 | } 175 | -------------------------------------------------------------------------------- /sync/mutex2.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package syncext 5 | 6 | import ( 7 | "sync" 8 | 9 | resultext "github.com/go-playground/pkg/v5/values/result" 10 | ) 11 | 12 | // MutexGuard protects the inner contents of a Mutex2 for safety and unlocking. 13 | type MutexGuard[T any, M interface{ Unlock() }] struct { 14 | m M 15 | // T is the inner generic type of the Mutex 16 | T T 17 | } 18 | 19 | // Unlock unlocks the Mutex2 value. 20 | func (g MutexGuard[T, M]) Unlock() { 21 | g.m.Unlock() 22 | } 23 | 24 | // NewMutex2 creates a new Mutex for use. 25 | func NewMutex2[T any](value T) Mutex2[T] { 26 | return Mutex2[T]{ 27 | m: new(sync.Mutex), 28 | value: value, 29 | } 30 | } 31 | 32 | // Mutex2 creates a type safe mutex wrapper ensuring one cannot access the values of a locked values 33 | // without first gaining a lock. 34 | type Mutex2[T any] struct { 35 | m *sync.Mutex 36 | value T 37 | } 38 | 39 | // Lock locks the Mutex and returns value for use, safe for mutation if 40 | // 41 | // If the lock is already in use, the calling goroutine blocks until the mutex is available. 42 | func (m Mutex2[T]) Lock() MutexGuard[T, *sync.Mutex] { 43 | m.m.Lock() 44 | return MutexGuard[T, *sync.Mutex]{ 45 | m: m.m, 46 | T: m.value, 47 | } 48 | } 49 | 50 | //// Unlock unlocks the Mutex accepting a value to set as the new or mutated value. 51 | //// It is optional to pass a new value to be set but NOT required for there reasons: 52 | //// 1. If the internal value is already mutable then no need to set as changes apply as they happen. 53 | //// 2. If there's a failure working with the locked value you may NOT want to set it, but still unlock. 54 | //// 3. Supports locked values that are not mutable. 55 | //// 56 | //// It is a run-time error if the Mutex is not locked on entry to Unlock. 57 | //func (m Mutex2[T]) Unlock() { 58 | // m.m.Unlock() 59 | //} 60 | 61 | // PerformMut safely locks and unlocks the Mutex values and performs the provided function returning its error if one 62 | // otherwise setting the returned value as the new mutex value. 63 | func (m Mutex2[T]) PerformMut(f func(T)) { 64 | guard := m.Lock() 65 | f(guard.T) 66 | guard.Unlock() 67 | } 68 | 69 | // TryLock tries to lock Mutex and reports whether it succeeded. 70 | // If it does the value is returned for use in the Ok result otherwise Err with empty value. 71 | func (m Mutex2[T]) TryLock() resultext.Result[MutexGuard[T, *sync.Mutex], struct{}] { 72 | if m.m.TryLock() { 73 | return resultext.Ok[MutexGuard[T, *sync.Mutex], struct{}](MutexGuard[T, *sync.Mutex]{ 74 | m: m.m, 75 | T: m.value, 76 | }) 77 | } else { 78 | return resultext.Err[MutexGuard[T, *sync.Mutex], struct{}](struct{}{}) 79 | } 80 | } 81 | 82 | // RMutexGuard protects the inner contents of a RWMutex2 for safety and unlocking. 83 | type RMutexGuard[T any] struct { 84 | rw *sync.RWMutex 85 | // T is the inner generic type of the Mutex 86 | T T 87 | } 88 | 89 | // RUnlock unlocks the RWMutex2 value. 90 | func (g RMutexGuard[T]) RUnlock() { 91 | g.rw.RUnlock() 92 | } 93 | 94 | // NewRWMutex2 creates a new RWMutex for use. 95 | func NewRWMutex2[T any](value T) RWMutex2[T] { 96 | return RWMutex2[T]{ 97 | rw: new(sync.RWMutex), 98 | value: value, 99 | } 100 | } 101 | 102 | // RWMutex2 creates a type safe RWMutex wrapper ensuring one cannot access the values of a locked values 103 | // without first gaining a lock. 104 | type RWMutex2[T any] struct { 105 | rw *sync.RWMutex 106 | value T 107 | } 108 | 109 | // Lock locks the Mutex and returns value for use, safe for mutation if 110 | // 111 | // If the lock is already in use, the calling goroutine blocks until the mutex is available. 112 | func (m RWMutex2[T]) Lock() MutexGuard[T, *sync.RWMutex] { 113 | m.rw.Lock() 114 | return MutexGuard[T, *sync.RWMutex]{ 115 | m: m.rw, 116 | T: m.value, 117 | } 118 | } 119 | 120 | // PerformMut safely locks and unlocks the RWMutex mutable values and performs the provided function. 121 | func (m RWMutex2[T]) PerformMut(f func(T)) { 122 | guard := m.Lock() 123 | f(guard.T) 124 | guard.Unlock() 125 | } 126 | 127 | // TryLock tries to lock RWMutex and returns the value in the Ok result if successful. 128 | // If it does the value is returned for use in the Ok result otherwise Err with empty value. 129 | func (m RWMutex2[T]) TryLock() resultext.Result[MutexGuard[T, *sync.RWMutex], struct{}] { 130 | if m.rw.TryLock() { 131 | return resultext.Ok[MutexGuard[T, *sync.RWMutex], struct{}]( 132 | MutexGuard[T, *sync.RWMutex]{ 133 | m: m.rw, 134 | T: m.value, 135 | }) 136 | } else { 137 | return resultext.Err[MutexGuard[T, *sync.RWMutex], struct{}](struct{}{}) 138 | } 139 | } 140 | 141 | // Perform safely locks and unlocks the RWMutex read-only values and performs the provided function. 142 | func (m RWMutex2[T]) Perform(f func(T)) { 143 | guard := m.RLock() 144 | f(guard.T) 145 | guard.RUnlock() 146 | } 147 | 148 | // RLock locks the RWMutex for reading and returns the value for read-only use. 149 | // It should not be used for recursive read locking; a blocked Lock call excludes new readers from acquiring the lock 150 | func (m RWMutex2[T]) RLock() RMutexGuard[T] { 151 | m.rw.RLock() 152 | return RMutexGuard[T]{ 153 | rw: m.rw, 154 | T: m.value, 155 | } 156 | } 157 | 158 | // TryRLock tries to lock RWMutex for reading and returns the value in the Ok result if successful. 159 | // If it does the value is returned for use in the Ok result otherwise Err with empty value. 160 | func (m RWMutex2[T]) TryRLock() resultext.Result[RMutexGuard[T], struct{}] { 161 | if m.rw.TryRLock() { 162 | return resultext.Ok[RMutexGuard[T], struct{}]( 163 | RMutexGuard[T]{ 164 | rw: m.rw, 165 | T: m.value, 166 | }, 167 | ) 168 | } else { 169 | return resultext.Err[RMutexGuard[T], struct{}](struct{}{}) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /sync/mutex.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package syncext 5 | 6 | import ( 7 | optionext "github.com/go-playground/pkg/v5/values/option" 8 | "sync" 9 | 10 | resultext "github.com/go-playground/pkg/v5/values/result" 11 | ) 12 | 13 | // NewMutex creates a new Mutex for use. 14 | // 15 | // Deprecated: use `syncext.NewMutex2(...)` instead which corrects design issues with the current implementation. 16 | func NewMutex[T any](value T) *Mutex[T] { 17 | return &Mutex[T]{ 18 | value: value, 19 | } 20 | } 21 | 22 | // Mutex creates a type safe mutex wrapper ensuring one cannot access the values of a locked values 23 | // without first gaining a lock. 24 | type Mutex[T any] struct { 25 | m sync.Mutex 26 | value T 27 | } 28 | 29 | // Lock locks the Mutex and returns value for use, safe for mutation if 30 | // 31 | // If the lock is already in use, the calling goroutine blocks until the mutex is available. 32 | func (m *Mutex[T]) Lock() T { 33 | m.m.Lock() 34 | return m.value 35 | } 36 | 37 | // Unlock unlocks the Mutex accepting a value to set as the new or mutated value. 38 | // It is optional to pass a new value to be set but NOT required for there reasons: 39 | // 1. If the internal value is already mutable then no need to set as changes apply as they happen. 40 | // 2. If there's a failure working with the locked value you may NOT want to set it, but still unlock. 41 | // 3. Supports locked values that are not mutable. 42 | // 43 | // It is a run-time error if the Mutex is not locked on entry to Unlock. 44 | func (m *Mutex[T]) Unlock(value optionext.Option[T]) { 45 | if value.IsSome() { 46 | m.value = value.Unwrap() 47 | } 48 | m.m.Unlock() 49 | } 50 | 51 | // PerformMut safely locks and unlocks the Mutex values and performs the provided function returning its error if one 52 | // otherwise setting the returned value as the new mutex value. 53 | func (m *Mutex[T]) PerformMut(f func(T) (T, error)) error { 54 | value := m.Lock() 55 | result, err := f(value) 56 | if err != nil { 57 | m.Unlock(optionext.None[T]()) 58 | return err 59 | } 60 | m.Unlock(optionext.Some(result)) 61 | return nil 62 | } 63 | 64 | // TryLock tries to lock Mutex and reports whether it succeeded. 65 | // If it does the value is returned for use in the Ok result otherwise Err with empty value. 66 | func (m *Mutex[T]) TryLock() resultext.Result[T, struct{}] { 67 | if m.m.TryLock() { 68 | return resultext.Ok[T, struct{}](m.value) 69 | } else { 70 | return resultext.Err[T, struct{}](struct{}{}) 71 | } 72 | } 73 | 74 | // NewRWMutex creates a new RWMutex for use. 75 | // 76 | // Deprecated: use `syncext.NewRWMutex2(...)` instead which corrects design issues with the current implementation. 77 | func NewRWMutex[T any](value T) *RWMutex[T] { 78 | return &RWMutex[T]{ 79 | value: value, 80 | } 81 | } 82 | 83 | // RWMutex creates a type safe RWMutex wrapper ensuring one cannot access the values of a locked values 84 | // without first gaining a lock. 85 | type RWMutex[T any] struct { 86 | rw sync.RWMutex 87 | value T 88 | } 89 | 90 | // Lock locks the Mutex and returns value for use, safe for mutation if 91 | // 92 | // If the lock is already in use, the calling goroutine blocks until the mutex is available. 93 | func (m *RWMutex[T]) Lock() T { 94 | m.rw.Lock() 95 | return m.value 96 | } 97 | 98 | // Unlock unlocks the Mutex accepting a value to set as the new or mutated value. 99 | // It is optional to pass a new value to be set but NOT required for there reasons: 100 | // 1. If the internal value is already mutable then no need to set as changes apply as they happen. 101 | // 2. If there's a failure working with the locked value you may NOT want to set it, but still unlock. 102 | // 3. Supports locked values that are not mutable. 103 | // 104 | // It is a run-time error if the Mutex is not locked on entry to Unlock. 105 | func (m *RWMutex[T]) Unlock(value optionext.Option[T]) { 106 | if value.IsSome() { 107 | m.value = value.Unwrap() 108 | } 109 | m.rw.Unlock() 110 | } 111 | 112 | // PerformMut safely locks and unlocks the RWMutex mutable values and performs the provided function. 113 | func (m *RWMutex[T]) PerformMut(f func(T) (T, error)) error { 114 | value := m.Lock() 115 | result, err := f(value) 116 | if err != nil { 117 | m.Unlock(optionext.None[T]()) 118 | return err 119 | } 120 | m.Unlock(optionext.Some(result)) 121 | return nil 122 | } 123 | 124 | // TryLock tries to lock RWMutex and returns the value in the Ok result if successful. 125 | // If it does the value is returned for use in the Ok result otherwise Err with empty value. 126 | func (m *RWMutex[T]) TryLock() resultext.Result[T, struct{}] { 127 | if m.rw.TryLock() { 128 | return resultext.Ok[T, struct{}](m.value) 129 | } else { 130 | return resultext.Err[T, struct{}](struct{}{}) 131 | } 132 | } 133 | 134 | // Perform safely locks and unlocks the RWMutex read-only values and performs the provided function. 135 | func (m *RWMutex[T]) Perform(f func(T) error) error { 136 | result := m.RLock() 137 | err := f(result) 138 | if err != nil { 139 | m.RUnlock() 140 | return err 141 | } 142 | m.RUnlock() 143 | return nil 144 | } 145 | 146 | // RLock locks the RWMutex for reading and returns the value for read-only use. 147 | // It should not be used for recursive read locking; a blocked Lock call excludes new readers from acquiring the lock 148 | func (m *RWMutex[T]) RLock() T { 149 | m.rw.RLock() 150 | return m.value 151 | } 152 | 153 | // RUnlock undoes a single RLock call; it does not affect other simultaneous readers. 154 | // It is a run-time error if rw is not locked for reading on entry to RUnlock. 155 | func (m *RWMutex[T]) RUnlock() { 156 | m.rw.RUnlock() 157 | } 158 | 159 | // TryRLock tries to lock RWMutex for reading and returns the value in the Ok result if successful. 160 | // If it does the value is returned for use in the Ok result otherwise Err with empty value. 161 | func (m *RWMutex[T]) TryRLock() resultext.Result[T, struct{}] { 162 | if m.rw.TryRLock() { 163 | return resultext.Ok[T, struct{}](m.value) 164 | } else { 165 | return resultext.Err[T, struct{}](struct{}{}) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /errors/retrier.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package errorsext 5 | 6 | import ( 7 | "context" 8 | "time" 9 | 10 | . "github.com/go-playground/pkg/v5/values/result" 11 | ) 12 | 13 | // MaxAttemptsMode is used to set the mode for the maximum number of attempts. 14 | // 15 | // eg. Should the max attempts apply to all errors, just ones not determined to be retryable, reset on retryable errors, etc. 16 | type MaxAttemptsMode uint8 17 | 18 | const ( 19 | // MaxAttemptsNonRetryableReset will apply the max attempts to all errors not determined to be retryable, but will 20 | // reset the attempts if a retryable error is encountered after a non-retryable error. 21 | MaxAttemptsNonRetryableReset MaxAttemptsMode = iota 22 | 23 | // MaxAttemptsNonRetryable will apply the max attempts to all errors not determined to be retryable. 24 | MaxAttemptsNonRetryable 25 | 26 | // MaxAttempts will apply the max attempts to all errors, even those determined to be retryable. 27 | MaxAttempts 28 | 29 | // MaxAttemptsUnlimited will not apply a maximum number of attempts. 30 | MaxAttemptsUnlimited 31 | ) 32 | 33 | // BackoffFn is a function used to apply a backoff strategy to the retryable function. 34 | // 35 | // It accepts `E` in cases where the amount of time to backoff is dynamic, for example when and http request fails 36 | // with a 429 status code, the `Retry-After` header can be used to determine how long to backoff. It is not required 37 | // to use or handle `E` and can be ignored if desired. 38 | type BackoffFn[E any] func(ctx context.Context, attempt int, e E) 39 | 40 | // IsRetryableFn2 is called to determine if the type E is retryable. 41 | type IsRetryableFn2[E any] func(ctx context.Context, e E) (isRetryable bool) 42 | 43 | // EarlyReturnFn is the function that can be used to bypass all retry logic, no matter the MaxAttemptsMode, for when the 44 | // type of `E` will never succeed and should not be retried. 45 | // 46 | // eg. If retrying an HTTP request and getting 400 Bad Request, it's unlikely to ever succeed and should not be retried. 47 | type EarlyReturnFn[E any] func(ctx context.Context, e E) (earlyReturn bool) 48 | 49 | // Retryer is used to retry any fallible operation. 50 | type Retryer[T, E any] struct { 51 | isRetryableFn IsRetryableFn2[E] 52 | isEarlyReturnFn EarlyReturnFn[E] 53 | maxAttemptsMode MaxAttemptsMode 54 | maxAttempts uint8 55 | bo BackoffFn[E] 56 | timeout time.Duration 57 | } 58 | 59 | // NewRetryer returns a new `Retryer` with sane default values. 60 | // 61 | // The default values are: 62 | // - `MaxAttemptsMode` is `MaxAttemptsNonRetryableReset`. 63 | // - `MaxAttempts` is 5. 64 | // - `Timeout` is 0 no context timeout. 65 | // - `IsRetryableFn` will always return false as `E` is unknown until defined. 66 | // - `BackoffFn` will sleep for 200ms. It's recommended to use exponential backoff for production. 67 | // - `EarlyReturnFn` will be None. 68 | func NewRetryer[T, E any]() Retryer[T, E] { 69 | return Retryer[T, E]{ 70 | isRetryableFn: func(_ context.Context, _ E) bool { return false }, 71 | maxAttemptsMode: MaxAttemptsNonRetryableReset, 72 | maxAttempts: 5, 73 | bo: func(ctx context.Context, attempt int, _ E) { 74 | t := time.NewTimer(time.Millisecond * 200) 75 | defer t.Stop() 76 | select { 77 | case <-ctx.Done(): 78 | case <-t.C: 79 | } 80 | }, 81 | } 82 | } 83 | 84 | // IsRetryableFn sets the `IsRetryableFn` for the `Retryer`. 85 | func (r Retryer[T, E]) IsRetryableFn(fn IsRetryableFn2[E]) Retryer[T, E] { 86 | if fn == nil { 87 | fn = func(_ context.Context, _ E) bool { return false } 88 | } 89 | r.isRetryableFn = fn 90 | return r 91 | } 92 | 93 | // IsEarlyReturnFn sets the `EarlyReturnFn` for the `Retryer`. 94 | // 95 | // NOTE: If the `EarlyReturnFn` and `IsRetryableFn` are both set and a conflicting `IsRetryableFn` will take precedence. 96 | func (r Retryer[T, E]) IsEarlyReturnFn(fn EarlyReturnFn[E]) Retryer[T, E] { 97 | r.isEarlyReturnFn = fn 98 | return r 99 | } 100 | 101 | // MaxAttempts sets the maximum number of attempts for the `Retryer`. 102 | // 103 | // NOTE: Max attempts is optional and if not set will retry indefinitely on retryable errors. 104 | func (r Retryer[T, E]) MaxAttempts(mode MaxAttemptsMode, maxAttempts uint8) Retryer[T, E] { 105 | r.maxAttemptsMode, r.maxAttempts = mode, maxAttempts 106 | return r 107 | } 108 | 109 | // Backoff sets the backoff function for the `Retryer`. 110 | func (r Retryer[T, E]) Backoff(fn BackoffFn[E]) Retryer[T, E] { 111 | if fn == nil { 112 | fn = func(_ context.Context, _ int, _ E) {} 113 | } 114 | r.bo = fn 115 | return r 116 | } 117 | 118 | // Timeout sets the timeout for the `Retryer`. This is the timeout per `RetyableFn` attempt and not the entirety 119 | // of the `Retryer` execution. 120 | // 121 | // A timeout of 0 will disable the timeout and is the default. 122 | func (r Retryer[T, E]) Timeout(timeout time.Duration) Retryer[T, E] { 123 | r.timeout = timeout 124 | return r 125 | } 126 | 127 | // Do will execute the provided functions code and automatically retry using the provided retry function. 128 | func (r Retryer[T, E]) Do(ctx context.Context, fn RetryableFn[T, E]) Result[T, E] { 129 | var attempt int 130 | remaining := r.maxAttempts 131 | for { 132 | var result Result[T, E] 133 | if r.timeout == 0 { 134 | result = fn(ctx) 135 | } else { 136 | ctx, cancel := context.WithTimeout(ctx, r.timeout) 137 | result = fn(ctx) 138 | cancel() 139 | } 140 | if result.IsErr() { 141 | err := result.Err() 142 | isRetryable := r.isRetryableFn(ctx, err) 143 | if !isRetryable && r.isEarlyReturnFn != nil && r.isEarlyReturnFn(ctx, err) { 144 | return result 145 | } 146 | 147 | switch r.maxAttemptsMode { 148 | case MaxAttemptsUnlimited: 149 | goto RETRY 150 | case MaxAttemptsNonRetryableReset: 151 | if isRetryable { 152 | remaining = r.maxAttempts 153 | goto RETRY 154 | } else if remaining > 0 { 155 | remaining-- 156 | } 157 | case MaxAttemptsNonRetryable: 158 | if isRetryable { 159 | goto RETRY 160 | } else if remaining > 0 { 161 | remaining-- 162 | } 163 | case MaxAttempts: 164 | if remaining > 0 { 165 | remaining-- 166 | } 167 | } 168 | if remaining == 0 { 169 | return result 170 | } 171 | 172 | RETRY: 173 | r.bo(ctx, attempt, err) 174 | attempt++ 175 | continue 176 | } 177 | return result 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /net/http/retrier_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package httpext 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | 13 | . "github.com/go-playground/assert/v2" 14 | errorsext "github.com/go-playground/pkg/v5/errors" 15 | . "github.com/go-playground/pkg/v5/values/result" 16 | ) 17 | 18 | func TestRetryer_SuccessNoRetries(t *testing.T) { 19 | ctx := context.Background() 20 | 21 | type Test struct { 22 | Name string 23 | } 24 | tst := Test{Name: "test"} 25 | 26 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | _ = JSON(w, http.StatusOK, tst) 28 | })) 29 | defer server.Close() 30 | 31 | retryer := NewRetryer() 32 | 33 | result := retryer.DoResponse(ctx, func(ctx context.Context) Result[*http.Request, error] { 34 | req, err := http.NewRequestWithContext(ctx, "GET", server.URL, nil) 35 | if err != nil { 36 | return Err[*http.Request, error](err) 37 | } 38 | return Ok[*http.Request, error](req) 39 | }, http.StatusOK) 40 | Equal(t, result.IsOk(), true) 41 | Equal(t, result.Unwrap().StatusCode, http.StatusOK) 42 | defer result.Unwrap().Body.Close() 43 | 44 | var responseResult Test 45 | err := retryer.Do(ctx, func(ctx context.Context) Result[*http.Request, error] { 46 | req, err := http.NewRequestWithContext(ctx, "GET", server.URL, nil) 47 | if err != nil { 48 | return Err[*http.Request, error](err) 49 | } 50 | return Ok[*http.Request, error](req) 51 | }, &responseResult, http.StatusOK) 52 | Equal(t, err, nil) 53 | Equal(t, responseResult, tst) 54 | } 55 | 56 | func TestRetryer_SuccessWithRetries(t *testing.T) { 57 | ctx := context.Background() 58 | var count int 59 | 60 | type Test struct { 61 | Name string 62 | } 63 | tst := Test{Name: "test"} 64 | 65 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 66 | if count < 2 { 67 | w.WriteHeader(http.StatusServiceUnavailable) 68 | count++ 69 | return 70 | } 71 | _ = JSON(w, http.StatusOK, tst) 72 | })) 73 | defer server.Close() 74 | 75 | retryer := NewRetryer().Backoff(nil) 76 | 77 | result := retryer.DoResponse(ctx, func(ctx context.Context) Result[*http.Request, error] { 78 | req, err := http.NewRequestWithContext(ctx, "GET", server.URL, nil) 79 | if err != nil { 80 | return Err[*http.Request, error](err) 81 | } 82 | return Ok[*http.Request, error](req) 83 | }, http.StatusOK) 84 | Equal(t, result.IsOk(), true) 85 | Equal(t, result.Unwrap().StatusCode, http.StatusOK) 86 | defer result.Unwrap().Body.Close() 87 | 88 | count = 0 // reset count 89 | 90 | var responseResult Test 91 | err := retryer.Do(ctx, func(ctx context.Context) Result[*http.Request, error] { 92 | req, err := http.NewRequestWithContext(ctx, "GET", server.URL, nil) 93 | if err != nil { 94 | return Err[*http.Request, error](err) 95 | } 96 | return Ok[*http.Request, error](req) 97 | }, &responseResult, http.StatusOK) 98 | Equal(t, err, nil) 99 | Equal(t, responseResult, tst) 100 | } 101 | 102 | func TestRetryer_FailureMaxRetries(t *testing.T) { 103 | ctx := context.Background() 104 | 105 | type Test struct { 106 | Name string 107 | } 108 | 109 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 110 | w.WriteHeader(http.StatusInternalServerError) 111 | })) 112 | defer server.Close() 113 | 114 | retryer := NewRetryer().Backoff(nil).MaxAttempts(errorsext.MaxAttempts, 2) 115 | 116 | result := retryer.DoResponse(ctx, func(ctx context.Context) Result[*http.Request, error] { 117 | req, err := http.NewRequestWithContext(ctx, "GET", server.URL, nil) 118 | if err != nil { 119 | return Err[*http.Request, error](err) 120 | } 121 | return Ok[*http.Request, error](req) 122 | }, http.StatusOK) 123 | Equal(t, result.IsErr(), true) 124 | 125 | var responseResult Test 126 | err := retryer.Do(ctx, func(ctx context.Context) Result[*http.Request, error] { 127 | req, err := http.NewRequestWithContext(ctx, "GET", server.URL, nil) 128 | if err != nil { 129 | return Err[*http.Request, error](err) 130 | } 131 | return Ok[*http.Request, error](req) 132 | }, &responseResult, http.StatusOK) 133 | NotEqual(t, err, nil) 134 | } 135 | 136 | func TestRetryer_ExtractStatusBody(t *testing.T) { 137 | ctx := context.Background() 138 | eStr := "nooooooooooooo!" 139 | 140 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 141 | w.WriteHeader(http.StatusInternalServerError) 142 | _, _ = w.Write([]byte(eStr)) 143 | })) 144 | defer server.Close() 145 | 146 | retryer := NewRetryer().MaxAttempts(errorsext.MaxAttempts, 3) 147 | 148 | result := retryer.DoResponse(ctx, func(ctx context.Context) Result[*http.Request, error] { 149 | req, err := http.NewRequestWithContext(ctx, "GET", server.URL, nil) 150 | if err != nil { 151 | return Err[*http.Request, error](err) 152 | } 153 | return Ok[*http.Request, error](req) 154 | }, http.StatusOK) 155 | Equal(t, result.IsErr(), true) 156 | var esc ErrStatusCode 157 | Equal(t, errors.As(result.Err(), &esc), true) 158 | Equal(t, esc.IsRetryableStatusCode, false) 159 | // check the ultimate failed response body is intact 160 | Equal(t, string(esc.Body), eStr) 161 | } 162 | 163 | func TestRetryer_ExtractStatusBodyEarlyReturn(t *testing.T) { 164 | ctx := context.Background() 165 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 166 | w.WriteHeader(http.StatusUnauthorized) 167 | _, _ = w.Write([]byte(http.StatusText(http.StatusUnauthorized))) 168 | })) 169 | defer server.Close() 170 | 171 | var count int 172 | 173 | retryer := NewRetryer().Backoff(func(_ context.Context, _ int, _ error) { 174 | count++ 175 | }).MaxAttempts(errorsext.MaxAttempts, 2) 176 | 177 | result := retryer.DoResponse(ctx, func(ctx context.Context) Result[*http.Request, error] { 178 | req, err := http.NewRequestWithContext(ctx, "GET", server.URL, nil) 179 | if err != nil { 180 | return Err[*http.Request, error](err) 181 | } 182 | return Ok[*http.Request, error](req) 183 | }, http.StatusOK) 184 | Equal(t, result.IsErr(), true) 185 | var esc ErrStatusCode 186 | Equal(t, errors.As(result.Err(), &esc), true) 187 | Equal(t, esc.IsRetryableStatusCode, false) 188 | // check the ultimate failed response body is intact 189 | Equal(t, string(esc.Body), http.StatusText(http.StatusUnauthorized)) 190 | Equal(t, count, 0) 191 | } 192 | -------------------------------------------------------------------------------- /net/http/retryable.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package httpext 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net/http" 10 | "strconv" 11 | 12 | bytesext "github.com/go-playground/pkg/v5/bytes" 13 | errorsext "github.com/go-playground/pkg/v5/errors" 14 | . "github.com/go-playground/pkg/v5/values/result" 15 | ) 16 | 17 | var ( 18 | // retryableStatusCodes defines the common HTTP response codes that are considered retryable. 19 | retryableStatusCodes = map[int]bool{ 20 | http.StatusServiceUnavailable: true, 21 | http.StatusTooManyRequests: true, 22 | http.StatusBadGateway: true, 23 | http.StatusGatewayTimeout: true, 24 | http.StatusRequestTimeout: true, 25 | 26 | // 524 is a Cloudflare specific error which indicates it connected to the origin server but did not receive 27 | // response within 100 seconds and so times out. 28 | // https://support.cloudflare.com/hc/en-us/articles/115003011431-Error-524-A-timeout-occurred#524error 29 | 524: true, 30 | } 31 | // nonRetryableStatusCodes defines common HTTP responses that are not considered never to be retryable. 32 | nonRetryableStatusCodes = map[int]bool{ 33 | http.StatusBadRequest: true, 34 | http.StatusUnauthorized: true, 35 | http.StatusForbidden: true, 36 | http.StatusNotFound: true, 37 | http.StatusMethodNotAllowed: true, 38 | http.StatusNotAcceptable: true, 39 | http.StatusProxyAuthRequired: true, 40 | http.StatusConflict: true, 41 | http.StatusLengthRequired: true, 42 | http.StatusPreconditionFailed: true, 43 | http.StatusRequestEntityTooLarge: true, 44 | http.StatusRequestURITooLong: true, 45 | http.StatusUnsupportedMediaType: true, 46 | http.StatusRequestedRangeNotSatisfiable: true, 47 | http.StatusExpectationFailed: true, 48 | http.StatusTeapot: true, 49 | http.StatusMisdirectedRequest: true, 50 | http.StatusUnprocessableEntity: true, 51 | http.StatusPreconditionRequired: true, 52 | http.StatusRequestHeaderFieldsTooLarge: true, 53 | http.StatusUnavailableForLegalReasons: true, 54 | http.StatusNotImplemented: true, 55 | http.StatusHTTPVersionNotSupported: true, 56 | http.StatusLoopDetected: true, 57 | http.StatusNotExtended: true, 58 | http.StatusNetworkAuthenticationRequired: true, 59 | } 60 | ) 61 | 62 | // ErrRetryableStatusCode can be used to indicate a retryable HTTP status code was encountered as an error. 63 | type ErrRetryableStatusCode struct { 64 | Response *http.Response 65 | } 66 | 67 | func (e ErrRetryableStatusCode) Error() string { 68 | return fmt.Sprintf("retryable HTTP status code encountered: %d", e.Response.StatusCode) 69 | } 70 | 71 | // ErrUnexpectedResponse can be used to indicate an unexpected response was encountered as an error and provide access to the *http.Response. 72 | type ErrUnexpectedResponse struct { 73 | Response *http.Response 74 | } 75 | 76 | func (e ErrUnexpectedResponse) Error() string { 77 | return "unexpected response encountered" 78 | } 79 | 80 | // IsRetryableStatusCode returns true if the provided status code is considered retryable. 81 | func IsRetryableStatusCode(code int) bool { 82 | return retryableStatusCodes[code] 83 | } 84 | 85 | // IsNonRetryableStatusCode returns true if the provided status code should generally not be retryable. 86 | func IsNonRetryableStatusCode(code int) bool { 87 | return nonRetryableStatusCodes[code] 88 | } 89 | 90 | // BuildRequestFn is a function used to rebuild an HTTP request for use in retryable code. 91 | type BuildRequestFn func(ctx context.Context) (*http.Request, error) 92 | 93 | // IsRetryableStatusCodeFn is a function used to determine if the provided status code is considered retryable. 94 | type IsRetryableStatusCodeFn func(code int) bool 95 | 96 | // DoRetryableResponse will execute the provided functions code and automatically retry before returning the *http.Response. 97 | // 98 | // Deprecated: use `httpext.Retrier` instead which corrects design issues with the current implementation. 99 | func DoRetryableResponse(ctx context.Context, onRetryFn errorsext.OnRetryFn[error], isRetryableStatusCode IsRetryableStatusCodeFn, client *http.Client, buildFn BuildRequestFn) Result[*http.Response, error] { 100 | if client == nil { 101 | client = http.DefaultClient 102 | } 103 | var attempt int 104 | for { 105 | req, err := buildFn(ctx) 106 | if err != nil { 107 | return Err[*http.Response, error](err) 108 | } 109 | 110 | resp, err := client.Do(req) 111 | if err != nil { 112 | if retryReason, isRetryable := errorsext.IsRetryableHTTP(err); isRetryable { 113 | opt := onRetryFn(ctx, err, retryReason, attempt) 114 | if opt.IsSome() { 115 | return Err[*http.Response, error](opt.Unwrap()) 116 | } 117 | attempt++ 118 | continue 119 | } 120 | return Err[*http.Response, error](err) 121 | } 122 | 123 | if isRetryableStatusCode(resp.StatusCode) { 124 | opt := onRetryFn(ctx, ErrRetryableStatusCode{Response: resp}, strconv.Itoa(resp.StatusCode), attempt) 125 | if opt.IsSome() { 126 | return Err[*http.Response, error](opt.Unwrap()) 127 | } 128 | attempt++ 129 | continue 130 | } 131 | return Ok[*http.Response, error](resp) 132 | } 133 | } 134 | 135 | // DoRetryable will execute the provided functions code and automatically retry before returning the result. 136 | // 137 | // This function currently supports decoding the following automatically based on the response Content-Type with 138 | // Gzip supported: 139 | // - JSON 140 | // - XML 141 | // 142 | // Deprecated: use `httpext.Retrier` instead which corrects design issues with the current implementation. 143 | func DoRetryable[T any](ctx context.Context, isRetryableFn errorsext.IsRetryableFn[error], onRetryFn errorsext.OnRetryFn[error], isRetryableStatusCode IsRetryableStatusCodeFn, client *http.Client, expectedResponseCode int, maxMemory bytesext.Bytes, buildFn BuildRequestFn) Result[T, error] { 144 | 145 | return errorsext.DoRetryable(ctx, isRetryableFn, onRetryFn, func(ctx context.Context) Result[T, error] { 146 | 147 | result := DoRetryableResponse(ctx, onRetryFn, isRetryableStatusCode, client, buildFn) 148 | if result.IsErr() { 149 | return Err[T, error](result.Err()) 150 | } 151 | resp := result.Unwrap() 152 | 153 | if resp.StatusCode != expectedResponseCode { 154 | return Err[T, error](ErrUnexpectedResponse{Response: resp}) 155 | } 156 | defer resp.Body.Close() 157 | 158 | data, err := DecodeResponse[T](resp, maxMemory) 159 | if err != nil { 160 | return Err[T, error](err) 161 | } 162 | return Ok[T, error](data) 163 | }) 164 | } 165 | -------------------------------------------------------------------------------- /values/option/option_sql.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 && !go1.22 2 | // +build go1.18,!go1.22 3 | 4 | package optionext 5 | 6 | import ( 7 | "database/sql" 8 | "database/sql/driver" 9 | "encoding/json" 10 | "fmt" 11 | "math" 12 | "reflect" 13 | "time" 14 | ) 15 | 16 | var ( 17 | scanType = reflect.TypeOf((*sql.Scanner)(nil)).Elem() 18 | byteSliceType = reflect.TypeOf(([]byte)(nil)) 19 | valuerType = reflect.TypeOf((*driver.Valuer)(nil)).Elem() 20 | timeType = reflect.TypeOf((*time.Time)(nil)).Elem() 21 | stringType = reflect.TypeOf((*string)(nil)).Elem() 22 | int64Type = reflect.TypeOf((*int64)(nil)).Elem() 23 | float64Type = reflect.TypeOf((*float64)(nil)).Elem() 24 | boolType = reflect.TypeOf((*bool)(nil)).Elem() 25 | ) 26 | 27 | // Value implements the driver.Valuer interface. 28 | // 29 | // This honours the `driver.Valuer` interface if the value implements it. 30 | // It also supports custom types of the std types and treats all else as []byte 31 | func (o Option[T]) Value() (driver.Value, error) { 32 | if o.IsNone() { 33 | return nil, nil 34 | } 35 | val := reflect.ValueOf(o.value) 36 | 37 | if val.Type().Implements(valuerType) { 38 | return val.Interface().(driver.Valuer).Value() 39 | } 40 | switch val.Kind() { 41 | case reflect.String: 42 | return val.Convert(stringType).Interface(), nil 43 | case reflect.Bool: 44 | return val.Convert(boolType).Interface(), nil 45 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 46 | return val.Convert(int64Type).Interface(), nil 47 | case reflect.Float64: 48 | return val.Convert(float64Type).Interface(), nil 49 | case reflect.Slice, reflect.Array: 50 | if val.Type().ConvertibleTo(byteSliceType) { 51 | return val.Convert(byteSliceType).Interface(), nil 52 | } 53 | return json.Marshal(val.Interface()) 54 | case reflect.Struct: 55 | if val.CanConvert(timeType) { 56 | return val.Convert(timeType).Interface(), nil 57 | } 58 | return json.Marshal(val.Interface()) 59 | case reflect.Map: 60 | return json.Marshal(val.Interface()) 61 | default: 62 | return o.value, nil 63 | } 64 | } 65 | 66 | // Scan implements the sql.Scanner interface. 67 | func (o *Option[T]) Scan(value any) error { 68 | 69 | if value == nil { 70 | *o = None[T]() 71 | return nil 72 | } 73 | 74 | val := reflect.ValueOf(&o.value) 75 | 76 | if val.Type().Implements(scanType) { 77 | err := val.Interface().(sql.Scanner).Scan(value) 78 | if err != nil { 79 | return err 80 | } 81 | o.isSome = true 82 | return nil 83 | } 84 | 85 | val = val.Elem() 86 | 87 | switch val.Kind() { 88 | case reflect.String: 89 | var v sql.NullString 90 | if err := v.Scan(value); err != nil { 91 | return err 92 | } 93 | *o = Some(reflect.ValueOf(v.String).Convert(val.Type()).Interface().(T)) 94 | case reflect.Bool: 95 | var v sql.NullBool 96 | if err := v.Scan(value); err != nil { 97 | return err 98 | } 99 | *o = Some(reflect.ValueOf(v.Bool).Convert(val.Type()).Interface().(T)) 100 | case reflect.Uint8: 101 | var v sql.NullByte 102 | if err := v.Scan(value); err != nil { 103 | return err 104 | } 105 | *o = Some(reflect.ValueOf(v.Byte).Convert(val.Type()).Interface().(T)) 106 | case reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: 107 | v := reflect.ValueOf(value) 108 | if v.Type().ConvertibleTo(val.Type()) { 109 | *o = Some(reflect.ValueOf(v.Convert(val.Type()).Interface()).Interface().(T)) 110 | } else { 111 | return fmt.Errorf("value %T not convertable to %T", value, o.value) 112 | } 113 | case reflect.Float32: 114 | var v sql.NullFloat64 115 | if err := v.Scan(value); err != nil { 116 | return err 117 | } 118 | *o = Some(reflect.ValueOf(v.Float64).Convert(val.Type()).Interface().(T)) 119 | case reflect.Float64: 120 | var v sql.NullFloat64 121 | if err := v.Scan(value); err != nil { 122 | return err 123 | } 124 | *o = Some(reflect.ValueOf(v.Float64).Convert(val.Type()).Interface().(T)) 125 | case reflect.Int: 126 | var v sql.NullInt64 127 | if err := v.Scan(value); err != nil { 128 | return err 129 | } 130 | if v.Int64 > math.MaxInt || v.Int64 < math.MinInt { 131 | return fmt.Errorf("value %d out of range for int", v.Int64) 132 | } 133 | *o = Some(reflect.ValueOf(v.Int64).Convert(val.Type()).Interface().(T)) 134 | case reflect.Int8: 135 | var v sql.NullInt64 136 | if err := v.Scan(value); err != nil { 137 | return err 138 | } 139 | if v.Int64 > math.MaxInt8 || v.Int64 < math.MinInt8 { 140 | return fmt.Errorf("value %d out of range for int8", v.Int64) 141 | } 142 | *o = Some(reflect.ValueOf(v.Int64).Convert(val.Type()).Interface().(T)) 143 | case reflect.Int16: 144 | var v sql.NullInt16 145 | if err := v.Scan(value); err != nil { 146 | return err 147 | } 148 | *o = Some(reflect.ValueOf(v.Int16).Convert(val.Type()).Interface().(T)) 149 | case reflect.Int32: 150 | var v sql.NullInt32 151 | if err := v.Scan(value); err != nil { 152 | return err 153 | } 154 | *o = Some(reflect.ValueOf(v.Int32).Convert(val.Type()).Interface().(T)) 155 | case reflect.Int64: 156 | var v sql.NullInt64 157 | if err := v.Scan(value); err != nil { 158 | return err 159 | } 160 | *o = Some(reflect.ValueOf(v.Int64).Convert(val.Type()).Interface().(T)) 161 | case reflect.Interface: 162 | *o = Some(reflect.ValueOf(value).Convert(val.Type()).Interface().(T)) 163 | case reflect.Struct: 164 | if val.CanConvert(timeType) { 165 | switch t := value.(type) { 166 | case string: 167 | tm, err := time.Parse(time.RFC3339Nano, t) 168 | if err != nil { 169 | return err 170 | } 171 | *o = Some(reflect.ValueOf(tm).Convert(val.Type()).Interface().(T)) 172 | 173 | case []byte: 174 | tm, err := time.Parse(time.RFC3339Nano, string(t)) 175 | if err != nil { 176 | return err 177 | } 178 | *o = Some(reflect.ValueOf(tm).Convert(val.Type()).Interface().(T)) 179 | 180 | default: 181 | var v sql.NullTime 182 | if err := v.Scan(value); err != nil { 183 | return err 184 | } 185 | *o = Some(reflect.ValueOf(v.Time).Convert(val.Type()).Interface().(T)) 186 | } 187 | return nil 188 | } 189 | fallthrough 190 | 191 | default: 192 | switch val.Kind() { 193 | case reflect.Struct, reflect.Slice, reflect.Map: 194 | v := reflect.ValueOf(value) 195 | 196 | if v.Type().ConvertibleTo(byteSliceType) { 197 | if val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8 { 198 | *o = Some(reflect.ValueOf(v.Convert(val.Type()).Interface()).Interface().(T)) 199 | } else { 200 | if err := json.Unmarshal(v.Convert(byteSliceType).Interface().([]byte), &o.value); err != nil { 201 | return err 202 | } 203 | } 204 | o.isSome = true 205 | return nil 206 | } 207 | } 208 | return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", value, o.value) 209 | } 210 | return nil 211 | } 212 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [5.30.0] - 2024-06-01 10 | ### Changed 11 | - Changed NanoTome to not use linkname due to Go1.23 upcoming breaking changes. 12 | 13 | ## [5.29.1] - 2024-04-04 14 | ### Fixed 15 | - Added HTTP 404 to non retryable status codes. 16 | 17 | ## [5.29.0] - 2024-03-24 18 | ### Added 19 | - `asciiext` package for ASCII related functions. 20 | - `errorsext.Retrier` configurable retry helper for any fallible operation. 21 | - `httpext.Retrier` configurable retry helper for HTTP requests and parsing of responses. 22 | - `httpext.DecodeResponseAny` non-generic helper for decoding HTTP responses. 23 | - `httpext.HasRetryAfter` helper for checking if a response has a `Retry-After` header and returning duration to wait. 24 | 25 | ## [5.28.1] - 2024-02-14 26 | ### Fixed 27 | - Additional supported types, cast to `sql.Valuer` supported types, they need to be returned to the driver for evaluation. 28 | 29 | ## [5.28.0] - 2024-02-13 30 | ### Added 31 | - Additionally supported types, cast to `sql.Valuer` supported types. 32 | 33 | ### Changed 34 | - Option scan to take advantage of new `sql.Null` and `reflect.TypeFor` for go1.22+. 35 | - `BytesToString` & `StringToBytes` to use `unsafe.String` & `unsafe.Slice` for go1.21+. 36 | 37 | ### Deprecated 38 | - `mathext.Min` & `mathext.Max` in favour of std lib min & max. 39 | 40 | ### Fixed 41 | - Some documentation typos. 42 | 43 | ## [5.27.0] - 2024-01-29 44 | ### Changed 45 | - `sliceext.Retain` & `sliceext.Filter` to not shuffle data in the underlying slice array but create new slice referencing the data instead. In practice, it can cause unexpected behaviour and users expectations not met when the same data is also referenced elsewhere. If anyone still requires a `shuffle` implementation for efficiency I'd be happy to add a separate function for that as well. 46 | 47 | ## [5.26.0] - 2024-01-28 48 | ### Added 49 | - `stringsext.Join` a more ergonomic way to join strings with a separator when you don't have a slice of strings. 50 | 51 | ## [5.25.0] - 2024-01-22 52 | ### Added 53 | - Add additional `Option.Scan` type support for `sql.Scanner` interface of Uint, Uint16, Uint32, Uint64, Int, Int, Int8, Float32, []byte, json.RawValue. 54 | 55 | ## [5.24.0] - 2024-01-21 56 | ### Added 57 | - `appext` package for application level helpers. Specifically added setting up os signal trapping and cancellation of context.Context. 58 | 59 | ## [5.23.0] - 2024-01-14 60 | ### Added 61 | - `And` and `AndThen` functions to `Option` & `Result` types. 62 | 63 | ## [5.22.0] - 2023-10-18 64 | ### Added 65 | - `UnwrapOr`, `UnwrapOrElse` and `UnwrapOrDefault` functions to `Option` & `Result` types. 66 | 67 | ## [5.21.3] - 2023-10-11 68 | ### Fixed 69 | - Fix SQL Scanner interface not returning None for Option when source data is nil. 70 | 71 | ## [5.21.2] - 2023-07-13 72 | ### Fixed 73 | - Updated default form/url.Value encoder/decoder with fix for bubbling up invalid array index values. 74 | 75 | ## [5.21.1] - 2023-06-30 76 | ### Fixed 77 | - Instant type to not be wrapped in a struct but a type itself. 78 | 79 | ## [5.21.0] - 2023-06-30 80 | ### Added 81 | - Instant type to make working with monotonically increasing times more convenient. 82 | 83 | ## [5.20.0] - 2023-06-17 84 | ### Added 85 | - Expanded Option type SQL Value support to handle value custom types and honour the `driver.Valuer` interface. 86 | 87 | ### Changed 88 | - Option sql.Scanner to support custom types. 89 | 90 | ## [5.19.0] - 2023-06-14 91 | ### Added 92 | - strconvext.ParseBool(...) which is a drop-in replacement for the std lin strconv.ParseBool(..) with a few more supported values. 93 | - Expanded Option type SQL Scan support to handle Scanning to an Interface, Struct, Slice, Map and anything that implements the sql.Scanner interface. 94 | 95 | ## [5.18.0] - 2023-05-21 96 | ### Added 97 | - typesext.Nothing & valuesext.Nothing for better clarity in generic params and values that represent struct{}. This will provide better code readability and intent. 98 | 99 | ## [5.17.2] - 2023-05-09 100 | ### Fixed 101 | - Prematurely closing http.Response Body before error with it can be intercepted for ErrUnexpectedResponse. 102 | 103 | ## [5.17.1] - 2023-05-09 104 | ### Fixed 105 | - ErrRetryableStatusCode passing the *http.Response to have access to not only the status code but headers etc. related to retrying. 106 | - Added ErrUnexpectedResponse to pass back when encountering an unexpected response code to allow the caller to decide what to do. 107 | 108 | ## [5.17.0] - 2023-05-08 109 | ### Added 110 | - bytesext.Bytes alias to int64 for better code clarity. 111 | - errorext.DoRetryable(...) building block for automating retryable errors. 112 | - sqlext.DoTransaction(...) building block for abstracting away transactions. 113 | - httpext.DoRetryableResponse(...) & httpext.DoRetryable(...) building blocks for automating retryable http requests. 114 | - httpext.DecodeResponse(...) building block for decoding http responses. 115 | - httpext.ErrRetryableStatusCode error for retryable http status code detection and handling. 116 | - errorsext.ErrMaxAttemptsReached error for retryable retryable logic & reuse. 117 | 118 | ## [5.16.0] - 2023-04-16 119 | ### Added 120 | - sliceext.Reverse(...) 121 | 122 | ## [5.15.2] - 2023-03-06 123 | ### Remove 124 | - Unnecessary second type param for Mutex2. 125 | 126 | ## [5.15.1] - 2023-03-06 127 | ### Fixed 128 | - New Mutex2 functions and guards; checked in the wrong code accidentally last commit. 129 | 130 | ## [5.15.0] - 2023-03-05 131 | ### Added 132 | - New Mutex2 and RWMutex2 which corrects the original Mutex's design issues. 133 | - Deprecation warning for original Mutex usage. 134 | 135 | ## [5.14.0] - 2023-02-25 136 | ### Added 137 | - Added `timext.NanoTime` for fast low level monotonic time with nanosecond precision. 138 | 139 | [Unreleased]: https://github.com/go-playground/pkg/compare/v5.30.0...HEAD 140 | [5.30.0]: https://github.com/go-playground/pkg/compare/v5.29.1..v5.30.0 141 | [5.29.1]: https://github.com/go-playground/pkg/compare/v5.29.0..v5.29.1 142 | [5.29.0]: https://github.com/go-playground/pkg/compare/v5.28.1..v5.29.0 143 | [5.28.1]: https://github.com/go-playground/pkg/compare/v5.28.0..v5.28.1 144 | [5.28.0]: https://github.com/go-playground/pkg/compare/v5.27.0..v5.28.0 145 | [5.27.0]: https://github.com/go-playground/pkg/compare/v5.26.0..v5.27.0 146 | [5.26.0]: https://github.com/go-playground/pkg/compare/v5.25.0..v5.26.0 147 | [5.25.0]: https://github.com/go-playground/pkg/compare/v5.24.0..v5.25.0 148 | [5.24.0]: https://github.com/go-playground/pkg/compare/v5.23.0..v5.24.0 149 | [5.23.0]: https://github.com/go-playground/pkg/compare/v5.22.0..v5.23.0 150 | [5.22.0]: https://github.com/go-playground/pkg/compare/v5.21.3..v5.22.0 151 | [5.21.3]: https://github.com/go-playground/pkg/compare/v5.21.2..v5.21.3 152 | [5.21.2]: https://github.com/go-playground/pkg/compare/v5.21.1..v5.21.2 153 | [5.21.1]: https://github.com/go-playground/pkg/compare/v5.21.0..v5.21.1 154 | [5.21.0]: https://github.com/go-playground/pkg/compare/v5.20.0..v5.21.0 155 | [5.20.0]: https://github.com/go-playground/pkg/compare/v5.19.0..v5.20.0 156 | [5.19.0]: https://github.com/go-playground/pkg/compare/v5.18.0..v5.19.0 157 | [5.18.0]: https://github.com/go-playground/pkg/compare/v5.17.2..v5.18.0 158 | [5.17.2]: https://github.com/go-playground/pkg/compare/v5.17.1..v5.17.2 159 | [5.17.1]: https://github.com/go-playground/pkg/compare/v5.17.0...v5.17.1 160 | [5.17.0]: https://github.com/go-playground/pkg/compare/v5.16.0...v5.17.0 161 | [5.16.0]: https://github.com/go-playground/pkg/compare/v5.15.2...v5.16.0 162 | [5.15.2]: https://github.com/go-playground/pkg/compare/v5.15.1...v5.15.2 163 | [5.15.1]: https://github.com/go-playground/pkg/compare/v5.15.0...v5.15.1 164 | [5.15.0]: https://github.com/go-playground/pkg/compare/v5.14.0...v5.15.0 165 | [5.14.0]: https://github.com/go-playground/pkg/commit/v5.14.0 -------------------------------------------------------------------------------- /container/list/doubly_linked.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package listext 5 | 6 | // Node is an element of the doubly linked list. 7 | type Node[V any] struct { 8 | next, prev *Node[V] 9 | Value V 10 | } 11 | 12 | // Next returns the nodes next Value or nil if it is at the tail. 13 | func (n *Node[V]) Next() *Node[V] { 14 | return n.next 15 | } 16 | 17 | // Prev returns the nodes previous Value or nil if it is at the head. 18 | func (n *Node[V]) Prev() *Node[V] { 19 | return n.prev 20 | } 21 | 22 | // DoublyLinkedList is a doubly linked list 23 | type DoublyLinkedList[V any] struct { 24 | head, tail *Node[V] 25 | len int 26 | } 27 | 28 | // NewDoublyLinked creates a DoublyLinkedList for use. 29 | func NewDoublyLinked[V any]() *DoublyLinkedList[V] { 30 | return new(DoublyLinkedList[V]) 31 | } 32 | 33 | // PushFront adds an element first in the list. 34 | func (d *DoublyLinkedList[V]) PushFront(v V) *Node[V] { 35 | node := &Node[V]{ 36 | Value: v, 37 | } 38 | d.pushFront(node) 39 | return d.head 40 | } 41 | 42 | func (d *DoublyLinkedList[V]) pushFront(node *Node[V]) { 43 | node.next = d.head 44 | node.prev = nil 45 | 46 | if d.head == nil { 47 | d.tail = node 48 | } else { 49 | d.head.prev = node 50 | } 51 | d.head = node 52 | d.len++ 53 | } 54 | 55 | // PopFront removes the first element and returns it or nil. 56 | func (d *DoublyLinkedList[V]) PopFront() *Node[V] { 57 | if d.IsEmpty() { 58 | return nil 59 | } 60 | 61 | node := d.head 62 | d.head = node.next 63 | if d.head == nil { 64 | d.tail = nil 65 | } else { 66 | d.head.prev = nil 67 | } 68 | d.len-- 69 | // ensure no leakage 70 | node.next, node.prev = nil, nil 71 | return node 72 | } 73 | 74 | // PushBack appends an element to the back of a list. 75 | func (d *DoublyLinkedList[V]) PushBack(v V) *Node[V] { 76 | node := &Node[V]{ 77 | Value: v, 78 | } 79 | d.pushBack(node) 80 | return d.tail 81 | } 82 | 83 | func (d *DoublyLinkedList[V]) pushBack(node *Node[V]) { 84 | node.prev = d.tail 85 | node.next = nil 86 | 87 | if d.tail == nil { 88 | d.head = node 89 | } else { 90 | d.tail.next = node 91 | } 92 | d.tail = node 93 | d.len++ 94 | } 95 | 96 | // PushAfter pushes the supplied Value after the supplied node. 97 | // 98 | // The supplied node must be attached to the current list otherwise undefined behaviour could occur. 99 | func (d *DoublyLinkedList[V]) PushAfter(node *Node[V], v V) *Node[V] { 100 | newNode := &Node[V]{ 101 | Value: v, 102 | } 103 | d.moveAfter(node, newNode) 104 | return newNode 105 | } 106 | 107 | // MoveAfter moves the `moving` node after the supplied `node`. 108 | // 109 | // The supplied `node` and `moving` nodes must be attached to the current list otherwise 110 | // undefined behaviour could occur. 111 | func (d *DoublyLinkedList[V]) MoveAfter(node *Node[V], moving *Node[V]) { 112 | // first detach node were moving after, in case it was already attached somewhere else in the list. 113 | d.Remove(moving) 114 | d.moveAfter(node, moving) 115 | } 116 | 117 | func (d *DoublyLinkedList[V]) moveAfter(node *Node[V], moving *Node[V]) { 118 | next := node.next 119 | 120 | // no next means node == d.tail 121 | if next == nil { 122 | d.pushBack(moving) 123 | } else { 124 | node.next = moving 125 | moving.prev = node 126 | moving.next = next 127 | next.prev = moving 128 | d.len++ 129 | } 130 | } 131 | 132 | // PushBefore pushes the supplied Value before the supplied node. 133 | // 134 | // The supplied node must be attached to the current list otherwise undefined behaviour could occur. 135 | func (d *DoublyLinkedList[V]) PushBefore(node *Node[V], v V) *Node[V] { 136 | newNode := &Node[V]{ 137 | Value: v, 138 | } 139 | d.moveBefore(node, newNode) 140 | return newNode 141 | } 142 | 143 | // InsertBefore inserts the supplied node before the supplied node. 144 | // 145 | // The supplied node must be attached to the current list otherwise undefined behaviour could occur. 146 | func (d *DoublyLinkedList[V]) InsertBefore(node *Node[V], inserting *Node[V]) { 147 | d.moveBefore(node, inserting) 148 | } 149 | 150 | // InsertAfter inserts the supplied node after the supplied node. 151 | // 152 | // The supplied node must be attached to the current list otherwise undefined behaviour could occur. 153 | func (d *DoublyLinkedList[V]) InsertAfter(node *Node[V], inserting *Node[V]) { 154 | d.moveAfter(node, inserting) 155 | } 156 | 157 | // MoveBefore moves the `moving` node before the supplied `node`. 158 | // 159 | // The supplied `node` and `moving` nodes must be attached to the current list otherwise 160 | // undefined behaviour could occur. 161 | func (d *DoublyLinkedList[V]) MoveBefore(node *Node[V], moving *Node[V]) { 162 | // first detach node were moving after, in case it was already attached somewhere else in the list. 163 | d.Remove(moving) 164 | d.moveBefore(node, moving) 165 | } 166 | 167 | func (d *DoublyLinkedList[V]) moveBefore(node *Node[V], moving *Node[V]) { 168 | prev := node.prev 169 | 170 | // no prev means node == d.head 171 | if prev == nil { 172 | d.pushFront(moving) 173 | } else { 174 | node.prev = moving 175 | moving.next = node 176 | moving.prev = prev 177 | prev.next = moving 178 | d.len++ 179 | } 180 | } 181 | 182 | // PopBack removes the last element from a list and returns it or nil. 183 | func (d *DoublyLinkedList[V]) PopBack() *Node[V] { 184 | if d.IsEmpty() { 185 | return nil 186 | } 187 | 188 | node := d.tail 189 | d.tail = node.prev 190 | 191 | if d.tail == nil { 192 | d.head = nil 193 | } else { 194 | d.tail.next = nil 195 | } 196 | d.len-- 197 | // ensure no leakage 198 | node.next, node.prev = nil, nil 199 | return node 200 | } 201 | 202 | // Front returns the front/head element for use without removing it or nil list is empty. 203 | func (d *DoublyLinkedList[V]) Front() *Node[V] { 204 | return d.head 205 | } 206 | 207 | // Back returns the end/tail element for use without removing it or nil list is empty. 208 | func (d *DoublyLinkedList[V]) Back() *Node[V] { 209 | return d.tail 210 | } 211 | 212 | // Remove removes the provided element from the Linked List. 213 | // 214 | // The supplied node must be attached to the current list otherwise undefined behaviour could occur. 215 | func (d *DoublyLinkedList[V]) Remove(node *Node[V]) { 216 | if node.prev == nil { 217 | // is head node 218 | _ = d.PopFront() 219 | } else if node.next == nil { 220 | // is tail node 221 | _ = d.PopBack() 222 | } else { 223 | // is both head and tail nodes, must remap 224 | node.next.prev = node.prev 225 | node.prev.next = node.next 226 | // ensure no leakage 227 | node.next, node.prev = nil, nil 228 | d.len-- 229 | } 230 | } 231 | 232 | // MoveToFront moves the provided node to the front/head. 233 | // 234 | // The supplied node must be attached to the current list otherwise undefined behaviour could occur. 235 | func (d *DoublyLinkedList[V]) MoveToFront(node *Node[V]) { 236 | d.Remove(node) 237 | d.pushFront(node) 238 | } 239 | 240 | // InsertAtFront pushes the provided node to the front/head. 241 | // 242 | // The supplied node must not be attached to any list otherwise undefined behaviour could occur. 243 | func (d *DoublyLinkedList[V]) InsertAtFront(node *Node[V]) { 244 | d.pushFront(node) 245 | } 246 | 247 | // MoveToBack moves the provided node to the end/tail. 248 | // 249 | // The supplied node must be attached to the current list otherwise undefined behaviour could occur. 250 | func (d *DoublyLinkedList[V]) MoveToBack(node *Node[V]) { 251 | d.Remove(node) 252 | d.pushBack(node) 253 | } 254 | 255 | // InsertAtBack pushes the provided node to the back/tail. 256 | // 257 | // The supplied node must not be attached to any list otherwise undefined behaviour could occur. 258 | func (d *DoublyLinkedList[V]) InsertAtBack(node *Node[V]) { 259 | d.pushBack(node) 260 | } 261 | 262 | // IsEmpty returns true if the list is empty. 263 | func (d *DoublyLinkedList[V]) IsEmpty() bool { 264 | return d.len == 0 265 | } 266 | 267 | // Len returns length of the Linked List. 268 | func (d *DoublyLinkedList[V]) Len() int { 269 | return d.len 270 | } 271 | 272 | // Clear removes all elements from the Linked List. 273 | func (d *DoublyLinkedList[V]) Clear() { 274 | // must loop and clean up references to each other. 275 | for { 276 | if d.PopBack() == nil { 277 | break 278 | } 279 | } 280 | d.head, d.tail, d.len = nil, nil, 0 281 | } 282 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /net/http/retrier.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package httpext 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "io" 10 | "net/http" 11 | "strconv" 12 | "time" 13 | 14 | bytesext "github.com/go-playground/pkg/v5/bytes" 15 | errorsext "github.com/go-playground/pkg/v5/errors" 16 | ioext "github.com/go-playground/pkg/v5/io" 17 | typesext "github.com/go-playground/pkg/v5/types" 18 | valuesext "github.com/go-playground/pkg/v5/values" 19 | . "github.com/go-playground/pkg/v5/values/result" 20 | ) 21 | 22 | // ErrStatusCode can be used to treat/indicate a status code as an error and ability to indicate if it is retryable. 23 | type ErrStatusCode struct { 24 | // StatusCode is the HTTP response status code that was encountered. 25 | StatusCode int 26 | 27 | // IsRetryableStatusCode indicates if the status code is considered retryable. 28 | IsRetryableStatusCode bool 29 | 30 | // Headers contains the headers from the HTTP response. 31 | Headers http.Header 32 | 33 | // Body is the optional body of the HTTP response. 34 | Body []byte 35 | } 36 | 37 | // Error returns the error message for the status code. 38 | func (e ErrStatusCode) Error() string { 39 | return "status code encountered: " + strconv.Itoa(e.StatusCode) 40 | } 41 | 42 | // IsRetryable returns if the provided status code is considered retryable. 43 | func (e ErrStatusCode) IsRetryable() bool { 44 | return e.IsRetryableStatusCode 45 | } 46 | 47 | // BuildRequestFn2 is a function used to rebuild an HTTP request for use in retryable code. 48 | type BuildRequestFn2 func(ctx context.Context) Result[*http.Request, error] 49 | 50 | // DecodeAnyFn is a function used to decode the response body into the desired type. 51 | type DecodeAnyFn func(ctx context.Context, resp *http.Response, maxMemory bytesext.Bytes, v any) error 52 | 53 | // IsRetryableStatusCodeFn2 is a function used to determine if the provided status code is considered retryable. 54 | type IsRetryableStatusCodeFn2 func(ctx context.Context, code int) bool 55 | 56 | // Retryer is used to retry any fallible operation. 57 | // 58 | // The `Retryer` is designed to be stateless and reusable. Configuration is also copy and so a base `Retryer` can be 59 | // used and changed for one-off requests eg. changing max attempts resulting in a new `Retrier` for that request. 60 | type Retryer struct { 61 | isRetryableFn errorsext.IsRetryableFn2[error] 62 | isRetryableStatusCodeFn IsRetryableStatusCodeFn2 63 | isEarlyReturnFn errorsext.EarlyReturnFn[error] 64 | decodeFn DecodeAnyFn 65 | backoffFn errorsext.BackoffFn[error] 66 | client *http.Client 67 | timeout time.Duration 68 | maxBytes bytesext.Bytes 69 | mode errorsext.MaxAttemptsMode 70 | maxAttempts uint8 71 | } 72 | 73 | // NewRetryer returns a new `Retryer` with sane default values. 74 | // 75 | // The default values are: 76 | // - `IsRetryableFn` uses the existing `errorsext.IsRetryableHTTP` function. 77 | // - `MaxAttemptsMode` is `MaxAttemptsNonRetryableReset`. 78 | // - `MaxAttempts` is 5. 79 | // - `BackoffFn` will sleep for 200ms or is successful `Retry-After` header can be parsed. It's recommended to use 80 | // exponential backoff for production with a quick copy-paste-modify of the default function 81 | // - `Timeout` is 0. 82 | // - `IsRetryableStatusCodeFn` is set to the existing `IsRetryableStatusCode` function. 83 | // - `IsEarlyReturnFn` is set to check if the error is an `ErrStatusCode` and if the status code is non-retryable. 84 | // - `Client` is set to `http.DefaultClient`. 85 | // - `MaxBytes` is set to 2MiB. 86 | // - `DecodeAnyFn` is set to the existing `DecodeResponseAny` function that supports JSON and XML. 87 | // 88 | // WARNING: The default functions may receive enhancements or fixes in the future which could change their behavior, 89 | // however every attempt will be made to maintain backwards compatibility or made additive-only if possible. 90 | func NewRetryer() Retryer { 91 | return Retryer{ 92 | client: http.DefaultClient, 93 | maxBytes: 2 * bytesext.MiB, 94 | mode: errorsext.MaxAttemptsNonRetryableReset, 95 | maxAttempts: 5, 96 | isRetryableFn: func(ctx context.Context, err error) (isRetryable bool) { 97 | _, isRetryable = errorsext.IsRetryableHTTP(err) 98 | return 99 | }, 100 | isRetryableStatusCodeFn: func(_ context.Context, code int) bool { return IsRetryableStatusCode(code) }, 101 | isEarlyReturnFn: func(_ context.Context, err error) bool { 102 | var sce ErrStatusCode 103 | if errors.As(err, &sce) { 104 | return IsNonRetryableStatusCode(sce.StatusCode) 105 | } 106 | return false 107 | }, 108 | decodeFn: func(ctx context.Context, resp *http.Response, maxMemory bytesext.Bytes, v any) error { 109 | err := DecodeResponseAny(resp, maxMemory, v) 110 | if err != nil { 111 | return err 112 | } 113 | return nil 114 | }, 115 | backoffFn: func(ctx context.Context, attempt int, err error) { 116 | 117 | wait := time.Millisecond * 200 118 | 119 | var sce ErrStatusCode 120 | if errors.As(err, &sce) { 121 | if sce.Headers != nil && (sce.StatusCode == http.StatusTooManyRequests || sce.StatusCode == http.StatusServiceUnavailable) { 122 | if ra := HasRetryAfter(sce.Headers); ra.IsSome() { 123 | wait = ra.Unwrap() 124 | } 125 | } 126 | } 127 | 128 | t := time.NewTimer(wait) 129 | defer t.Stop() 130 | select { 131 | case <-ctx.Done(): 132 | case <-t.C: 133 | } 134 | }, 135 | } 136 | } 137 | 138 | // Client sets the `http.Client` for the `Retryer`. 139 | func (r Retryer) Client(client *http.Client) Retryer { 140 | r.client = client 141 | return r 142 | } 143 | 144 | // IsRetryableFn sets the `IsRetryableFn` for the `Retryer`. 145 | func (r Retryer) IsRetryableFn(fn errorsext.IsRetryableFn2[error]) Retryer { 146 | r.isRetryableFn = fn 147 | return r 148 | } 149 | 150 | // IsRetryableStatusCodeFn is called to determine if the status code is retryable. 151 | func (r Retryer) IsRetryableStatusCodeFn(fn IsRetryableStatusCodeFn2) Retryer { 152 | if fn == nil { 153 | fn = func(_ context.Context, _ int) bool { return false } 154 | } 155 | r.isRetryableStatusCodeFn = fn 156 | return r 157 | } 158 | 159 | // IsEarlyReturnFn sets the `EarlyReturnFn` for the `Retryer`. 160 | func (r Retryer) IsEarlyReturnFn(fn errorsext.EarlyReturnFn[error]) Retryer { 161 | r.isEarlyReturnFn = fn 162 | return r 163 | } 164 | 165 | // DecodeFn sets the decode function for the `Retryer`. 166 | func (r Retryer) DecodeFn(fn DecodeAnyFn) Retryer { 167 | if fn == nil { 168 | fn = func(_ context.Context, _ *http.Response, _ bytesext.Bytes, _ any) error { return nil } 169 | } 170 | r.decodeFn = fn 171 | return r 172 | } 173 | 174 | // MaxAttempts sets the maximum number of attempts for the `Retryer`. 175 | // 176 | // NOTE: Max attempts is optional and if not set will retry indefinitely on retryable errors. 177 | func (r Retryer) MaxAttempts(mode errorsext.MaxAttemptsMode, maxAttempts uint8) Retryer { 178 | r.mode, r.maxAttempts = mode, maxAttempts 179 | return r 180 | } 181 | 182 | // Backoff sets the backoff function for the `Retryer`. 183 | func (r Retryer) Backoff(fn errorsext.BackoffFn[error]) Retryer { 184 | r.backoffFn = fn 185 | return r 186 | } 187 | 188 | // MaxBytes sets the maximum memory to use when decoding the response body including: 189 | // - upon unexpected status codes. 190 | // - when decoding the response body. 191 | // - when draining the response body before closing allowing connection re-use. 192 | func (r Retryer) MaxBytes(i bytesext.Bytes) Retryer { 193 | r.maxBytes = i 194 | return r 195 | 196 | } 197 | 198 | // Timeout sets the timeout for the `Retryer`. This is the timeout per `RetyableFn` attempt and not the entirety 199 | // of the `Retryer` execution. 200 | // 201 | // A timeout of 0 will disable the timeout and is the default. 202 | func (r Retryer) Timeout(timeout time.Duration) Retryer { 203 | r.timeout = timeout 204 | return r 205 | } 206 | 207 | // DoResponse will execute the provided functions code and automatically retry before returning the *http.Response 208 | // based on HTTP status code, if defined, and can be used when processing of the response body may not be necessary 209 | // or something custom is required. 210 | // 211 | // NOTE: it is up to the caller to close the response body if a successful request is made. 212 | func (r Retryer) DoResponse(ctx context.Context, fn BuildRequestFn2, expectedResponseCodes ...int) Result[*http.Response, error] { 213 | return errorsext.NewRetryer[*http.Response, error](). 214 | IsRetryableFn(r.isRetryableFn). 215 | MaxAttempts(r.mode, r.maxAttempts). 216 | Backoff(r.backoffFn). 217 | Timeout(r.timeout). 218 | IsEarlyReturnFn(r.isEarlyReturnFn). 219 | Do(ctx, func(ctx context.Context) Result[*http.Response, error] { 220 | req := fn(ctx) 221 | if req.IsErr() { 222 | return Err[*http.Response, error](req.Err()) 223 | } 224 | 225 | resp, err := r.client.Do(req.Unwrap()) 226 | if err != nil { 227 | return Err[*http.Response, error](err) 228 | } 229 | 230 | if len(expectedResponseCodes) > 0 { 231 | for _, code := range expectedResponseCodes { 232 | if resp.StatusCode == code { 233 | goto RETURN 234 | } 235 | } 236 | b, _ := io.ReadAll(ioext.LimitReader(resp.Body, r.maxBytes)) 237 | _ = resp.Body.Close() 238 | return Err[*http.Response, error](ErrStatusCode{ 239 | StatusCode: resp.StatusCode, 240 | IsRetryableStatusCode: r.isRetryableStatusCodeFn(ctx, resp.StatusCode), 241 | Headers: resp.Header, 242 | Body: b, 243 | }) 244 | } 245 | 246 | RETURN: 247 | return Ok[*http.Response, error](resp) 248 | }) 249 | } 250 | 251 | // Do will execute the provided functions code and automatically retry using the provided retry function decoding 252 | // the response body into the desired type `v`, which must be passed as mutable. 253 | func (r Retryer) Do(ctx context.Context, fn BuildRequestFn2, v any, expectedResponseCodes ...int) error { 254 | result := errorsext.NewRetryer[typesext.Nothing, error](). 255 | IsRetryableFn(r.isRetryableFn). 256 | MaxAttempts(r.mode, r.maxAttempts). 257 | Backoff(r.backoffFn). 258 | Timeout(r.timeout). 259 | IsEarlyReturnFn(r.isEarlyReturnFn). 260 | Do(ctx, func(ctx context.Context) Result[typesext.Nothing, error] { 261 | req := fn(ctx) 262 | if req.IsErr() { 263 | return Err[typesext.Nothing, error](req.Err()) 264 | } 265 | 266 | resp, err := r.client.Do(req.Unwrap()) 267 | if err != nil { 268 | return Err[typesext.Nothing, error](err) 269 | } 270 | defer func() { 271 | _, _ = io.Copy(io.Discard, ioext.LimitReader(resp.Body, r.maxBytes)) 272 | _ = resp.Body.Close() 273 | }() 274 | 275 | if len(expectedResponseCodes) > 0 { 276 | for _, code := range expectedResponseCodes { 277 | if resp.StatusCode == code { 278 | goto DECODE 279 | } 280 | } 281 | 282 | b, _ := io.ReadAll(ioext.LimitReader(resp.Body, r.maxBytes)) 283 | return Err[typesext.Nothing, error](ErrStatusCode{ 284 | StatusCode: resp.StatusCode, 285 | IsRetryableStatusCode: r.isRetryableStatusCodeFn(ctx, resp.StatusCode), 286 | Headers: resp.Header, 287 | Body: b, 288 | }) 289 | } 290 | 291 | DECODE: 292 | if err = r.decodeFn(ctx, resp, r.maxBytes, v); err != nil { 293 | return Err[typesext.Nothing, error](err) 294 | } 295 | return Ok[typesext.Nothing, error](valuesext.Nothing) 296 | }) 297 | if result.IsErr() { 298 | return result.Err() 299 | } 300 | return nil 301 | } 302 | -------------------------------------------------------------------------------- /net/http/helpers.go: -------------------------------------------------------------------------------- 1 | package httpext 2 | 3 | import ( 4 | "compress/gzip" 5 | "encoding/json" 6 | "encoding/xml" 7 | "errors" 8 | "io" 9 | "mime" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | "path/filepath" 14 | "strings" 15 | 16 | bytesext "github.com/go-playground/pkg/v5/bytes" 17 | ioext "github.com/go-playground/pkg/v5/io" 18 | ) 19 | 20 | // QueryParamsOption represents the options for including query parameters during Decode helper functions 21 | type QueryParamsOption uint8 22 | 23 | // QueryParamsOption's 24 | const ( 25 | QueryParams QueryParamsOption = iota 26 | NoQueryParams 27 | ) 28 | 29 | var ( 30 | xmlHeaderBytes = []byte(xml.Header) 31 | ) 32 | 33 | func detectContentType(filename string) string { 34 | ext := strings.ToLower(filepath.Ext(filename)) 35 | if t := mime.TypeByExtension(ext); t != "" { 36 | return t 37 | } 38 | switch ext { 39 | case ".md": 40 | return TextMarkdown 41 | default: 42 | return ApplicationOctetStream 43 | } 44 | } 45 | 46 | // AcceptedLanguages returns an array of accepted languages denoted by 47 | // the Accept-Language header sent by the browser 48 | func AcceptedLanguages(r *http.Request) (languages []string) { 49 | accepted := r.Header.Get(AcceptedLanguage) 50 | if accepted == "" { 51 | return 52 | } 53 | options := strings.Split(accepted, ",") 54 | l := len(options) 55 | languages = make([]string, l) 56 | 57 | for i := 0; i < l; i++ { 58 | locale := strings.SplitN(options[i], ";", 2) 59 | languages[i] = strings.Trim(locale[0], " ") 60 | } 61 | return 62 | } 63 | 64 | // Attachment is a helper method for returning an attachment file 65 | // to be downloaded, if you with to open inline see function Inline 66 | func Attachment(w http.ResponseWriter, r io.Reader, filename string) (err error) { 67 | w.Header().Set(ContentDisposition, "attachment;filename="+filename) 68 | w.Header().Set(ContentType, detectContentType(filename)) 69 | w.WriteHeader(http.StatusOK) 70 | _, err = io.Copy(w, r) 71 | return 72 | } 73 | 74 | // Inline is a helper method for returning a file inline to 75 | // be rendered/opened by the browser 76 | func Inline(w http.ResponseWriter, r io.Reader, filename string) (err error) { 77 | w.Header().Set(ContentDisposition, "inline;filename="+filename) 78 | w.Header().Set(ContentType, detectContentType(filename)) 79 | w.WriteHeader(http.StatusOK) 80 | _, err = io.Copy(w, r) 81 | return 82 | } 83 | 84 | // ClientIP implements the best effort algorithm to return the real client IP, it parses 85 | // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. 86 | func ClientIP(r *http.Request) (clientIP string) { 87 | values := r.Header[XRealIP] 88 | if len(values) > 0 { 89 | clientIP = strings.TrimSpace(values[0]) 90 | if clientIP != "" { 91 | return 92 | } 93 | } 94 | if values = r.Header[XForwardedFor]; len(values) > 0 { 95 | clientIP = values[0] 96 | if index := strings.IndexByte(clientIP, ','); index >= 0 { 97 | clientIP = clientIP[0:index] 98 | } 99 | clientIP = strings.TrimSpace(clientIP) 100 | if clientIP != "" { 101 | return 102 | } 103 | } 104 | clientIP, _, _ = net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)) 105 | return 106 | } 107 | 108 | // JSONStream uses json.Encoder to stream the JSON response body. 109 | // 110 | // This differs from the JSON helper which unmarshalls into memory first allowing the capture of JSON encoding errors. 111 | func JSONStream(w http.ResponseWriter, status int, i interface{}) error { 112 | w.Header().Set(ContentType, ApplicationJSON) 113 | w.WriteHeader(status) 114 | return json.NewEncoder(w).Encode(i) 115 | } 116 | 117 | // JSON marshals provided interface + returns JSON + status code 118 | func JSON(w http.ResponseWriter, status int, i interface{}) error { 119 | b, err := json.Marshal(i) 120 | if err != nil { 121 | return err 122 | } 123 | w.Header().Set(ContentType, ApplicationJSON) 124 | w.WriteHeader(status) 125 | _, err = w.Write(b) 126 | return err 127 | } 128 | 129 | // JSONBytes returns provided JSON response with status code 130 | func JSONBytes(w http.ResponseWriter, status int, b []byte) (err error) { 131 | w.Header().Set(ContentType, ApplicationJSON) 132 | w.WriteHeader(status) 133 | _, err = w.Write(b) 134 | return err 135 | } 136 | 137 | // JSONP sends a JSONP response with status code and uses `callback` to construct 138 | // the JSONP payload. 139 | func JSONP(w http.ResponseWriter, status int, i interface{}, callback string) error { 140 | b, err := json.Marshal(i) 141 | if err != nil { 142 | return err 143 | } 144 | w.Header().Set(ContentType, ApplicationJSON) 145 | w.WriteHeader(status) 146 | if _, err = w.Write([]byte(callback + "(")); err == nil { 147 | if _, err = w.Write(b); err == nil { 148 | _, err = w.Write([]byte(");")) 149 | } 150 | } 151 | return err 152 | } 153 | 154 | // XML marshals provided interface + returns XML + status code 155 | func XML(w http.ResponseWriter, status int, i interface{}) error { 156 | b, err := xml.Marshal(i) 157 | if err != nil { 158 | return err 159 | } 160 | w.Header().Set(ContentType, ApplicationXML) 161 | w.WriteHeader(status) 162 | if _, err = w.Write(xmlHeaderBytes); err == nil { 163 | _, err = w.Write(b) 164 | } 165 | return err 166 | } 167 | 168 | // XMLBytes returns provided XML response with status code 169 | func XMLBytes(w http.ResponseWriter, status int, b []byte) (err error) { 170 | w.Header().Set(ContentType, ApplicationXML) 171 | w.WriteHeader(status) 172 | if _, err = w.Write(xmlHeaderBytes); err == nil { 173 | _, err = w.Write(b) 174 | } 175 | return 176 | } 177 | 178 | // DecodeForm parses the requests form data into the provided struct. 179 | // 180 | // The Content-Type and http method are not checked. 181 | // 182 | // NOTE: when QueryParamsOption=QueryParams the query params will be parsed and included eg. route /user?test=true 'test' 183 | // is added to parsed Form. 184 | func DecodeForm(r *http.Request, qp QueryParamsOption, v interface{}) (err error) { 185 | if err = r.ParseForm(); err == nil { 186 | switch qp { 187 | case QueryParams: 188 | err = DefaultFormDecoder.Decode(v, r.Form) 189 | case NoQueryParams: 190 | err = DefaultFormDecoder.Decode(v, r.PostForm) 191 | } 192 | } 193 | return 194 | } 195 | 196 | // DecodeMultipartForm parses the requests form data into the provided struct. 197 | // 198 | // The Content-Type and http method are not checked. 199 | // 200 | // NOTE: when includeQueryParams=true query params will be parsed and included eg. route /user?test=true 'test' 201 | // is added to parsed MultipartForm. 202 | func DecodeMultipartForm(r *http.Request, qp QueryParamsOption, maxMemory int64, v interface{}) (err error) { 203 | if err = r.ParseMultipartForm(maxMemory); err == nil { 204 | switch qp { 205 | case QueryParams: 206 | err = DefaultFormDecoder.Decode(v, r.Form) 207 | case NoQueryParams: 208 | err = DefaultFormDecoder.Decode(v, r.MultipartForm.Value) 209 | } 210 | } 211 | return 212 | } 213 | 214 | // DecodeJSON decodes the request body into the provided struct and limits the request size via 215 | // an ioext.LimitReader using the maxBytes param. 216 | // 217 | // The Content-Type e.g. "application/json" and http method are not checked. 218 | // 219 | // NOTE: when includeQueryParams=true query params will be parsed and included eg. route /user?test=true 'test' 220 | // is added to parsed JSON and replaces any values that may have been present 221 | func DecodeJSON(r *http.Request, qp QueryParamsOption, maxMemory int64, v interface{}) (err error) { 222 | var values url.Values 223 | if qp == QueryParams { 224 | values = r.URL.Query() 225 | } 226 | return decodeJSON(r.Header, r.Body, qp, values, maxMemory, v) 227 | } 228 | 229 | func decodeJSON(headers http.Header, body io.Reader, qp QueryParamsOption, values url.Values, maxMemory int64, v interface{}) (err error) { 230 | if encoding := headers.Get(ContentEncoding); encoding == Gzip { 231 | var gzr *gzip.Reader 232 | gzr, err = gzip.NewReader(body) 233 | if err != nil { 234 | return 235 | } 236 | defer func() { 237 | _ = gzr.Close() 238 | }() 239 | body = gzr 240 | } 241 | err = json.NewDecoder(ioext.LimitReader(body, maxMemory)).Decode(v) 242 | if qp == QueryParams && err == nil { 243 | err = decodeQueryParams(values, v) 244 | } 245 | return 246 | } 247 | 248 | // DecodeXML decodes the request body into the provided struct and limits the request size via 249 | // an ioext.LimitReader using the maxBytes param. 250 | // 251 | // The Content-Type e.g. "application/xml" and http method are not checked. 252 | // 253 | // NOTE: when includeQueryParams=true query params will be parsed and included eg. route /user?test=true 'test' 254 | // is added to parsed XML and replaces any values that may have been present 255 | func DecodeXML(r *http.Request, qp QueryParamsOption, maxMemory int64, v interface{}) (err error) { 256 | var values url.Values 257 | if qp == QueryParams { 258 | values = r.URL.Query() 259 | } 260 | return decodeXML(r.Header, r.Body, qp, values, maxMemory, v) 261 | } 262 | 263 | func decodeXML(headers http.Header, body io.Reader, qp QueryParamsOption, values url.Values, maxMemory int64, v interface{}) (err error) { 264 | if encoding := headers.Get(ContentEncoding); encoding == Gzip { 265 | var gzr *gzip.Reader 266 | gzr, err = gzip.NewReader(body) 267 | if err != nil { 268 | return 269 | } 270 | defer func() { 271 | _ = gzr.Close() 272 | }() 273 | body = gzr 274 | } 275 | err = xml.NewDecoder(ioext.LimitReader(body, maxMemory)).Decode(v) 276 | if qp == QueryParams && err == nil { 277 | err = decodeQueryParams(values, v) 278 | } 279 | return 280 | } 281 | 282 | // DecodeQueryParams takes the URL Query params flag. 283 | func DecodeQueryParams(r *http.Request, v interface{}) (err error) { 284 | return decodeQueryParams(r.URL.Query(), v) 285 | } 286 | 287 | func decodeQueryParams(values url.Values, v interface{}) (err error) { 288 | err = DefaultFormDecoder.Decode(v, values) 289 | return 290 | } 291 | 292 | const ( 293 | nakedApplicationJSON string = "application/json" 294 | nakedApplicationXML string = "application/xml" 295 | ) 296 | 297 | // Decode takes the request and attempts to discover its content type via 298 | // the http headers and then decode the request body into the provided struct. 299 | // Example if header was "application/json" would decode using 300 | // json.NewDecoder(ioext.LimitReader(r.Body, maxBytes)).Decode(v). 301 | // 302 | // This default to parsing query params if includeQueryParams=true and no other content type matches. 303 | // 304 | // NOTE: when includeQueryParams=true query params will be parsed and included eg. route /user?test=true 'test' 305 | // is added to parsed XML and replaces any values that may have been present 306 | func Decode(r *http.Request, qp QueryParamsOption, maxMemory int64, v interface{}) (err error) { 307 | typ := r.Header.Get(ContentType) 308 | if idx := strings.Index(typ, ";"); idx != -1 { 309 | typ = typ[:idx] 310 | } 311 | switch typ { 312 | case nakedApplicationJSON: 313 | err = DecodeJSON(r, qp, maxMemory, v) 314 | case nakedApplicationXML: 315 | err = DecodeXML(r, qp, maxMemory, v) 316 | case ApplicationForm: 317 | err = DecodeForm(r, qp, v) 318 | case MultipartForm: 319 | err = DecodeMultipartForm(r, qp, maxMemory, v) 320 | default: 321 | if qp == QueryParams { 322 | err = DecodeQueryParams(r, v) 323 | } 324 | } 325 | return 326 | } 327 | 328 | // DecodeResponseAny takes the response and attempts to discover its content type via 329 | // the http headers and then decode the request body into the provided type. 330 | // 331 | // Example if header was "application/json" would decode using 332 | // json.NewDecoder(ioext.LimitReader(r.Body, maxBytes)).Decode(v). 333 | func DecodeResponseAny(r *http.Response, maxMemory bytesext.Bytes, v interface{}) (err error) { 334 | typ := r.Header.Get(ContentType) 335 | if idx := strings.Index(typ, ";"); idx != -1 { 336 | typ = typ[:idx] 337 | } 338 | switch typ { 339 | case nakedApplicationJSON: 340 | err = decodeJSON(r.Header, r.Body, NoQueryParams, nil, maxMemory, v) 341 | case nakedApplicationXML: 342 | err = decodeXML(r.Header, r.Body, NoQueryParams, nil, maxMemory, v) 343 | default: 344 | err = errors.New("unsupported content type") 345 | } 346 | return 347 | } 348 | -------------------------------------------------------------------------------- /container/list/doubly_linked_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package listext 5 | 6 | import ( 7 | "container/list" 8 | . "github.com/go-playground/assert/v2" 9 | "testing" 10 | ) 11 | 12 | func TestLinkedListInserts(t *testing.T) { 13 | l := NewDoublyLinked[int]() 14 | Equal(t, l.IsEmpty(), true) 15 | Equal(t, l.Len(), 0) 16 | 17 | node1 := l.PushFront(1) 18 | node2 := l.PushFront(2) 19 | node3 := l.PushFront(3) 20 | 21 | l.Remove(node2) 22 | l.InsertAtFront(node2) 23 | Equal(t, l.Front().Value, node2.Value) 24 | 25 | l.Remove(node2) 26 | l.InsertAtBack(node2) 27 | Equal(t, l.Back().Value, node2.Value) 28 | 29 | l.Remove(node2) 30 | l.InsertBefore(node3, node2) 31 | Equal(t, l.Front().Value, node2.Value) 32 | 33 | l.Remove(node2) 34 | l.InsertAfter(node1, node2) 35 | Equal(t, l.Back().Value, node2.Value) 36 | } 37 | 38 | func TestSingleEntryPopBack(t *testing.T) { 39 | 40 | l := NewDoublyLinked[int]() 41 | Equal(t, l.IsEmpty(), true) 42 | Equal(t, l.Len(), 0) 43 | 44 | // push some data and then re-check 45 | zeroNode := l.PushFront(0) 46 | Equal(t, zeroNode.Value, 0) 47 | Equal(t, l.IsEmpty(), false) 48 | Equal(t, l.Len(), 1) 49 | Equal(t, zeroNode.Prev(), nil) 50 | Equal(t, zeroNode.Next(), nil) 51 | 52 | // test popping where one node is both head and tail 53 | back := l.PopBack() 54 | Equal(t, back.Value, 0) 55 | Equal(t, back.Next(), nil) 56 | Equal(t, back.Prev(), nil) 57 | Equal(t, l.IsEmpty(), true) 58 | Equal(t, l.Len(), 0) 59 | 60 | front := l.PopFront() 61 | Equal(t, front, nil) 62 | } 63 | 64 | func TestSingleEntryPopFront(t *testing.T) { 65 | 66 | l := NewDoublyLinked[int]() 67 | Equal(t, l.IsEmpty(), true) 68 | Equal(t, l.Len(), 0) 69 | 70 | // push some data and then re-check 71 | zeroNode := l.PushFront(0) 72 | Equal(t, zeroNode.Value, 0) 73 | Equal(t, l.IsEmpty(), false) 74 | Equal(t, l.Len(), 1) 75 | Equal(t, zeroNode.Prev(), nil) 76 | Equal(t, zeroNode.Next(), nil) 77 | 78 | // test popping where one node is both head and tail 79 | front := l.PopFront() 80 | Equal(t, front.Value, 0) 81 | Equal(t, front.Next(), nil) 82 | Equal(t, front.Prev(), nil) 83 | Equal(t, l.IsEmpty(), true) 84 | Equal(t, l.Len(), 0) 85 | 86 | back := l.PopBack() 87 | Equal(t, back, nil) 88 | 89 | } 90 | 91 | func TestDoubleEntryPopBack(t *testing.T) { 92 | 93 | l := NewDoublyLinked[int]() 94 | Equal(t, l.IsEmpty(), true) 95 | Equal(t, l.Len(), 0) 96 | 97 | // push some data and then re-check 98 | zeroNode := l.PushFront(0) 99 | oneNode := l.PushFront(1) 100 | Equal(t, l.IsEmpty(), false) 101 | Equal(t, l.Len(), 2) 102 | Equal(t, zeroNode.Value, 0) 103 | Equal(t, oneNode.Value, 1) 104 | Equal(t, zeroNode.Prev().Value, 1) 105 | Equal(t, zeroNode.Next(), nil) 106 | Equal(t, oneNode.Prev(), nil) 107 | Equal(t, oneNode.Next().Value, 0) 108 | 109 | back := l.PopBack() 110 | Equal(t, l.IsEmpty(), false) 111 | Equal(t, l.Len(), 1) 112 | Equal(t, back.Value, 0) 113 | Equal(t, back.Next(), nil) 114 | Equal(t, back.Prev(), nil) 115 | Equal(t, l.Front().Value, 1) 116 | Equal(t, l.Back().Value, 1) 117 | 118 | back2 := l.PopBack() 119 | Equal(t, l.IsEmpty(), true) 120 | Equal(t, l.Len(), 0) 121 | Equal(t, back2.Value, 1) 122 | Equal(t, back2.Next(), nil) 123 | Equal(t, back2.Prev(), nil) 124 | Equal(t, l.Front(), nil) 125 | Equal(t, l.Back(), nil) 126 | } 127 | 128 | func TestTripleEntryPopBack(t *testing.T) { 129 | 130 | l := NewDoublyLinked[int]() 131 | Equal(t, l.IsEmpty(), true) 132 | Equal(t, l.Len(), 0) 133 | 134 | // push some data and then re-check 135 | zeroNode := l.PushFront(0) 136 | oneNode := l.PushFront(1) 137 | twoNode := l.PushFront(2) 138 | Equal(t, l.IsEmpty(), false) 139 | Equal(t, l.Len(), 3) 140 | Equal(t, zeroNode.Value, 0) 141 | Equal(t, oneNode.Value, 1) 142 | Equal(t, twoNode.Value, 2) 143 | Equal(t, zeroNode.Next(), nil) 144 | Equal(t, zeroNode.Prev().Value, 1) 145 | Equal(t, zeroNode.Prev().Prev().Value, 2) 146 | Equal(t, zeroNode.Prev().Prev().Prev(), nil) 147 | Equal(t, oneNode.Next().Value, 0) 148 | Equal(t, oneNode.Next().Next(), nil) 149 | Equal(t, oneNode.Prev().Value, 2) 150 | Equal(t, oneNode.Prev().Prev(), nil) 151 | Equal(t, twoNode.Prev(), nil) 152 | Equal(t, twoNode.Next().Value, 1) 153 | Equal(t, twoNode.Next().Next().Value, 0) 154 | Equal(t, twoNode.Next().Next().Next(), nil) 155 | 156 | // remove front 157 | l.Remove(twoNode) 158 | 159 | // remove back 160 | l.Remove(zeroNode) 161 | } 162 | 163 | func TestLinkedListPushFront(t *testing.T) { 164 | 165 | l := NewDoublyLinked[int]() 166 | Equal(t, l.IsEmpty(), true) 167 | Equal(t, l.Len(), 0) 168 | 169 | // push some data and then re-check 170 | zeroNode := l.PushFront(0) 171 | oneNode := l.PushFront(1) 172 | twoNode := l.PushFront(2) 173 | Equal(t, l.IsEmpty(), false) 174 | Equal(t, l.Len(), 3) 175 | 176 | // test next logic 177 | Equal(t, zeroNode.Value, 0) 178 | Equal(t, zeroNode.Next(), nil) 179 | Equal(t, zeroNode.Prev().Value, 1) 180 | Equal(t, zeroNode.Prev().Prev().Value, 2) 181 | Equal(t, zeroNode.Prev().Prev().Prev(), nil) 182 | Equal(t, oneNode.Value, 1) 183 | Equal(t, oneNode.Next().Value, 0) 184 | Equal(t, oneNode.Next().Next(), nil) 185 | Equal(t, oneNode.Prev().Value, 2) 186 | Equal(t, oneNode.Prev().Prev(), nil) 187 | Equal(t, twoNode.Value, 2) 188 | Equal(t, twoNode.Prev(), nil) 189 | Equal(t, twoNode.Next().Value, 1) 190 | Equal(t, twoNode.Next().Next().Value, 0) 191 | Equal(t, twoNode.Next().Next().Next(), nil) 192 | 193 | // remove middle node and test again 194 | l.Remove(oneNode) 195 | Equal(t, oneNode.Value, 1) 196 | Equal(t, oneNode.Prev(), nil) 197 | Equal(t, oneNode.Next(), nil) 198 | 199 | // move to front 200 | l.MoveToFront(zeroNode) 201 | Equal(t, l.Front().Value, 0) 202 | Equal(t, l.Back().Value, 2) 203 | 204 | // move to back 205 | l.MoveToBack(zeroNode) 206 | Equal(t, l.Front().Value, 2) 207 | Equal(t, l.Back().Value, 0) 208 | 209 | // test clearing 210 | l.Clear() 211 | Equal(t, l.IsEmpty(), true) 212 | Equal(t, l.Len(), 0) 213 | } 214 | 215 | func TestLinkedListPushBack(t *testing.T) { 216 | 217 | l := NewDoublyLinked[int]() 218 | Equal(t, l.IsEmpty(), true) 219 | Equal(t, l.Len(), 0) 220 | 221 | // push some data and then re-check 222 | zeroNode := l.PushBack(0) 223 | oneNode := l.PushBack(1) 224 | twoNode := l.PushBack(2) 225 | Equal(t, l.IsEmpty(), false) 226 | Equal(t, l.Len(), 3) 227 | 228 | // test next logic 229 | Equal(t, zeroNode.Value, 0) 230 | Equal(t, zeroNode.Next().Value, 1) 231 | Equal(t, zeroNode.Next().Next().Value, 2) 232 | Equal(t, zeroNode.Next().Next().Next(), nil) 233 | Equal(t, zeroNode.Prev(), nil) 234 | Equal(t, oneNode.Value, 1) 235 | Equal(t, oneNode.Next().Value, 2) 236 | Equal(t, oneNode.Next().Next(), nil) 237 | Equal(t, oneNode.Prev().Value, 0) 238 | Equal(t, oneNode.Prev().Prev(), nil) 239 | Equal(t, twoNode.Value, 2) 240 | Equal(t, twoNode.Prev().Value, 1) 241 | Equal(t, twoNode.Prev().Prev().Value, 0) 242 | Equal(t, twoNode.Prev().Prev().Prev(), nil) 243 | Equal(t, twoNode.Next(), nil) 244 | 245 | // remove middle node and test again 246 | l.Remove(oneNode) 247 | Equal(t, oneNode.Value, 1) 248 | Equal(t, oneNode.Prev(), nil) 249 | Equal(t, oneNode.Next(), nil) 250 | 251 | // move to front 252 | l.MoveToBack(zeroNode) 253 | Equal(t, l.Front().Value, 2) 254 | Equal(t, l.Back().Value, 0) 255 | 256 | // move to back 257 | l.MoveToFront(zeroNode) 258 | Equal(t, l.Front().Value, 0) 259 | Equal(t, l.Back().Value, 2) 260 | 261 | // test clearing 262 | l.Clear() 263 | Equal(t, l.IsEmpty(), true) 264 | Equal(t, l.Len(), 0) 265 | } 266 | 267 | func TestLinkedListMoving(t *testing.T) { 268 | 269 | l := NewDoublyLinked[int]() 270 | Equal(t, l.IsEmpty(), true) 271 | Equal(t, l.Len(), 0) 272 | 273 | // test pushing after with one node 274 | node1 := l.PushFront(0) 275 | node2 := l.PushAfter(node1, 1) 276 | Equal(t, l.IsEmpty(), false) 277 | Equal(t, l.Len(), 2) 278 | Equal(t, l.Front().Value, node1.Value) 279 | Equal(t, l.Back().Value, node2.Value) 280 | 281 | // test moving after with two nodes 282 | l.MoveAfter(node2, node1) 283 | Equal(t, l.IsEmpty(), false) 284 | Equal(t, l.Len(), 2) 285 | Equal(t, l.Front().Value, node2.Value) 286 | Equal(t, l.Back().Value, node1.Value) 287 | 288 | // test clearing 289 | l.Clear() 290 | Equal(t, l.IsEmpty(), true) 291 | Equal(t, l.Len(), 0) 292 | 293 | // test pushing before with one node 294 | node1 = l.PushFront(0) 295 | node2 = l.PushBefore(node1, 1) 296 | Equal(t, l.IsEmpty(), false) 297 | Equal(t, l.Len(), 2) 298 | Equal(t, l.Front().Value, node2.Value) 299 | Equal(t, l.Back().Value, node1.Value) 300 | 301 | // test moving before with two nodes 302 | l.MoveBefore(node2, node1) 303 | Equal(t, l.IsEmpty(), false) 304 | Equal(t, l.Len(), 2) 305 | Equal(t, l.Front().Value, node1.Value) 306 | Equal(t, l.Back().Value, node2.Value) 307 | 308 | // test clearing 309 | l.Clear() 310 | Equal(t, l.IsEmpty(), true) 311 | Equal(t, l.Len(), 0) 312 | 313 | // testing the same as above BUT with 3 nodes attached 314 | node1 = l.PushFront(0) 315 | node2 = l.PushAfter(node1, 1) 316 | node3 := l.PushAfter(node2, 2) 317 | Equal(t, l.IsEmpty(), false) 318 | Equal(t, l.Len(), 3) 319 | Equal(t, l.Front().Value, node1.Value) 320 | Equal(t, l.Front().Next().Value, node2.Value) 321 | Equal(t, l.Back().Value, node3.Value) 322 | Equal(t, l.Back().Prev().Value, node2.Value) 323 | 324 | l.MoveBefore(node2, node3) 325 | Equal(t, l.IsEmpty(), false) 326 | Equal(t, l.Len(), 3) 327 | Equal(t, l.Front().Value, node1.Value) 328 | Equal(t, l.Front().Next().Value, node3.Value) 329 | Equal(t, l.Back().Value, node2.Value) 330 | Equal(t, l.Back().Prev().Value, node3.Value) 331 | 332 | l.MoveAfter(node3, node1) 333 | Equal(t, l.IsEmpty(), false) 334 | Equal(t, l.Len(), 3) 335 | Equal(t, l.Front().Value, node3.Value) 336 | Equal(t, l.Front().Next().Value, node1.Value) 337 | Equal(t, l.Back().Value, node2.Value) 338 | Equal(t, l.Back().Prev().Value, node1.Value) 339 | 340 | // test clearing 341 | l.Clear() 342 | Equal(t, l.IsEmpty(), true) 343 | Equal(t, l.Len(), 0) 344 | 345 | // testing the same as above BUT with 4 nodes attached moving the middle nodes back and forth 346 | node1 = l.PushFront(0) 347 | node2 = l.PushAfter(node1, 1) 348 | node3 = l.PushAfter(node2, 2) 349 | node4 := l.PushAfter(node3, 3) 350 | Equal(t, l.IsEmpty(), false) 351 | Equal(t, l.Len(), 4) 352 | Equal(t, l.Front().Value, node1.Value) 353 | Equal(t, l.Front().Next().Value, node2.Value) 354 | Equal(t, l.Front().Next().Next().Value, node3.Value) 355 | Equal(t, l.Front().Next().Next().Next().Value, node4.Value) 356 | Equal(t, l.Front().Next().Next().Next().Next(), nil) 357 | Equal(t, l.Back().Value, node4.Value) 358 | Equal(t, l.Back().Prev().Value, node3.Value) 359 | Equal(t, l.Back().Prev().Prev().Value, node2.Value) 360 | Equal(t, l.Back().Prev().Prev().Prev().Value, node1.Value) 361 | Equal(t, l.Back().Prev().Prev().Prev().Prev(), nil) 362 | 363 | l.MoveAfter(node3, node2) 364 | Equal(t, l.IsEmpty(), false) 365 | Equal(t, l.Len(), 4) 366 | Equal(t, l.Front().Value, node1.Value) 367 | Equal(t, l.Front().Next().Value, node3.Value) 368 | Equal(t, l.Front().Next().Next().Value, node2.Value) 369 | Equal(t, l.Front().Next().Next().Next().Value, node4.Value) 370 | Equal(t, l.Front().Next().Next().Next().Next(), nil) 371 | Equal(t, l.Back().Value, node4.Value) 372 | Equal(t, l.Back().Prev().Value, node2.Value) 373 | Equal(t, l.Back().Prev().Prev().Value, node3.Value) 374 | Equal(t, l.Back().Prev().Prev().Prev().Value, node1.Value) 375 | Equal(t, l.Back().Prev().Prev().Prev().Prev(), nil) 376 | 377 | l.MoveAfter(node2, node3) 378 | Equal(t, l.IsEmpty(), false) 379 | Equal(t, l.Len(), 4) 380 | Equal(t, l.Front().Value, node1.Value) 381 | Equal(t, l.Front().Next().Value, node2.Value) 382 | Equal(t, l.Front().Next().Next().Value, node3.Value) 383 | Equal(t, l.Front().Next().Next().Next().Value, node4.Value) 384 | Equal(t, l.Front().Next().Next().Next().Next(), nil) 385 | Equal(t, l.Back().Value, node4.Value) 386 | Equal(t, l.Back().Prev().Value, node3.Value) 387 | Equal(t, l.Back().Prev().Prev().Value, node2.Value) 388 | Equal(t, l.Back().Prev().Prev().Prev().Value, node1.Value) 389 | Equal(t, l.Back().Prev().Prev().Prev().Prev(), nil) 390 | 391 | // test clearing 392 | l.Clear() 393 | Equal(t, l.IsEmpty(), true) 394 | Equal(t, l.Len(), 0) 395 | } 396 | 397 | func TestLinkedListRemoveSingleEntry(t *testing.T) { 398 | 399 | l := NewDoublyLinked[int]() 400 | Equal(t, l.IsEmpty(), true) 401 | Equal(t, l.Len(), 0) 402 | 403 | // test pushing after with one node 404 | node := l.PushFront(0) 405 | Equal(t, l.IsEmpty(), false) 406 | Equal(t, l.Len(), 1) 407 | Equal(t, l.Front().Value, node.Value) 408 | Equal(t, l.Back().Value, node.Value) 409 | 410 | l.Remove(node) 411 | Equal(t, l.IsEmpty(), true) 412 | Equal(t, l.Len(), 0) 413 | } 414 | 415 | func BenchmarkDoublyLinkedList(b *testing.B) { 416 | for i := 0; i < b.N; i++ { 417 | l := NewDoublyLinked[int]() 418 | node := l.PushBack(0) 419 | l.Remove(node) 420 | _ = node.Value 421 | } 422 | } 423 | 424 | func BenchmarkDoublyLinkedList_STD(b *testing.B) { 425 | for i := 0; i < b.N; i++ { 426 | l := list.New() 427 | node := l.PushBack(0) 428 | _ = l.Remove(node) 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /net/http/helpers_test.go: -------------------------------------------------------------------------------- 1 | package httpext 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "encoding/json" 7 | "encoding/xml" 8 | "mime/multipart" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "os" 13 | "strings" 14 | "testing" 15 | 16 | . "github.com/go-playground/assert/v2" 17 | ) 18 | 19 | func TestAcceptedLanguages(t *testing.T) { 20 | req, _ := http.NewRequest("POST", "/", nil) 21 | req.Header.Set(AcceptedLanguage, "da, en-GB;q=0.8, en;q=0.7") 22 | 23 | languages := AcceptedLanguages(req) 24 | 25 | Equal(t, languages[0], "da") 26 | Equal(t, languages[1], "en-GB") 27 | Equal(t, languages[2], "en") 28 | 29 | req.Header.Del(AcceptedLanguage) 30 | 31 | languages = AcceptedLanguages(req) 32 | Equal(t, len(languages), 0) 33 | 34 | req.Header.Set(AcceptedLanguage, "") 35 | languages = AcceptedLanguages(req) 36 | Equal(t, len(languages), 0) 37 | } 38 | 39 | func TestAttachment(t *testing.T) { 40 | mux := http.NewServeMux() 41 | mux.HandleFunc("/dl", func(w http.ResponseWriter, r *http.Request) { 42 | f, _ := os.Open("../../README.md") 43 | if err := Attachment(w, f, "README.md"); err != nil { 44 | panic(err) 45 | } 46 | }) 47 | mux.HandleFunc("/dl-unknown-type", func(w http.ResponseWriter, r *http.Request) { 48 | f, _ := os.Open("../../README.md") 49 | if err := Attachment(w, f, "readme"); err != nil { 50 | panic(err) 51 | } 52 | }) 53 | mux.HandleFunc("/dl-fake-png", func(w http.ResponseWriter, r *http.Request) { 54 | f, _ := os.Open("../../README.md") 55 | if err := Attachment(w, f, "logo.png"); err != nil { 56 | panic(err) 57 | } 58 | }) 59 | 60 | tests := []struct { 61 | name string 62 | code int 63 | disposition string 64 | typ string 65 | url string 66 | }{ 67 | { 68 | code: http.StatusOK, 69 | disposition: "attachment;filename=README.md", 70 | typ: TextMarkdown, 71 | url: "/dl", 72 | }, 73 | { 74 | code: http.StatusOK, 75 | disposition: "attachment;filename=readme", 76 | typ: ApplicationOctetStream, 77 | url: "/dl-unknown-type", 78 | }, 79 | { 80 | code: http.StatusOK, 81 | disposition: "attachment;filename=logo.png", 82 | typ: ImagePNG, 83 | url: "/dl-fake-png", 84 | }, 85 | } 86 | 87 | for _, tt := range tests { 88 | t.Run(tt.name, func(t *testing.T) { 89 | req, err := http.NewRequest(http.MethodGet, tt.url, nil) 90 | Equal(t, err, nil) 91 | 92 | w := httptest.NewRecorder() 93 | mux.ServeHTTP(w, req) 94 | 95 | if tt.code != w.Code { 96 | t.Errorf("Status Code = %d, want %d", w.Code, tt.code) 97 | } 98 | if tt.disposition != w.Header().Get(ContentDisposition) { 99 | t.Errorf("Content Disaposition = %s, want %s", w.Header().Get(ContentDisposition), tt.disposition) 100 | } 101 | if tt.typ != w.Header().Get(ContentType) { 102 | t.Errorf("Content Type = %s, want %s", w.Header().Get(ContentType), tt.typ) 103 | } 104 | }) 105 | } 106 | } 107 | 108 | func TestInline(t *testing.T) { 109 | mux := http.NewServeMux() 110 | mux.HandleFunc("/dl-inline", func(w http.ResponseWriter, r *http.Request) { 111 | f, _ := os.Open("../../README.md") 112 | if err := Inline(w, f, "README.md"); err != nil { 113 | panic(err) 114 | } 115 | }) 116 | mux.HandleFunc("/dl-unknown-type-inline", func(w http.ResponseWriter, r *http.Request) { 117 | f, _ := os.Open("../../README.md") 118 | if err := Inline(w, f, "readme"); err != nil { 119 | panic(err) 120 | } 121 | }) 122 | 123 | tests := []struct { 124 | name string 125 | code int 126 | disposition string 127 | typ string 128 | url string 129 | }{ 130 | { 131 | code: http.StatusOK, 132 | disposition: "inline;filename=README.md", 133 | typ: TextMarkdown, 134 | url: "/dl-inline", 135 | }, 136 | { 137 | code: http.StatusOK, 138 | disposition: "inline;filename=readme", 139 | typ: ApplicationOctetStream, 140 | url: "/dl-unknown-type-inline", 141 | }, 142 | } 143 | 144 | for _, tt := range tests { 145 | t.Run(tt.name, func(t *testing.T) { 146 | req, err := http.NewRequest(http.MethodGet, tt.url, nil) 147 | Equal(t, err, nil) 148 | 149 | w := httptest.NewRecorder() 150 | mux.ServeHTTP(w, req) 151 | 152 | if tt.code != w.Code { 153 | t.Errorf("Status Code = %d, want %d", w.Code, tt.code) 154 | } 155 | if tt.disposition != w.Header().Get(ContentDisposition) { 156 | t.Errorf("Content Disaposition = %s, want %s", w.Header().Get(ContentDisposition), tt.disposition) 157 | } 158 | if tt.typ != w.Header().Get(ContentType) { 159 | t.Errorf("Content Type = %s, want %s", w.Header().Get(ContentType), tt.typ) 160 | } 161 | }) 162 | } 163 | } 164 | 165 | func TestClientIP(t *testing.T) { 166 | req, _ := http.NewRequest("POST", "/", nil) 167 | req.Header.Set("X-Real-IP", " 10.10.10.10 ") 168 | req.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30") 169 | req.RemoteAddr = " 40.40.40.40:42123 " 170 | 171 | Equal(t, ClientIP(req), "10.10.10.10") 172 | 173 | req.Header.Del("X-Real-IP") 174 | Equal(t, ClientIP(req), "20.20.20.20") 175 | 176 | req.Header.Set("X-Forwarded-For", "30.30.30.30 ") 177 | Equal(t, ClientIP(req), "30.30.30.30") 178 | 179 | req.Header.Del("X-Forwarded-For") 180 | Equal(t, ClientIP(req), "40.40.40.40") 181 | } 182 | 183 | func TestJSON(t *testing.T) { 184 | w := httptest.NewRecorder() 185 | type test struct { 186 | Field string `json:"field"` 187 | } 188 | tst := test{Field: "myfield"} 189 | b, err := json.Marshal(tst) 190 | Equal(t, err, nil) 191 | 192 | err = JSON(w, http.StatusOK, tst) 193 | Equal(t, err, nil) 194 | Equal(t, w.Header().Get(ContentType), ApplicationJSON) 195 | Equal(t, w.Body.Bytes(), b) 196 | 197 | err = JSON(w, http.StatusOK, func() {}) 198 | NotEqual(t, err, nil) 199 | } 200 | 201 | func TestJSONBytes(t *testing.T) { 202 | w := httptest.NewRecorder() 203 | type test struct { 204 | Field string `json:"field"` 205 | } 206 | tst := test{Field: "myfield"} 207 | b, err := json.Marshal(tst) 208 | Equal(t, err, nil) 209 | 210 | err = JSONBytes(w, http.StatusOK, b) 211 | Equal(t, err, nil) 212 | Equal(t, w.Header().Get(ContentType), ApplicationJSON) 213 | Equal(t, w.Body.Bytes(), b) 214 | } 215 | 216 | func TestJSONP(t *testing.T) { 217 | callbackFunc := "CallbackFunc" 218 | w := httptest.NewRecorder() 219 | type test struct { 220 | Field string `json:"field"` 221 | } 222 | tst := test{Field: "myfield"} 223 | err := JSONP(w, http.StatusOK, tst, callbackFunc) 224 | Equal(t, err, nil) 225 | Equal(t, w.Header().Get(ContentType), ApplicationJSON) 226 | 227 | err = JSON(w, http.StatusOK, func() {}) 228 | NotEqual(t, err, nil) 229 | } 230 | 231 | func TestXML(t *testing.T) { 232 | w := httptest.NewRecorder() 233 | type zombie struct { 234 | ID int `json:"id" xml:"id"` 235 | Name string `json:"name" xml:"name"` 236 | } 237 | tst := zombie{1, "Patient Zero"} 238 | xmlData := `1Patient Zero` 239 | err := XML(w, http.StatusOK, tst) 240 | Equal(t, err, nil) 241 | Equal(t, w.Header().Get(ContentType), ApplicationXML) 242 | Equal(t, w.Body.Bytes(), []byte(xml.Header+xmlData)) 243 | 244 | err = JSON(w, http.StatusOK, func() {}) 245 | NotEqual(t, err, nil) 246 | } 247 | 248 | func TestXMLBytes(t *testing.T) { 249 | xmlData := `1Patient Zero` 250 | w := httptest.NewRecorder() 251 | err := XMLBytes(w, http.StatusOK, []byte(xmlData)) 252 | Equal(t, err, nil) 253 | Equal(t, w.Header().Get(ContentType), ApplicationXML) 254 | Equal(t, w.Body.Bytes(), []byte(xml.Header+xmlData)) 255 | } 256 | 257 | func TestDecode(t *testing.T) { 258 | type TestStruct struct { 259 | ID int `form:"id"` 260 | Posted string 261 | MultiPartPosted string 262 | } 263 | 264 | test := new(TestStruct) 265 | 266 | mux := http.NewServeMux() 267 | mux.HandleFunc("/decode-noquery", func(w http.ResponseWriter, r *http.Request) { 268 | err := Decode(r, NoQueryParams, 16<<10, test) 269 | Equal(t, err, nil) 270 | }) 271 | mux.HandleFunc("/decode-query", func(w http.ResponseWriter, r *http.Request) { 272 | err := Decode(r, QueryParams, 16<<10, test) 273 | Equal(t, err, nil) 274 | }) 275 | 276 | // test query params 277 | r, _ := http.NewRequest(http.MethodGet, "/decode-query?id=5", nil) 278 | w := httptest.NewRecorder() 279 | mux.ServeHTTP(w, r) 280 | Equal(t, w.Code, http.StatusOK) 281 | Equal(t, test.ID, 5) 282 | Equal(t, test.Posted, "") 283 | Equal(t, test.MultiPartPosted, "") 284 | 285 | // test Form decode 286 | form := url.Values{} 287 | form.Add("Posted", "values") 288 | 289 | test = new(TestStruct) 290 | r, _ = http.NewRequest(http.MethodPost, "/decode-query?id=13", strings.NewReader(form.Encode())) 291 | r.Header.Set(ContentType, ApplicationForm) 292 | w = httptest.NewRecorder() 293 | mux.ServeHTTP(w, r) 294 | Equal(t, w.Code, http.StatusOK) 295 | Equal(t, test.ID, 13) 296 | Equal(t, test.Posted, "values") 297 | Equal(t, test.MultiPartPosted, "") 298 | 299 | test = new(TestStruct) 300 | r, _ = http.NewRequest(http.MethodPost, "/decode-noquery?id=14", strings.NewReader(form.Encode())) 301 | r.Header.Set(ContentType, ApplicationForm) 302 | w = httptest.NewRecorder() 303 | mux.ServeHTTP(w, r) 304 | Equal(t, w.Code, http.StatusOK) 305 | Equal(t, test.ID, 0) 306 | Equal(t, test.Posted, "values") 307 | Equal(t, test.MultiPartPosted, "") 308 | 309 | // test MultipartForm 310 | body := &bytes.Buffer{} 311 | writer := multipart.NewWriter(body) 312 | 313 | err := writer.WriteField("MultiPartPosted", "values") 314 | Equal(t, err, nil) 315 | 316 | // Don't forget to close the multipart writer. 317 | // If you don't close it, your request will be missing the terminating boundary. 318 | err = writer.Close() 319 | Equal(t, err, nil) 320 | 321 | test = new(TestStruct) 322 | r, _ = http.NewRequest(http.MethodPost, "/decode-query?id=12", body) 323 | r.Header.Set(ContentType, writer.FormDataContentType()) 324 | w = httptest.NewRecorder() 325 | mux.ServeHTTP(w, r) 326 | Equal(t, w.Code, http.StatusOK) 327 | Equal(t, test.ID, 12) 328 | Equal(t, test.Posted, "") 329 | Equal(t, test.MultiPartPosted, "values") 330 | 331 | body = &bytes.Buffer{} 332 | writer = multipart.NewWriter(body) 333 | 334 | err = writer.WriteField("MultiPartPosted", "values") 335 | Equal(t, err, nil) 336 | 337 | // Don't forget to close the multipart writer. 338 | // If you don't close it, your request will be missing the terminating boundary. 339 | err = writer.Close() 340 | Equal(t, err, nil) 341 | 342 | test = new(TestStruct) 343 | r, _ = http.NewRequest(http.MethodPost, "/decode-noquery?id=13", body) 344 | r.Header.Set(ContentType, writer.FormDataContentType()) 345 | w = httptest.NewRecorder() 346 | mux.ServeHTTP(w, r) 347 | Equal(t, w.Code, http.StatusOK) 348 | Equal(t, test.ID, 0) 349 | Equal(t, test.Posted, "") 350 | Equal(t, test.MultiPartPosted, "values") 351 | 352 | // test JSON 353 | jsonBody := `{"ID":13,"Posted":"values","MultiPartPosted":"values"}` 354 | test = new(TestStruct) 355 | r, _ = http.NewRequest(http.MethodPost, "/decode-query?id=13", strings.NewReader(jsonBody)) 356 | r.Header.Set(ContentType, ApplicationJSON) 357 | w = httptest.NewRecorder() 358 | mux.ServeHTTP(w, r) 359 | Equal(t, w.Code, http.StatusOK) 360 | Equal(t, test.ID, 13) 361 | Equal(t, test.Posted, "values") 362 | Equal(t, test.MultiPartPosted, "values") 363 | 364 | var buff bytes.Buffer 365 | gzw := gzip.NewWriter(&buff) 366 | defer func() { 367 | _ = gzw.Close() 368 | }() 369 | _, err = gzw.Write([]byte(jsonBody)) 370 | Equal(t, err, nil) 371 | 372 | err = gzw.Close() 373 | Equal(t, err, nil) 374 | 375 | test = new(TestStruct) 376 | r, _ = http.NewRequest(http.MethodPost, "/decode-query?id=14", &buff) 377 | r.Header.Set(ContentType, ApplicationJSON) 378 | r.Header.Set(ContentEncoding, Gzip) 379 | w = httptest.NewRecorder() 380 | mux.ServeHTTP(w, r) 381 | Equal(t, w.Code, http.StatusOK) 382 | Equal(t, test.ID, 14) 383 | Equal(t, test.Posted, "values") 384 | Equal(t, test.MultiPartPosted, "values") 385 | 386 | test = new(TestStruct) 387 | r, _ = http.NewRequest(http.MethodPost, "/decode-noquery?id=14", strings.NewReader(jsonBody)) 388 | r.Header.Set(ContentType, ApplicationJSON) 389 | w = httptest.NewRecorder() 390 | mux.ServeHTTP(w, r) 391 | Equal(t, w.Code, http.StatusOK) 392 | Equal(t, test.ID, 13) 393 | Equal(t, test.Posted, "values") 394 | Equal(t, test.MultiPartPosted, "values") 395 | 396 | // test XML 397 | xmlBody := `13valuesvalues` 398 | test = new(TestStruct) 399 | r, _ = http.NewRequest(http.MethodPost, "/decode-noquery?id=14", strings.NewReader(xmlBody)) 400 | r.Header.Set(ContentType, ApplicationXML) 401 | w = httptest.NewRecorder() 402 | mux.ServeHTTP(w, r) 403 | Equal(t, w.Code, http.StatusOK) 404 | Equal(t, test.ID, 13) 405 | Equal(t, test.Posted, "values") 406 | Equal(t, test.MultiPartPosted, "values") 407 | 408 | test = new(TestStruct) 409 | r, _ = http.NewRequest(http.MethodPost, "/decode-query?id=14", strings.NewReader(xmlBody)) 410 | r.Header.Set(ContentType, ApplicationXML) 411 | w = httptest.NewRecorder() 412 | mux.ServeHTTP(w, r) 413 | Equal(t, w.Code, http.StatusOK) 414 | Equal(t, test.ID, 14) 415 | Equal(t, test.Posted, "values") 416 | Equal(t, test.MultiPartPosted, "values") 417 | 418 | buff.Reset() 419 | gzw = gzip.NewWriter(&buff) 420 | defer func() { 421 | _ = gzw.Close() 422 | }() 423 | _, err = gzw.Write([]byte(xmlBody)) 424 | Equal(t, err, nil) 425 | 426 | err = gzw.Close() 427 | Equal(t, err, nil) 428 | 429 | test = new(TestStruct) 430 | r, _ = http.NewRequest(http.MethodPost, "/decode-noquery?id=14", &buff) 431 | r.Header.Set(ContentType, ApplicationXML) 432 | r.Header.Set(ContentEncoding, Gzip) 433 | w = httptest.NewRecorder() 434 | mux.ServeHTTP(w, r) 435 | Equal(t, w.Code, http.StatusOK) 436 | Equal(t, test.ID, 13) 437 | Equal(t, test.Posted, "values") 438 | Equal(t, test.MultiPartPosted, "values") 439 | } 440 | -------------------------------------------------------------------------------- /values/option/option_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package optionext 5 | 6 | import ( 7 | "database/sql/driver" 8 | "encoding/json" 9 | "math" 10 | "reflect" 11 | "testing" 12 | "time" 13 | 14 | . "github.com/go-playground/assert/v2" 15 | ) 16 | 17 | type valueTest struct { 18 | } 19 | 20 | func (valueTest) Value() (driver.Value, error) { 21 | return "value", nil 22 | } 23 | 24 | type customStringType string 25 | 26 | type testStructType struct { 27 | Name string 28 | } 29 | 30 | func TestAndXXX(t *testing.T) { 31 | s := Some(1) 32 | Equal(t, Some(3), s.And(func(i int) int { return 3 })) 33 | Equal(t, Some(3), s.AndThen(func(i int) Option[int] { return Some(3) })) 34 | Equal(t, None[int](), s.AndThen(func(i int) Option[int] { return None[int]() })) 35 | 36 | n := None[int]() 37 | Equal(t, None[int](), n.And(func(i int) int { return 3 })) 38 | Equal(t, None[int](), n.AndThen(func(i int) Option[int] { return Some(3) })) 39 | Equal(t, None[int](), n.AndThen(func(i int) Option[int] { return None[int]() })) 40 | Equal(t, None[int](), s.AndThen(func(i int) Option[int] { return None[int]() })) 41 | } 42 | 43 | func TestUnwraps(t *testing.T) { 44 | none := None[int]() 45 | PanicMatches(t, func() { none.Unwrap() }, "Option.Unwrap: option is None") 46 | 47 | v := none.UnwrapOr(3) 48 | Equal(t, 3, v) 49 | 50 | v = none.UnwrapOrElse(func() int { return 2 }) 51 | Equal(t, 2, v) 52 | 53 | v = none.UnwrapOrDefault() 54 | Equal(t, 0, v) 55 | 56 | // now test with a pointer type. 57 | type myStruct struct { 58 | S string 59 | } 60 | 61 | sNone := None[*myStruct]() 62 | PanicMatches(t, func() { sNone.Unwrap() }, "Option.Unwrap: option is None") 63 | 64 | v2 := sNone.UnwrapOr(&myStruct{S: "blah"}) 65 | Equal(t, &myStruct{S: "blah"}, v2) 66 | 67 | v2 = sNone.UnwrapOrElse(func() *myStruct { return &myStruct{S: "blah 2"} }) 68 | Equal(t, &myStruct{S: "blah 2"}, v2) 69 | 70 | v2 = sNone.UnwrapOrDefault() 71 | Equal(t, nil, v2) 72 | } 73 | 74 | func TestSQLDriverValue(t *testing.T) { 75 | 76 | var v valueTest 77 | Equal(t, reflect.TypeOf(v).Implements(valuerType), true) 78 | 79 | // none 80 | nOpt := None[string]() 81 | nVal, err := nOpt.Value() 82 | Equal(t, err, nil) 83 | Equal(t, nVal, nil) 84 | 85 | // string + convert custom string type 86 | sOpt := Some("myString") 87 | sVal, err := sOpt.Value() 88 | Equal(t, err, nil) 89 | 90 | _, ok := sVal.(string) 91 | Equal(t, ok, true) 92 | Equal(t, sVal, "myString") 93 | 94 | sCustOpt := Some(customStringType("string")) 95 | sCustVal, err := sCustOpt.Value() 96 | Equal(t, err, nil) 97 | Equal(t, sCustVal, "string") 98 | 99 | _, ok = sCustVal.(string) 100 | Equal(t, ok, true) 101 | 102 | // bool 103 | bOpt := Some(true) 104 | bVal, err := bOpt.Value() 105 | Equal(t, err, nil) 106 | 107 | _, ok = bVal.(bool) 108 | Equal(t, ok, true) 109 | Equal(t, bVal, true) 110 | 111 | // int64 112 | iOpt := Some(int64(2)) 113 | iVal, err := iOpt.Value() 114 | Equal(t, err, nil) 115 | 116 | _, ok = iVal.(int64) 117 | Equal(t, ok, true) 118 | Equal(t, iVal, int64(2)) 119 | 120 | // float64 121 | fOpt := Some(1.1) 122 | fVal, err := fOpt.Value() 123 | Equal(t, err, nil) 124 | 125 | _, ok = fVal.(float64) 126 | Equal(t, ok, true) 127 | Equal(t, fVal, 1.1) 128 | 129 | // time.Time 130 | dt := time.Now().UTC() 131 | dtOpt := Some(dt) 132 | dtVal, err := dtOpt.Value() 133 | Equal(t, err, nil) 134 | 135 | _, ok = dtVal.(time.Time) 136 | Equal(t, ok, true) 137 | Equal(t, dtVal, dt) 138 | 139 | // Slice []byte 140 | b := []byte("myBytes") 141 | bytesOpt := Some(b) 142 | bytesVal, err := bytesOpt.Value() 143 | Equal(t, err, nil) 144 | 145 | _, ok = bytesVal.([]byte) 146 | Equal(t, ok, true) 147 | Equal(t, bytesVal, b) 148 | 149 | // Slice []uint8 150 | b2 := []uint8("myBytes") 151 | bytes2Opt := Some(b2) 152 | bytes2Val, err := bytes2Opt.Value() 153 | Equal(t, err, nil) 154 | 155 | _, ok = bytes2Val.([]byte) 156 | Equal(t, ok, true) 157 | Equal(t, bytes2Val, b2) 158 | 159 | // Array []byte 160 | a := []byte{'1', '2', '3'} 161 | arrayOpt := Some(a) 162 | arrayVal, err := arrayOpt.Value() 163 | Equal(t, err, nil) 164 | 165 | _, ok = arrayVal.([]byte) 166 | Equal(t, ok, true) 167 | Equal(t, arrayVal, a) 168 | 169 | // Slice []byte 170 | data := []testStructType{{Name: "test"}} 171 | b, err = json.Marshal(data) 172 | Equal(t, err, nil) 173 | 174 | dataOpt := Some(data) 175 | dataVal, err := dataOpt.Value() 176 | Equal(t, err, nil) 177 | 178 | _, ok = dataVal.([]byte) 179 | Equal(t, ok, true) 180 | Equal(t, dataVal, b) 181 | 182 | // Map 183 | data2 := map[string]int{"test": 1} 184 | b, err = json.Marshal(data2) 185 | Equal(t, err, nil) 186 | 187 | data2Opt := Some(data2) 188 | data2Val, err := data2Opt.Value() 189 | Equal(t, err, nil) 190 | 191 | _, ok = data2Val.([]byte) 192 | Equal(t, ok, true) 193 | Equal(t, data2Val, b) 194 | 195 | // Struct 196 | data3 := testStructType{Name: "test"} 197 | b, err = json.Marshal(data3) 198 | Equal(t, err, nil) 199 | 200 | data3Opt := Some(data3) 201 | data3Val, err := data3Opt.Value() 202 | Equal(t, err, nil) 203 | 204 | _, ok = data3Val.([]byte) 205 | Equal(t, ok, true) 206 | Equal(t, data3Val, b) 207 | } 208 | 209 | type customScanner struct { 210 | S string 211 | } 212 | 213 | func (c *customScanner) Scan(src interface{}) error { 214 | if src == nil { 215 | return nil 216 | } 217 | c.S = src.(string) 218 | return nil 219 | } 220 | 221 | func TestSQLScanner(t *testing.T) { 222 | value := int64(123) 223 | var optionI64 Option[int64] 224 | var optionI32 Option[int32] 225 | var optionI16 Option[int16] 226 | var optionI8 Option[int8] 227 | var optionI Option[int] 228 | var optionString Option[string] 229 | var optionBool Option[bool] 230 | var optionF32 Option[float32] 231 | var optionF64 Option[float64] 232 | var optionByte Option[byte] 233 | var optionTime Option[time.Time] 234 | var optionInterface Option[any] 235 | var optionArrBytes Option[[]byte] 236 | var optionRawMessage Option[json.RawMessage] 237 | var optionUint64 Option[uint64] 238 | var optionUint32 Option[uint32] 239 | var optionUint16 Option[uint16] 240 | var optionUint8 Option[uint8] 241 | var optionUint Option[uint] 242 | 243 | err := optionInterface.Scan(1) 244 | Equal(t, err, nil) 245 | Equal(t, optionInterface, Some(any(1))) 246 | 247 | err = optionInterface.Scan("blah") 248 | Equal(t, err, nil) 249 | Equal(t, optionInterface, Some(any("blah"))) 250 | 251 | err = optionUint64.Scan(uint64(200)) 252 | Equal(t, err, nil) 253 | Equal(t, optionUint64, Some(uint64(200))) 254 | 255 | err = optionUint32.Scan(uint32(200)) 256 | Equal(t, err, nil) 257 | Equal(t, optionUint32, Some(uint32(200))) 258 | 259 | err = optionUint16.Scan(uint16(200)) 260 | Equal(t, err, nil) 261 | Equal(t, optionUint16, Some(uint16(200))) 262 | 263 | err = optionUint8.Scan(uint8(200)) 264 | Equal(t, err, nil) 265 | Equal(t, optionUint8, Some(uint8(200))) 266 | 267 | err = optionUint.Scan(uint(200)) 268 | Equal(t, err, nil) 269 | Equal(t, optionUint, Some(uint(200))) 270 | 271 | err = optionUint64.Scan("200") 272 | Equal(t, err.Error(), "value string not convertable to uint64") 273 | 274 | err = optionI64.Scan(value) 275 | Equal(t, err, nil) 276 | Equal(t, optionI64, Some(value)) 277 | 278 | err = optionI32.Scan(value) 279 | Equal(t, err, nil) 280 | Equal(t, optionI32, Some(int32(value))) 281 | 282 | err = optionI16.Scan(value) 283 | Equal(t, err, nil) 284 | Equal(t, optionI16, Some(int16(value))) 285 | 286 | err = optionI8.Scan(math.MaxInt32) 287 | Equal(t, err.Error(), "value 2147483647 out of range for int8") 288 | Equal(t, optionI8, None[int8]()) 289 | 290 | err = optionI8.Scan(int8(3)) 291 | Equal(t, err, nil) 292 | Equal(t, optionI8, Some(int8(3))) 293 | 294 | err = optionI.Scan(3) 295 | Equal(t, err, nil) 296 | Equal(t, optionI, Some(3)) 297 | 298 | err = optionBool.Scan(1) 299 | Equal(t, err, nil) 300 | Equal(t, optionBool, Some(true)) 301 | 302 | err = optionString.Scan(value) 303 | Equal(t, err, nil) 304 | Equal(t, optionString, Some("123")) 305 | 306 | err = optionF32.Scan(float32(2.0)) 307 | Equal(t, err, nil) 308 | Equal(t, optionF32, Some(float32(2.0))) 309 | 310 | err = optionF32.Scan(math.MaxFloat64) 311 | Equal(t, err, nil) 312 | Equal(t, optionF32, Some(float32(math.Inf(1)))) 313 | 314 | err = optionF64.Scan(2.0) 315 | Equal(t, err, nil) 316 | Equal(t, optionF64, Some(2.0)) 317 | 318 | err = optionByte.Scan(uint8('1')) 319 | Equal(t, err, nil) 320 | Equal(t, optionByte, Some(uint8('1'))) 321 | 322 | err = optionTime.Scan("2023-06-13T06:34:32Z") 323 | Equal(t, err, nil) 324 | Equal(t, optionTime, Some(time.Date(2023, 6, 13, 6, 34, 32, 0, time.UTC))) 325 | 326 | err = optionTime.Scan([]byte("2023-06-13T06:34:32Z")) 327 | Equal(t, err, nil) 328 | Equal(t, optionTime, Some(time.Date(2023, 6, 13, 6, 34, 32, 0, time.UTC))) 329 | 330 | err = optionTime.Scan(time.Date(2023, 6, 13, 6, 34, 32, 0, time.UTC)) 331 | Equal(t, err, nil) 332 | Equal(t, optionTime, Some(time.Date(2023, 6, 13, 6, 34, 32, 0, time.UTC))) 333 | 334 | // Test nil 335 | var nullableOption Option[int64] 336 | err = nullableOption.Scan(nil) 337 | Equal(t, err, nil) 338 | Equal(t, nullableOption, None[int64]()) 339 | 340 | // custom scanner 341 | var custom Option[customScanner] 342 | err = custom.Scan("GOT HERE") 343 | Equal(t, err, nil) 344 | Equal(t, custom, Some(customScanner{S: "GOT HERE"})) 345 | 346 | // custom scanner scan nil 347 | var customNil Option[customScanner] 348 | err = customNil.Scan(nil) 349 | Equal(t, err, nil) 350 | Equal(t, customNil, None[customScanner]()) 351 | 352 | // test unmarshal to struct 353 | type myStruct struct { 354 | Name string `json:"name"` 355 | } 356 | 357 | var optionMyStruct Option[myStruct] 358 | err = optionMyStruct.Scan([]byte(`{"name":"test"}`)) 359 | Equal(t, err, nil) 360 | Equal(t, optionMyStruct, Some(myStruct{Name: "test"})) 361 | 362 | err = optionMyStruct.Scan(json.RawMessage(`{"name":"test2"}`)) 363 | Equal(t, err, nil) 364 | Equal(t, optionMyStruct, Some(myStruct{Name: "test2"})) 365 | 366 | var optionArrayOfMyStruct Option[[]myStruct] 367 | err = optionArrayOfMyStruct.Scan([]byte(`[{"name":"test"}]`)) 368 | Equal(t, err, nil) 369 | Equal(t, optionArrayOfMyStruct, Some([]myStruct{{Name: "test"}})) 370 | 371 | var optionMap Option[map[string]any] 372 | err = optionMap.Scan([]byte(`{"name":"test"}`)) 373 | Equal(t, err, nil) 374 | Equal(t, optionMap, Some(map[string]any{"name": "test"})) 375 | 376 | // test custom types 377 | var ct Option[customStringType] 378 | err = ct.Scan("test") 379 | Equal(t, err, nil) 380 | Equal(t, ct, Some(customStringType("test"))) 381 | 382 | err = optionArrBytes.Scan([]byte(`[1,2,3]`)) 383 | Equal(t, err, nil) 384 | Equal(t, optionArrBytes, Some([]byte(`[1,2,3]`))) 385 | 386 | err = optionArrBytes.Scan([]byte{4, 5, 6}) 387 | Equal(t, err, nil) 388 | Equal(t, optionArrBytes, Some([]byte{4, 5, 6})) 389 | 390 | err = optionRawMessage.Scan([]byte(`[1,2,3]`)) 391 | Equal(t, err, nil) 392 | Equal(t, true, string(optionRawMessage.Unwrap()) == "[1,2,3]") 393 | 394 | err = optionRawMessage.Scan([]byte{4, 5, 6}) 395 | Equal(t, err, nil) 396 | Equal(t, true, string(optionRawMessage.Unwrap()) == string([]byte{4, 5, 6})) 397 | } 398 | 399 | func TestNilOption(t *testing.T) { 400 | value := Some[any](nil) 401 | Equal(t, false, value.IsNone()) 402 | Equal(t, true, value.IsSome()) 403 | Equal(t, nil, value.Unwrap()) 404 | 405 | ret := returnTypedNoneOption() 406 | Equal(t, true, ret.IsNone()) 407 | Equal(t, false, ret.IsSome()) 408 | PanicMatches(t, func() { 409 | ret.Unwrap() 410 | }, "Option.Unwrap: option is None") 411 | 412 | ret = returnTypedSomeOption() 413 | Equal(t, false, ret.IsNone()) 414 | Equal(t, true, ret.IsSome()) 415 | Equal(t, myStruct{}, ret.Unwrap()) 416 | 417 | retPtr := returnTypedNoneOptionPtr() 418 | Equal(t, true, retPtr.IsNone()) 419 | Equal(t, false, retPtr.IsSome()) 420 | 421 | retPtr = returnTypedSomeOptionPtr() 422 | Equal(t, false, retPtr.IsNone()) 423 | Equal(t, true, retPtr.IsSome()) 424 | Equal(t, new(myStruct), retPtr.Unwrap()) 425 | } 426 | 427 | func TestOptionJSON(t *testing.T) { 428 | type s struct { 429 | Timestamp Option[time.Time] `json:"ts"` 430 | } 431 | now := time.Now().UTC().Truncate(time.Minute) 432 | tv := s{Timestamp: Some(now)} 433 | 434 | b, err := json.Marshal(tv) 435 | Equal(t, nil, err) 436 | Equal(t, `{"ts":"`+now.Format(time.RFC3339)+`"}`, string(b)) 437 | 438 | tv = s{} 439 | b, err = json.Marshal(tv) 440 | Equal(t, nil, err) 441 | Equal(t, `{"ts":null}`, string(b)) 442 | } 443 | 444 | func TestOptionJSONOmitempty(t *testing.T) { 445 | type s struct { 446 | Timestamp Option[time.Time] `json:"ts,omitempty"` 447 | } 448 | now := time.Now().UTC().Truncate(time.Minute) 449 | tv := s{Timestamp: Some(now)} 450 | 451 | b, err := json.Marshal(tv) 452 | Equal(t, nil, err) 453 | Equal(t, `{"ts":"`+now.Format(time.RFC3339)+`"}`, string(b)) 454 | 455 | type s2 struct { 456 | Timestamp *Option[time.Time] `json:"ts,omitempty"` 457 | } 458 | tv2 := &s2{} 459 | b, err = json.Marshal(tv2) 460 | Equal(t, nil, err) 461 | Equal(t, `{}`, string(b)) 462 | } 463 | 464 | type myStruct struct{} 465 | 466 | func returnTypedNoneOption() Option[myStruct] { 467 | return None[myStruct]() 468 | } 469 | 470 | func returnTypedSomeOption() Option[myStruct] { 471 | return Some(myStruct{}) 472 | } 473 | 474 | func returnTypedNoneOptionPtr() Option[*myStruct] { 475 | return None[*myStruct]() 476 | } 477 | 478 | func returnTypedSomeOptionPtr() Option[*myStruct] { 479 | return Some(new(myStruct)) 480 | } 481 | 482 | func BenchmarkOption(b *testing.B) { 483 | for i := 0; i < b.N; i++ { 484 | opt := returnTypedSomeOption() 485 | if opt.IsSome() { 486 | _ = opt.Unwrap() 487 | } 488 | } 489 | } 490 | 491 | func BenchmarkOptionPtr(b *testing.B) { 492 | for i := 0; i < b.N; i++ { 493 | opt := returnTypedSomeOptionPtr() 494 | if opt.IsSome() { 495 | _ = opt.Unwrap() 496 | } 497 | } 498 | } 499 | 500 | func BenchmarkNoOptionPtr(b *testing.B) { 501 | for i := 0; i < b.N; i++ { 502 | result := returnTypedNoOption() 503 | if result != nil { 504 | _ = result 505 | } 506 | } 507 | } 508 | 509 | func BenchmarkOptionNil(b *testing.B) { 510 | for i := 0; i < b.N; i++ { 511 | opt := returnTypedSomeOptionNil() 512 | if opt.IsSome() { 513 | _ = opt.Unwrap() 514 | } 515 | } 516 | } 517 | 518 | func BenchmarkNoOptionNil(b *testing.B) { 519 | for i := 0; i < b.N; i++ { 520 | result, found := returnNoOptionNil() 521 | if found { 522 | _ = result 523 | } 524 | } 525 | } 526 | 527 | func returnTypedSomeOptionNil() Option[any] { 528 | return Some[any](nil) 529 | } 530 | 531 | func returnTypedNoOption() *myStruct { 532 | return new(myStruct) 533 | } 534 | 535 | func returnNoOptionNil() (any, bool) { 536 | return nil, true 537 | } 538 | --------------------------------------------------------------------------------