├── 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 | 
4 | [](https://github.com/go-playground/pkg/actions/workflows/go.yml)
5 | [](https://coveralls.io/github/go-playground/pkg?branch=master)
6 | [](https://pkg.go.dev/mod/github.com/go-playground/pkg/v5)
7 | 
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 |
--------------------------------------------------------------------------------