From de9eb33c63150340bf49d1af62b95eb9736fcce6 Mon Sep 17 00:00:00 2001 From: Planxnx Date: Mon, 23 Oct 2023 19:09:03 +0700 Subject: [PATCH] feat(utils): add common utils package --- README.md | 6 + go.mod | 3 - go.sum | 0 go.work | 2 +- go.work.sum | 1 + utils/README.md | 12 ++ utils/bytes.go | 12 ++ utils/errors.go | 96 +++++++++++++ utils/go.mod | 14 ++ utils/go.sum | 12 ++ utils/hex.go | 66 +++++++++ utils/hex_test.go | 82 +++++++++++ utils/map.go | 30 ++++ utils/map_test.go | 59 ++++++++ utils/ptr.go | 69 ++++++++++ utils/ptr_test.go | 57 ++++++++ utils/string.go | 208 ++++++++++++++++++++++++++++ utils/string_conv.go | 23 ++++ utils/string_conv_1_20.go | 29 ++++ utils/string_test.go | 108 +++++++++++++++ utils/struct.go | 109 +++++++++++++++ utils/struct_test.go | 283 ++++++++++++++++++++++++++++++++++++++ utils/utils.go | 42 ++++++ 23 files changed, 1319 insertions(+), 4 deletions(-) delete mode 100644 go.mod delete mode 100644 go.sum create mode 100644 go.work.sum create mode 100644 utils/README.md create mode 100644 utils/bytes.go create mode 100644 utils/errors.go create mode 100644 utils/go.mod create mode 100644 utils/go.sum create mode 100644 utils/hex.go create mode 100644 utils/hex_test.go create mode 100644 utils/map.go create mode 100644 utils/map_test.go create mode 100644 utils/ptr.go create mode 100644 utils/ptr_test.go create mode 100644 utils/string.go create mode 100644 utils/string_conv.go create mode 100644 utils/string_conv_1_20.go create mode 100644 utils/string_test.go create mode 100644 utils/struct.go create mode 100644 utils/struct_test.go create mode 100644 utils/utils.go diff --git a/README.md b/README.md index 6978d3b..ec71480 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,12 @@ Miscellaneous useful Go packages by [Cleverse](https://about.cleverse.com) +## utils + +Optimized common generic utilities for Cleverse Golang projects. + +[See here](utils/README.md). + ## errors Package errors adds stacktrace support to errors in go. diff --git a/go.mod b/go.mod deleted file mode 100644 index 99f581a..0000000 --- a/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/Cleverse/go-utilities - -go 1.21 diff --git a/go.sum b/go.sum deleted file mode 100644 index e69de29..0000000 diff --git a/go.work b/go.work index af5456d..3bcb3e1 100644 --- a/go.work +++ b/go.work @@ -1,6 +1,6 @@ go 1.21 use ( - ./ ./errors + ./utils ) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..0a85547 --- /dev/null +++ b/go.work.sum @@ -0,0 +1 @@ +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/utils/README.md b/utils/README.md new file mode 100644 index 0000000..acc27bd --- /dev/null +++ b/utils/README.md @@ -0,0 +1,12 @@ +[![GoDoc](https://godoc.org/github.com/Cleverse/go-utilities/utils?status.svg)](http://godoc.org/github.com/Cleverse/go-utilities/utils) +[![Report card](https://goreportcard.com/badge/github.com/Cleverse/go-utilities/utils)](https://goreportcard.com/report/github.com/Cleverse/go-utilities/utils) + +# utils + +Optimized common generic utilities for Cleverse projects. + +## Installation + +```shell +go get github.com/Cleverse/go-utilities/utils +``` diff --git a/utils/bytes.go b/utils/bytes.go new file mode 100644 index 0000000..97831a7 --- /dev/null +++ b/utils/bytes.go @@ -0,0 +1,12 @@ +package utils + +import ( + "crypto/rand" +) + +// RandomBytes returns a random byte slice with the given length with crypto/rand. +func RandomBytes(length int) []byte { + b := make([]byte, length) + Must(rand.Read(b)) + return b +} diff --git a/utils/errors.go b/utils/errors.go new file mode 100644 index 0000000..6d5507b --- /dev/null +++ b/utils/errors.go @@ -0,0 +1,96 @@ +package utils + +import ( + "fmt" + "reflect" + + "github.com/Cleverse/go-utilities/errors" +) + +// Must is used to simplify error handling. +// It's helpful to wraps a call to a function returning a value and an error. and panics if err is error or false. +// +// warning: this is not safe, use with caution!! (avoid to use it's in runtime) +func Must[T any](data T, err any, messageArgs ...interface{}) T { + must(err, messageArgs...) + return data +} + +// MustNotError is used to simplify error handling. +// +// warning: this is not safe, use with caution!! (avoid to use it's in runtime) +func MustNotError[T any](data T, err error) T { + if err != nil { + panic(errors.WithStack(err)) + } + return data +} + +// UnsafeMust is used to simplify error/ok handling by ignoring it in runtime. +// +// warning: this is not safe, use with caution!! +// be careful when value is pointer, it may be nil. (safe in runtime, but need to check nil before use) +func UnsafeMust[T any, E any](data T, e E) T { + return data +} + +// MustOK is used to simplify ok handling. +// for case ok should be true. +// +// warning: this is not safe, use with caution!! (avoid to use it's in runtime) +func MustOK[T any](data T, ok bool) T { + if !ok { + panic(errors.Errorf("got not ok, but should ok")) + } + return data +} + +// MustNotOK is used to simplify ok handling. +// for case ok should be false. +// +// warning: this is not safe, use with caution!! (avoid to use it's in runtime) +func MustNotOK[T any](data T, ok bool) T { + if ok { + panic(errors.Errorf("got ok, but should not ok")) + } + return data +} + +// must panics if err is error or false. +func must(err any, messageArgs ...interface{}) { + if err == nil { + return + } + + switch e := err.(type) { + case bool: + if !e { + panic(Default[string](msgFormatter(messageArgs...), "not ok")) + } + case error: + if e == nil { + return + } + message := msgFormatter(messageArgs...) + if message != "" { + panic(message + ": " + e.Error()) + } + panic(errors.WithStack(e)) + default: + panic("must: invalid err type '" + reflect.TypeOf(err).Name() + "', should either be a bool or an error") + } +} + +func msgFormatter(msgAndArgs ...interface{}) string { + switch len(msgAndArgs) { + case 0: + return "" + case 1: + if msgAsStr, ok := msgAndArgs[0].(string); ok { + return msgAsStr + } + return fmt.Sprintf("%+v", msgAndArgs[0]) + default: + return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + } +} diff --git a/utils/go.mod b/utils/go.mod new file mode 100644 index 0000000..80b2c3a --- /dev/null +++ b/utils/go.mod @@ -0,0 +1,14 @@ +module github.com/Cleverse/go-utilities/utils + +go 1.19 + +require ( + github.com/Cleverse/go-utilities/errors v0.0.0-20231019072721-442842e3dc09 + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/utils/go.sum b/utils/go.sum new file mode 100644 index 0000000..814bfe2 --- /dev/null +++ b/utils/go.sum @@ -0,0 +1,12 @@ +github.com/Cleverse/go-utilities/errors v0.0.0-20231019072721-442842e3dc09 h1:kNAgub14cUBr7uoTxQSrf0qiMNJSzMhIDa8RFdv6hkk= +github.com/Cleverse/go-utilities/errors v0.0.0-20231019072721-442842e3dc09/go.mod h1:1QK+h746G1DwellQ6KK2rBCJusZqIDTZ9QFVGnUX9+Q= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/utils/hex.go b/utils/hex.go new file mode 100644 index 0000000..d4bb114 --- /dev/null +++ b/utils/hex.go @@ -0,0 +1,66 @@ +package utils + +import ( + "encoding/hex" + + "github.com/Cleverse/go-utilities/errors" +) + +// RandomHex returns a random hex string with the given length. +func RandomHex(length int) string { + // TODO: reduce memory allocation by using a same buffer for random and hex encoding. + return hex.EncodeToString(RandomBytes(length)) +} + +// Has0xPrefix checks if the input string has 0x prefix or not. +// +// Returns `true“ if the input string has 0x prefix, otherwise `false`. +func Has0xPrefix(input string) bool { + return len(input) >= 2 && input[0] == '0' && (input[1] == 'x' || input[1] == 'X') +} + +// Trim0xPrefix returns the input string without 0x prefix. +func Trim0xPrefix(input string) string { + if Has0xPrefix(input) { + return input[2:] + } + return input +} + +// Add0xPrefix returns the input string with 0x prefix. +func Add0xPrefix(input string) string { + if !Has0xPrefix(input) { + return "0x" + input + } + return input +} + +// Flip0xPrefix returns the input string with 0x prefix if it doesn't have 0x prefix, otherwise returns the input string without 0x prefix. +func Flip0xPrefix(input string) string { + if Has0xPrefix(input) { + return input[2:] + } + return "0x" + input +} + +// IsHex verifies whether a string can represent a valid hex-encoded or not. +func IsHex(str string) bool { + str = Trim0xPrefix(str) + for _, c := range []byte(str) { + if !isHexCharacter(c) { + return false + } + } + return true +} + +// isHexCharacter returns bool of c being a valid hexadecimal. +func isHexCharacter(c byte) bool { + return ('0' <= c && c <= '9') || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F') +} + +// DecodeHex decodes a hex string into a byte slice. str can be prefixed with 0x. +func DecodeHex(str string) ([]byte, error) { + b, err := hex.DecodeString(Trim0xPrefix(str)) + return b, errors.WithStack(err) +} diff --git a/utils/hex_test.go b/utils/hex_test.go new file mode 100644 index 0000000..2ebd0e5 --- /dev/null +++ b/utils/hex_test.go @@ -0,0 +1,82 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHex0XPrefix(t *testing.T) { + type TestCase struct { + Input string + Has0xPrefix bool + Func func(string) string + Expected string + } + + testCases := []TestCase{ + { + Input: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + Has0xPrefix: true, + Func: Trim0xPrefix, + Expected: "EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + { + Input: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + Has0xPrefix: true, + Func: Add0xPrefix, + Expected: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + { + Input: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + Has0xPrefix: true, + Func: Flip0xPrefix, + Expected: "EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + { + Input: "EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + Has0xPrefix: false, + Func: Trim0xPrefix, + Expected: "EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + { + Input: "EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + Has0xPrefix: false, + Func: Add0xPrefix, + Expected: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + { + Input: "EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + Has0xPrefix: false, + Func: Flip0xPrefix, + Expected: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + + { + Input: "0XEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + Has0xPrefix: true, + Func: Trim0xPrefix, + Expected: "EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + { + Input: "0XEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + Has0xPrefix: true, + Func: Add0xPrefix, + Expected: "0XEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + { + Input: "0XEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + Has0xPrefix: true, + Func: Flip0xPrefix, + Expected: "EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + } + + for _, tc := range testCases { + t.Run(tc.Input, func(t *testing.T) { + assert := assert.New(t) + assert.Equal(tc.Has0xPrefix, Has0xPrefix(tc.Input), "Has0xPrefix should be equal") + assert.Equal(tc.Expected, tc.Func(tc.Input), "actual result from `Func(string) string` should equal to expected") + }) + } +} diff --git a/utils/map.go b/utils/map.go new file mode 100644 index 0000000..2247945 --- /dev/null +++ b/utils/map.go @@ -0,0 +1,30 @@ +package utils + +// CopyMapOfArray high performance copy map of array +// to new map of array with shared backing array. +// Reference: https://cs.opensource.google/go/go/+/master:src/net/http/header.go;l=94 +func CopyMapOfArray[K comparable, V any](src map[K][]V) map[K][]V { + if src == nil { + return nil + } + + // Find total number of values. + totalValue := 0 + for _, val := range src { + totalValue += len(val) + } + + tmp := make([]V, totalValue) // use shared backing array for reduce memory allocation. + dst := make(map[K][]V, len(src)) + for k, val := range src { + if val == nil { + dst[k] = nil + continue + } + n := copy(tmp, val) // copy values to shared array. + dst[k] = tmp[:n:n] // point to specific length and capacity of shared backing array. + tmp = tmp[n:] // move pointer to next position. + } + + return dst +} diff --git a/utils/map_test.go b/utils/map_test.go new file mode 100644 index 0000000..52514b9 --- /dev/null +++ b/utils/map_test.go @@ -0,0 +1,59 @@ +package utils + +import ( + "fmt" + "strings" + "testing" + "unsafe" +) + +func BenchmarkCopySlice(b *testing.B) { + sizes := []int{8, 16, 64} + + for _, mapSize := range sizes { + b.Run(fmt.Sprintf("map_size_%02d", mapSize), func(b *testing.B) { + sliceSize := 32 + org := make(map[int][]string, mapSize) + for i := 0; i < mapSize; i++ { + org[i] = strings.Split(RandomString(sliceSize), "") + } + + stringByte := int(unsafe.Sizeof("")) + sliceBytes := int(unsafe.Sizeof(make([]string, sliceSize))) + mapBytes := int(unsafe.Sizeof(org)) + totalSize := (sliceBytes + (stringByte*sliceSize)*mapSize) + mapBytes + + b.Run("NormalCopy", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + clone := copyNormal(org) + _ = clone + b.SetBytes(int64(totalSize)) + } + }) + + b.Run("Optimized", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + clone := CopyMapOfArray(org) + _ = clone + b.SetBytes(int64(totalSize)) + } + }) + }) + } +} + +func copyNormal(org map[int][]string) map[int][]string { + clone := make(map[int][]string, len(org)) + for k, val := range org { + if val == nil { + clone[k] = nil + continue + } + clone[k] = make([]string, len(val)) + copy(clone[k], val) + } + + return clone +} diff --git a/utils/ptr.go b/utils/ptr.go new file mode 100644 index 0000000..3a67cab --- /dev/null +++ b/utils/ptr.go @@ -0,0 +1,69 @@ +/* +References: + - k8s.io/utils/pointer + - k8s.io/utils/ptr +*/ +package utils + +import "reflect" + +// PtrOf returns a pointer to the given value. alias of ToPtr. +func PtrOf[T any](v T) *T { + return ToPtr(v) +} + +// ToPtr returns a pointer to the given value. +func ToPtr[T any](v T) *T { + return &v +} + +// EmptyableToPtr returns a pointer copy of value if it's nonzero. +// Otherwise, returns nil pointer. +func EmptyableToPtr[T any](x T) *T { + isZero := reflect.ValueOf(&x).Elem().IsZero() + if isZero { + return nil + } + + return &x +} + +// DerefPtr dereferences ptr and returns the value it points to if no nil, or else +// returns def. +func DerefPtr[T any](ptr *T) T { + if ptr != nil { + return *ptr + } + return Empty[T]() +} + +// DerefPtrOr dereferences ptr and returns the value it points to if no nil, or else +// returns def. +func DerefPtrOr[T any](ptr *T, def T) T { + if ptr != nil { + return DerefPtr(ptr) + } + return def +} + +// FromPtr alias of DerefPtr. returns the pointer value or empty. +func FromPtr[T any](x *T) T { + return DerefPtr(x) +} + +// FromPtrOr alias of DerefPtrOr. returns the pointer value or the fallback value. +func FromPtrOr[T any](x *T, fallback T) T { + return DerefPtrOr(x, fallback) +} + +// Equal returns true if both arguments are nil or both arguments +// dereference to the same value. +func EqualPtr[T comparable](a, b *T) bool { + if (a == nil) != (b == nil) { + return false + } + if a == nil { + return true + } + return *a == *b +} diff --git a/utils/ptr_test.go b/utils/ptr_test.go new file mode 100644 index 0000000..ea8e454 --- /dev/null +++ b/utils/ptr_test.go @@ -0,0 +1,57 @@ +package utils + +import ( + "testing" +) + +func TestRef(t *testing.T) { + type T int + + val := T(0) + pointer := PtrOf(val) + if *pointer != val { + t.Errorf("expected %d, got %d", val, *pointer) + } + + val = T(1) + pointer = PtrOf(val) + if *pointer != val { + t.Errorf("expected %d, got %d", val, *pointer) + } +} + +func TestDerefOr(t *testing.T) { + type T int + + var val, def T = 1, 0 + + out := DerefPtrOr(&val, def) + if out != val { + t.Errorf("expected %d, got %d", val, out) + } + + out = DerefPtrOr(nil, def) + if out != def { + t.Errorf("expected %d, got %d", def, out) + } +} + +func TestEqual(t *testing.T) { + type T int + + if !EqualPtr[T](nil, nil) { + t.Errorf("expected true (nil == nil)") + } + if !EqualPtr(ToPtr(T(123)), ToPtr(T(123))) { + t.Errorf("expected true (val == val)") + } + if EqualPtr(nil, ToPtr(T(123))) { + t.Errorf("expected false (nil != val)") + } + if EqualPtr(ToPtr(T(123)), nil) { + t.Errorf("expected false (val != nil)") + } + if EqualPtr(ToPtr(T(123)), ToPtr(T(456))) { + t.Errorf("expected false (val != val)") + } +} diff --git a/utils/string.go b/utils/string.go new file mode 100644 index 0000000..4a7bfd3 --- /dev/null +++ b/utils/string.go @@ -0,0 +1,208 @@ +package utils + +import ( + "crypto/rand" + "fmt" + "math" + "math/big" + "os" + "path" + "reflect" + "strconv" + "strings" + "time" +) + +const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +func RandomString(n int) string { + var s strings.Builder + s.Grow(n) + for i := 0; i < n; i++ { + num := MustNotError(rand.Int(rand.Reader, big.NewInt(math.MaxInt64))) + s.WriteByte(letters[num.Int64()%int64(len(letters))]) + } + return s.String() +} + +// CopyBytes copies a slice to make it immutable +func CopyBytes(b []byte) []byte { + tmp := make([]byte, len(b)) + copy(tmp, b) + return tmp +} + +const ( + uByte = 1 << (10 * iota) + uKilobyte + uMegabyte + uGigabyte + uTerabyte + uPetabyte + uExabyte +) + +// ByteSize returns a human-readable byte string, eg. 10M, 12.5K. +func ByteSize(bytes uint64) string { + unit := "" + value := float64(bytes) + switch { + case bytes >= uExabyte: + unit = "EB" + value /= uExabyte + case bytes >= uPetabyte: + unit = "PB" + value /= uPetabyte + case bytes >= uTerabyte: + unit = "TB" + value /= uTerabyte + case bytes >= uGigabyte: + unit = "GB" + value /= uGigabyte + case bytes >= uMegabyte: + unit = "MB" + value /= uMegabyte + case bytes >= uKilobyte: + unit = "KB" + value /= uKilobyte + case bytes >= uByte: + unit = "B" + default: + return "0B" + } + result := strconv.FormatFloat(value, 'f', 1, 64) + result = strings.TrimSuffix(result, ".0") + return result + unit +} + +// ToString Change any supports types to string +func ToString(arg interface{}, args ...any) string { + tmp := reflect.Indirect(reflect.ValueOf(arg)).Interface() + switch v := tmp.(type) { + case string: + return fmt.Sprintf(v, args...) + case []byte: + return string(v) + case int: + return strconv.Itoa(v) + case int8: + return strconv.FormatInt(int64(v), 10) + case int16: + return strconv.FormatInt(int64(v), 10) + case int32: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(v, 10) + case uint: + return strconv.Itoa(int(v)) + case uint8: + return strconv.FormatInt(int64(v), 10) + case uint16: + return strconv.FormatInt(int64(v), 10) + case uint32: + return strconv.FormatInt(int64(v), 10) + case uint64: + return strconv.FormatInt(int64(v), 10) + case bool: + return strconv.FormatBool(v) + case float32: + return strconv.FormatFloat(float64(v), 'f', -1, 32) + case float64: + return strconv.FormatFloat(v, 'f', -1, 64) + case time.Time: + if len(args) > 0 { + if format, ok := args[0].(string); ok { + return v.Format(format) + } + } + return v.Format("2006-01-02 15:04:05") + case reflect.Value: + return ToString(v.Interface(), args...) + case fmt.Stringer: + return v.String() + default: + return "" + } +} + +const twoAsterisks = "**" + +// Match matches patterns as gitignore pattern. +// Reference https://git-scm.com/docs/gitignore, https://gist.github.com/jstnlvns/ebaa046fae16543cc9efc7f24bcd0e31 +func Match(pattern, value string) bool { + if pattern == "" { + return false + } + + // Code Comment + if strings.HasPrefix(pattern, "#") { + return false + } + + pattern = strings.TrimSuffix(pattern, " ") + pattern = strings.TrimSuffix(pattern, string(os.PathSeparator)) + + // Reverse result if pattern starts with "!" + neg := strings.HasPrefix(pattern, "!") + if neg { + pattern = strings.TrimPrefix(pattern, "!") + } + + // Two Consecutive Asterisks ("**") + if strings.Contains(pattern, twoAsterisks) { + result := MatchTwoAsterisk(pattern, value) + if neg { + result = !result + } + return result + } + + // Shell-style Pattern Matching + matched, err := path.Match(pattern, value) + if err != nil { + return false + } + + if neg { + return !matched + } + + return matched +} + +func MatchTwoAsterisk(pattern, value string) bool { + // **.openapi.json == fund-api.openapi.json + if strings.HasPrefix(pattern, twoAsterisks) { + pattern = strings.TrimPrefix(pattern, twoAsterisks) + return strings.HasSuffix(value, pattern) + } + + // docs/** == docs/README.md or index.** == index.json, index.yaml + if strings.HasSuffix(pattern, twoAsterisks) { + pattern = strings.TrimSuffix(pattern, twoAsterisks) + return strings.HasPrefix(value, pattern) + } + + // "a/**/b" == "a/b", /"a/x/b", "a/x/y/b" + parts := strings.Split(pattern, twoAsterisks) + for i, part := range parts { + switch i { + case 0: // first part + if !strings.HasPrefix(value, part) { + return false + } + case len(parts) - 1: // last part + part = strings.TrimPrefix(part, string(os.PathSeparator)) + return strings.HasSuffix(value, part) + default: + if !strings.Contains(value, part) { + return false + } + } + + index := strings.Index(value, part) + len(part) + value = value[index:] + } + + return false +} diff --git a/utils/string_conv.go b/utils/string_conv.go new file mode 100644 index 0000000..aded75b --- /dev/null +++ b/utils/string_conv.go @@ -0,0 +1,23 @@ +//go:build go1.20 +// +build go1.20 + +package utils + +import ( + "unsafe" +) + +// UnsafeString returns a string pointer without allocation +func UnsafeString(b []byte) string { + return unsafe.String(unsafe.SliceData(b), len(b)) +} + +// UnsafeBytes returns a byte pointer without allocation +func UnsafeBytes(s string) (bs []byte) { + return unsafe.Slice(unsafe.StringData(s), len(s)) +} + +// CopyString copies a string to make it immutable +func CopyString(s string) string { + return string(UnsafeBytes(s)) +} diff --git a/utils/string_conv_1_20.go b/utils/string_conv_1_20.go new file mode 100644 index 0000000..fa805cf --- /dev/null +++ b/utils/string_conv_1_20.go @@ -0,0 +1,29 @@ +//go:build !go1.20 +// +build !go1.20 + +package utils + +import ( + "reflect" + "unsafe" +) + +// UnsafeString returns a string pointer without allocation +func UnsafeString(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} + +// UnsafeBytes returns a byte pointer without allocation +func UnsafeBytes(s string) (bs []byte) { + sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) + bh := (*reflect.SliceHeader)(unsafe.Pointer(&bs)) + bh.Data = sh.Data + bh.Len = sh.Len + bh.Cap = sh.Len + return +} + +// CopyString copies a string to make it immutable +func CopyString(s string) string { + return string(UnsafeBytes(s)) +} diff --git a/utils/string_test.go b/utils/string_test.go new file mode 100644 index 0000000..4693cc2 --- /dev/null +++ b/utils/string_test.go @@ -0,0 +1,108 @@ +package utils + +import ( + "crypto/rand" + "math" + "math/big" + "strings" + "testing" +) + +func BenchmarkRandomgString(b *testing.B) { + type testSpec struct { + name string + size int + } + testCases := []testSpec{ + {name: "extra-small", size: 1}, + {name: "small", size: 1 << 3}, // 8 + {name: "medium", size: 1 << 6}, // 64 + {name: "large", size: 1 << 9}, // 512 + } + + type randomStringFunc func(n int) string + randomers := map[string]randomStringFunc{ + "random_runes": func() randomStringFunc { + letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + return func(n int) string { + ret := make([]rune, n) + for i := 0; i < n; i++ { + num := MustNotError(rand.Int(rand.Reader, big.NewInt(int64(len(letters))))) + ret[i] = letters[num.Int64()] + } + return string(ret) + } + }(), + "random_bytes": func() randomStringFunc { + letters := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + return func(n int) string { + ret := make([]byte, n) + for i := 0; i < n; i++ { + num := MustNotError(rand.Int(rand.Reader, big.NewInt(int64(len(letters))))) + ret[i] = letters[num.Int64()] + } + return string(ret) + } + }(), + "random_bytes_optimized_1": func() randomStringFunc { + // use strings.Builder to reduce allocations. + letters := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + return func(n int) string { + var b strings.Builder + for i := 0; i < n; i++ { + num := MustNotError(rand.Int(rand.Reader, big.NewInt(int64(len(letters))))) + b.WriteByte(letters[num.Int64()%int64(len(letters))]) + } + return b.String() + } + }(), + "random_bytes_optimized_2": func() randomStringFunc { + // declare letters as a const to reduce len() calls to O(1). + const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + return func(n int) string { + ret := make([]byte, n) + for i := 0; i < n; i++ { + num := MustNotError(rand.Int(rand.Reader, big.NewInt(int64(len(letters))))) + ret[i] = letters[num.Int64()] + } + return string(ret) + } + }(), + "random_bytes_optimized_3": func() randomStringFunc { + letters := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + return func(n int) string { + ret := make([]byte, n) + for i := 0; i < n; i++ { + num := MustNotError(rand.Int(rand.Reader, big.NewInt(math.MaxInt64))) + ret[i] = letters[num.Int64()%int64(len(letters))] + } + return string(ret) + } + }(), + "random_bytes_optimized_all": func() randomStringFunc { + const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + return func(n int) string { + var b strings.Builder + b.Grow(n) + for i := 0; i < n; i++ { + num := MustNotError(rand.Int(rand.Reader, big.NewInt(math.MaxInt64))) + b.WriteByte(letters[num.Int64()%int64(len(letters))]) + } + return b.String() + } + }(), + } + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + for name, random := range randomers { + b.Run(name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + random(tc.size) + b.SetBytes(int64(tc.size)) + } + }) + } + }) + } +} diff --git a/utils/struct.go b/utils/struct.go new file mode 100644 index 0000000..e10e383 --- /dev/null +++ b/utils/struct.go @@ -0,0 +1,109 @@ +package utils + +import ( + "encoding/json" + "fmt" + "reflect" +) + +// PrettyStruct returns pretty json string of the given struct. +func PrettyStruct(s interface{}) string { + b, _ := json.MarshalIndent(s, "", " ") + return string(b) +} + +// StructHasZero returns true if a field in a struct is zero value(has not initialized) +// if includeNested is true, it will check nested struct (default is false) +func StructHasZero(s any, includeNested ...bool) bool { + checkNested := false + if len(includeNested) > 0 { + checkNested = includeNested[0] + } + return len(StructZeroFields(s, checkNested)) > 0 +} + +// StructZeroFields returns name of fields if that's field in a struct is zero value(has not initialized) +// if checkNested is true, it will check nested struct (default is false) +func StructZeroFields(s any, checkNested ...bool) []string { + checknested := false + if len(checkNested) > 0 { + checknested = checkNested[0] + } + value := reflect.ValueOf(s) + + // if it's pointe, then get the underlying element≤ + for value.Kind() == reflect.Ptr { + value = value.Elem() + } + + // support only struct + if value.Kind() != reflect.Struct { + panic(fmt.Sprintf("Invalid input type %q, must be struct", value.Kind())) + } + + t := value.Type() + tLen := t.NumField() + + // get all fields in the struct + fields := make([]reflect.StructField, 0, tLen) + for i := 0; i < tLen; i++ { + field := t.Field(i) + + // check is not public fields (can exported field) + if field.PkgPath == "" { + fields = append(fields, field) + } + } + + // find zero fields + zeroFields := make([]string, 0) + for _, field := range fields { + fieldValue := value.FieldByName(field.Name) + + zeroValue := reflect.Zero(fieldValue.Type()).Interface() + if reflect.DeepEqual(fieldValue.Interface(), zeroValue) { + zeroFields = append(zeroFields, field.Name) + } + + // Check nested struct + if checknested && IsStruct(fieldValue.Interface()) { + if nestedZero := StructZeroFields(fieldValue.Interface(), checknested); len(nestedZero) > 0 { + zeroFields = append(zeroFields, field.Name) + } + continue + } + } + + return zeroFields +} + +// IsStruct returns true if the given variable is a struct or *struct. +func IsStruct(s interface{}) bool { + v := reflect.ValueOf(s) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + return v.Kind() == reflect.Struct +} + +// Merge merges two structs with the same type. merge from -> to. +// +// warning: this function will modify the first struct (to) +func Merge[T comparable](to, from T) T { + if !IsStruct(to) { + return to + } + + t := reflect.ValueOf(to).Elem() + f := reflect.ValueOf(from).Elem() + + for i := 0; i < t.NumField(); i++ { + defaultField := t.Field(i) + newField := f.Field(i) + if newField.Interface() != reflect.Zero(defaultField.Type()).Interface() { + defaultField.Set(reflect.ValueOf(newField.Interface())) + } + } + return to +} diff --git a/utils/struct_test.go b/utils/struct_test.go new file mode 100644 index 0000000..d34857c --- /dev/null +++ b/utils/struct_test.go @@ -0,0 +1,283 @@ +package utils + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestZeroFieldsEqualStructHasZero(t *testing.T) { + type C struct { + C string + } + + type BC struct { + B string + C *C + } + + type ABC struct { + A string + B BC + } + + { + val := ABC{ + A: "A", + B: BC{}, + } + isZero_01 := len(StructZeroFields(val)) > 0 + isZero_02 := StructHasZero(val) + assert.Equal(t, isZero_01, isZero_02) + assert.True(t, isZero_01) + } + { + val := ABC{ + A: "A", + B: BC{ + B: "B", + }, + } + + { + isZero_01 := len(StructZeroFields(val)) > 0 + isZero_02 := StructHasZero(val) + assert.Equal(t, isZero_01, isZero_02) + assert.False(t, isZero_01) + } + { + isZero_01 := len(StructZeroFields(val, true)) > 0 + isZero_02 := StructHasZero(val, true) + assert.Equal(t, isZero_01, isZero_02) + assert.True(t, isZero_01) + } + } + { + val := ABC{ + A: "A", + B: BC{ + B: "B", + C: &C{}, + }, + } + + { + isZero_01 := len(StructZeroFields(val)) > 0 + isZero_02 := StructHasZero(val) + assert.Equal(t, isZero_01, isZero_02) + assert.False(t, isZero_01) + } + { + isZero_01 := len(StructZeroFields(val, true)) > 0 + isZero_02 := StructHasZero(val, true) + assert.Equal(t, isZero_01, isZero_02) + assert.True(t, isZero_01) + } + } +} + +func TestStructZeroFields(t *testing.T) { + type TestSpec struct { + ExpectedFields []string + CheckNested bool + Struct interface{} + } + + testSpecs := []TestSpec{ + { + ExpectedFields: []string{"C", "D"}, + Struct: struct { + A string + B int + C bool + D []string + }{ + A: "A", + B: 2, + }, + }, + { + ExpectedFields: []string{"B"}, + Struct: struct { + A string + B int + c bool + }{ + A: "A", + }, + }, + { + ExpectedFields: []string{}, + Struct: struct { + A string + b int + }{ + A: "A", + }, + }, + { + ExpectedFields: []string{"F", "X"}, + Struct: struct { + A string + F *bool + X struct { + Y string + Z *bool + } + }{ + A: "A", + }, + }, + { + ExpectedFields: []string{"B"}, + CheckNested: true, + Struct: struct { + A string + B interface{} + }{ + A: "A", + B: &struct { + C []string + }{ + C: nil, + }, + }, + }, + } + + for i, testSpec := range testSpecs { + t.Run(fmt.Sprint("#", i+1), func(t *testing.T) { + assert.Equal(t, testSpec.ExpectedFields, StructZeroFields(testSpec.Struct, testSpec.CheckNested)) + }) + } +} + +func TestStructHasZero(t *testing.T) { + type TestSpec struct { + ExpectedZero bool + Struct interface{} + } + + testSpecs := []TestSpec{ + { + ExpectedZero: true, + Struct: struct { + A string + B int + C bool + D []string + }{ + A: "A", + B: 2, + }, + }, + { + ExpectedZero: true, + Struct: struct { + A string + F *bool + }{ + A: "A", + }, + }, + { + ExpectedZero: false, + Struct: struct { + a string + b string + }{ + b: "Lowercase field B", + }, + }, + { + ExpectedZero: false, + Struct: struct { + a string + B string + }{ + B: "Uppercase field B", + }, + }, + { + ExpectedZero: false, + Struct: struct { + A string + B interface{} + }{ + A: "A", + B: struct { + C string + }{ + C: "C", + }, + }, + }, + { + ExpectedZero: true, + Struct: struct { + A string + B struct { + C []string + D string + } + }{ + A: "A", + }, + }, + } + + for i, testSpec := range testSpecs { + t.Run(fmt.Sprint("#", i+1), func(t *testing.T) { + assert.Equal(t, testSpec.ExpectedZero, StructHasZero(testSpec.Struct)) + }) + } +} + +func TestMerge(t *testing.T) { + type Data struct { + A string + B int + C *Data + D *Data + E *Data + } + + org := Data{ + A: "A", + B: 1, + C: nil, + D: &Data{ + A: "D", + }, + E: &Data{ + A: "E", + }, + } + + to := org + from := &Data{ + A: "AAAA", + B: 0, + C: &Data{ + A: "C", + }, + D: nil, + E: &Data{ + A: "EEEE", + }, + } + + result := Merge(&to, from) + + assert.Equal(t, from.A, result.A) + assert.NotEqual(t, org.A, result.A) + assert.Equal(t, org.B, result.B) + assert.NotEqual(t, from.B, result.B) + assert.Equal(t, from.C, result.C) + assert.NotEqual(t, org.C, result.C) + assert.Equal(t, org.D, result.D) + assert.NotEqual(t, from.D, result.D) + assert.Equal(t, from.E, result.E) + assert.NotEqual(t, org.E, result.E) +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..585f57e --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,42 @@ +package utils + +// Default inspired by Nullish coalescing operator (??) in JavaScript +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing +func Default[T comparable](value T, defaultValue T) (result T) { + // if value != zero + if value != result { + return value + } + // if value == zero + return defaultValue +} + +// Optional extract optional parameter from variadic function parameter. If parameter is not provided, return zero value of type T and false. +// If parameter is provided, return parameter and true. +// +// Example: func Get(key string, option ...Option) (string, error) { ... } +// +// It's useful to reduce boilerplate code when implementing optional parameter. You won't need to check if parameter is provided or not. +func Optional[T any](opt []T) (optional T, ok bool) { + if len(opt) > 0 { + return opt[0], true + } + + return optional, false +} + +// ToZero alias of Zero. returns zero value of the type of the given value. +func ToZero[T any](value T) (result T) { + return Zero[T](value) +} + +// Zero returns zero value of the type of the given value. +func Zero[T any](value T) T { + return Empty[T]() +} + +// Empty returns an empty value of the given type. +func Empty[T any]() T { + var zero T + return zero +}