├── .github ├── renovate.json └── workflows │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── .tool-versions ├── LICENSE ├── README.md ├── csv.go ├── debug.go ├── debug_test.go ├── ent.go ├── ent_test.go ├── env.go ├── env_test.go ├── exp ├── README.md └── channelx │ ├── example_test.go │ ├── puller.go │ ├── puller_test.go │ └── pusher.go ├── float.go ├── float_test.go ├── generic.go ├── generic_test.go ├── go.mod ├── go.sum ├── int.go ├── int_test.go ├── io.go ├── io_test.go ├── json.go ├── json_test.go ├── k8s.go ├── k8s_test.go ├── logger ├── format.go ├── logger.go ├── logger_test.go ├── loki │ ├── loki.go │ └── sink.go └── otelzap │ ├── arrayencoder.go │ ├── otelzap.go │ └── utils.go ├── opqcursor └── opqcursor.go ├── pagination ├── pagination.go └── pagination_test.go ├── path.go ├── protobufs ├── buf.gen.yaml ├── buf.lock ├── buf.yaml └── testpb │ ├── test.pb.go │ └── test.proto ├── random.go ├── random_test.go ├── slice.go ├── slice_test.go ├── string.go ├── string_test.go └── units.go /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":dependencyDashboard", 6 | ":semanticPrefixFixDepsChoreOthers", 7 | ":prHourlyLimitNone", 8 | ":prConcurrentLimitNone", 9 | ":ignoreModulesAndTests", 10 | "schedule:monthly", 11 | "group:allNonMajor", 12 | "replacements:all", 13 | "workarounds:all" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | paths-ignore: 11 | - "**/*.md" 12 | 13 | jobs: 14 | buildtest: 15 | name: Build Test 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: actions/setup-go@v5 21 | with: 22 | go-version: "^1.24" 23 | cache: true 24 | 25 | - name: Test Build 26 | run: go build ./... 27 | 28 | lint: 29 | name: Lint 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/setup-go@v5 33 | with: 34 | go-version: "^1.24" 35 | cache: true 36 | 37 | - uses: actions/checkout@v4 38 | - name: golangci-lint 39 | uses: golangci/golangci-lint-action@v8.0.0 40 | with: 41 | # Optional: golangci-lint command line arguments. 42 | args: "--timeout=10m" 43 | 44 | unittest: 45 | name: Unit Test 46 | runs-on: ubuntu-latest 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | 51 | - name: Setup Go 52 | uses: actions/setup-go@v5 53 | with: 54 | go-version: "^1.24" 55 | cache: true 56 | 57 | - name: Unit tests 58 | run: | 59 | go test ./... -coverprofile=coverage.out -covermode=atomic 60 | go tool cover -func coverage.out 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .vscode/ 3 | .idea/ 4 | 5 | # Build / Release 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | *.db 12 | *.bin 13 | *.tar.gz 14 | /release/ 15 | 16 | # Runtime / Compile Temporary Assets 17 | vendor/ 18 | logs/ 19 | 20 | # Credentials 21 | cert*/ 22 | *.pem 23 | *.crt 24 | *.cer 25 | *.key 26 | *.p12 27 | 28 | # Test binary, build with `go test -c` 29 | *.test 30 | cover* 31 | coverage* 32 | insights-bot 33 | insights-bot 34 | !cmd/insights-bot 35 | 36 | # Output of the go coverage tool, specifically when used with LiteIDE 37 | *.out 38 | 39 | # macOS 40 | .DS_Store 41 | 42 | # Configurations 43 | config.yaml 44 | config.yml 45 | .env 46 | 47 | # Local Configuration 48 | config.local.yaml 49 | 50 | # Temporary 51 | temp/ 52 | 53 | # Local pgSQL db 54 | .postgres/ 55 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - bodyclose 5 | - containedctx 6 | - contextcheck 7 | - dupl 8 | - durationcheck 9 | - errname 10 | - exhaustive 11 | - forcetypeassert 12 | - godot 13 | - goheader 14 | - goprintffuncname 15 | - gosec 16 | - musttag 17 | - nestif 18 | - nilerr 19 | - noctx 20 | - nolintlint 21 | - nosprintfhostport 22 | - prealloc 23 | - predeclared 24 | - reassign 25 | - revive 26 | - testableexamples 27 | - unconvert 28 | - unparam 29 | - usestdlibvars 30 | - whitespace 31 | - wsl 32 | settings: 33 | nestif: 34 | min-complexity: 10 35 | revive: 36 | rules: 37 | - name: blank-imports 38 | disabled: true 39 | wsl: 40 | strict-append: false 41 | allow-assign-and-call: false 42 | exclusions: 43 | generated: lax 44 | presets: 45 | - comments 46 | - common-false-positives 47 | - legacy 48 | - std-error-handling 49 | rules: 50 | - linters: 51 | - dupl 52 | path: example_.*\.go 53 | - path: (.+)\.go$ 54 | text: if statements should only be cuddled with assignments 55 | - path: (.+)\.go$ 56 | text: if statements should only be cuddled with assignments used in the if statement itself 57 | - path: (.+)\.go$ 58 | text: assignments should only be cuddled with other assignments 59 | - path: (.+)\.go$ 60 | text: declarations should never be cuddled 61 | paths: 62 | - third_party$ 63 | - builtin$ 64 | - examples$ 65 | formatters: 66 | enable: 67 | - gofmt 68 | exclusions: 69 | generated: lax 70 | paths: 71 | - third_party$ 72 | - builtin$ 73 | - examples$ 74 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.24.3 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Neko Ayaka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xo 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/nekomeowww/xo.svg)](https://pkg.go.dev/github.com/nekomeowww/xo) 4 | ![](https://github.com/nekomeowww/xo/actions/workflows/ci.yml/badge.svg) 5 | [![](https://goreportcard.com/badge/github.com/nekomeowww/xo)](https://goreportcard.com/report/github.com/nekomeowww/xo) 6 | 7 | 🪐 Universal external Golang utilities, implementations, and even experimental coding patterns, designs 8 | 9 | ## Development 10 | 11 | Clone the repository: 12 | 13 | ```shell 14 | git clone https://github.com/nekomeowww/xo 15 | cd xo 16 | ``` 17 | 18 | Prepare the dependencies: 19 | 20 | ```shell 21 | go mod tidy 22 | ``` 23 | 24 | > [!NOTE] 25 | > If you want to work with `protobufs/testpb` directory and generate new Golang code, you need to install the [`buf`](https://buf.build/docs/installation) tool. 26 | > 27 | > ```shell 28 | > cd protobufs 29 | > buf dep update 30 | > buf generate --path ./testpb 31 | > ``` 32 | 33 | ## 🤠 Spec 34 | 35 | GoDoc: [https://godoc.org/github.com/nekomeowww/xo](https://godoc.org/github.com/nekomeowww/xo) 36 | 37 | ## 👪 Other family members of `anyo` 38 | 39 | - [nekomeowww/fo](https://github.com/nekomeowww/fo): Functional programming utility library for Go 40 | - [nekomeowww/bo](https://github.com/nekomeowww/bo): BootKit for easily bootstrapping multi-goroutine applications, CLIs 41 | - [nekomeowww/tgo](https://github.com/nekomeowww/tgo): Telegram bot framework for Go 42 | - [nekomeowww/wso](https://github.com/nekomeowww/wso): WebSocket utility library for Go 43 | 44 | ## 🎆 Other cool related Golang projects I made & maintained 45 | 46 | - [nekomeowww/factorio-rcon-api](https://github.com/nekomeowww/factorio-rcon-api): Fully implemented wrapper for Factorio RCON as API 47 | - [Kollama - Ollama Operator](https://github.com/knoway-dev/knoway): Kubernetes Operator for managing Ollama instances across multiple clusters 48 | - [lingticio/llmg](https://github.com/lingticio/llmg): LLM Gateway with gRPC, WebSocket, and RESTful API adapters included. 49 | - [nekomeowww/hyphen](https://github.com/nekomeowww/hyphen): An elegant URL Shortener service 50 | - [nekomeowww/insights-bot](https://github.com/nekomeowww/insights-bot): Webpage summary & chat history recap bot for Telegram 51 | -------------------------------------------------------------------------------- /csv.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "encoding/csv" 5 | "os" 6 | ) 7 | 8 | func ReadCSV(path string) ([][]string, error) { 9 | file, err := os.Open(path) 10 | if err != nil { 11 | return make([][]string, 0), err 12 | } 13 | defer file.Close() 14 | 15 | csvReader := csv.NewReader(file) 16 | csvReader.Comma = ',' 17 | csvReader.LazyQuotes = true 18 | 19 | records, err := csvReader.ReadAll() 20 | if err != nil { 21 | return make([][]string, 0), err 22 | } 23 | 24 | return records, nil 25 | } 26 | -------------------------------------------------------------------------------- /debug.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/davecgh/go-spew/spew" 9 | ) 10 | 11 | // Print formats the output of all incoming values in terms of field, value, 12 | // type, and size. 13 | func Print(inputs ...interface{}) { 14 | fmt.Println(Sprint(inputs)) 15 | } 16 | 17 | // Sprint formats the output of all the fields, values, types, and sizes of 18 | // the values passed in and returns the string. 19 | // 20 | // NOTICE: newline control character is included. 21 | func Sprint(inputs ...interface{}) string { 22 | return spew.Sdump(inputs) 23 | } 24 | 25 | // PrintJSON formats the output of all incoming values in JSON format. 26 | func PrintJSON(inputs ...interface{}) { 27 | fmt.Println(SprintJSON(inputs)) 28 | } 29 | 30 | // SprintJSON formats the output of all incoming values in JSON format and 31 | // 32 | // NOTICE: newline control character is included. 33 | func SprintJSON(inputs ...interface{}) string { 34 | strSlice := make([]string, 0) 35 | 36 | for _, v := range inputs { 37 | b, _ := json.MarshalIndent(v, "", " ") 38 | strSlice = append(strSlice, string(b)) 39 | } 40 | 41 | return strings.Join(strSlice, "\n") 42 | } 43 | -------------------------------------------------------------------------------- /debug_test.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPrintAndPrintJSON(t *testing.T) { 10 | type testEmbedded struct { 11 | C int 12 | D []string 13 | } 14 | 15 | type testStruct struct { 16 | A int 17 | B string 18 | Embedded testEmbedded 19 | } 20 | 21 | t.Run("Print", func(t *testing.T) { 22 | assert := assert.New(t) 23 | 24 | assert.NotPanics(func() { 25 | Print(nil) 26 | }) 27 | assert.NotPanics(func() { 28 | Print(testStruct{}) 29 | }) 30 | }) 31 | 32 | t.Run("Sprint", func(t *testing.T) { 33 | assert := assert.New(t) 34 | 35 | assert.NotPanics(func() { 36 | str := Sprint(nil) 37 | assert.NotEmpty(str) 38 | }) 39 | assert.NotPanics(func() { 40 | str := Sprint(testStruct{}) 41 | assert.NotEmpty(str) 42 | }) 43 | }) 44 | 45 | t.Run("PrintJSON", func(t *testing.T) { 46 | assert := assert.New(t) 47 | 48 | assert.NotPanics(func() { 49 | PrintJSON(nil) 50 | }) 51 | assert.NotPanics(func() { 52 | PrintJSON(testStruct{}) 53 | }) 54 | }) 55 | 56 | t.Run("SprintJSON", func(t *testing.T) { 57 | assert := assert.New(t) 58 | 59 | assert.NotPanics(func() { 60 | str := SprintJSON(nil) 61 | assert.NotEmpty(str) 62 | }) 63 | 64 | assert.NotPanics(func() { 65 | str := SprintJSON(testStruct{}) 66 | assert.NotEmpty(str) 67 | }) 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /ent.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "fmt" 7 | 8 | "entgo.io/ent/schema/field" 9 | "github.com/samber/lo" 10 | "google.golang.org/protobuf/encoding/protojson" 11 | "google.golang.org/protobuf/proto" 12 | ) 13 | 14 | var _ field.TypeValueScanner[*any] = (*ProtoValueScanner[any])(nil) 15 | 16 | /* 17 | ProtoValueScanner is a field.ValueScanner that implements the ent.ValueScanner interface as helper for 18 | working with protobuf messages. It is used to scan and convert protobuf messages to and from the database. 19 | 20 | func (SomeTable) Fields() []ent.Field { 21 | return []ent.Field{ 22 | field. 23 | String("payload"). 24 | ValueScanner(utils.ProtoValueScanner[somepb.YourMessage]{}). 25 | GoType(&somepb.YourMessage{}). 26 | SchemaType(map[string]string{ 27 | dialect.Postgres: "jsonb", 28 | dialect.MySQL: "json", 29 | dialect.SQLite: "json", 30 | }), 31 | } 32 | } 33 | */ 34 | type ProtoValueScanner[T any] struct { 35 | } 36 | 37 | func (s ProtoValueScanner[T]) v(data *T) (driver.Value, error) { 38 | if data == nil { 39 | return sql.NullString{}, nil 40 | } 41 | 42 | pbMessage, ok := any(data).(proto.Message) 43 | pbMessage = lo.Must(pbMessage, ok) 44 | 45 | bytes, err := protojson.Marshal(pbMessage) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return &sql.NullString{String: string(bytes), Valid: true}, nil 51 | } 52 | func (s ProtoValueScanner[T]) s(sqlData *sql.NullString) (*T, error) { 53 | if sqlData == nil { 54 | return nil, nil 55 | } 56 | if !sqlData.Valid { 57 | return nil, nil 58 | } 59 | 60 | var data T 61 | 62 | pbMessage, ok := any(&data).(proto.Message) 63 | pbMessage = lo.Must(pbMessage, ok) 64 | 65 | err := protojson.Unmarshal([]byte(sqlData.String), pbMessage) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return &data, nil 71 | } 72 | 73 | // Value returns the driver.Valuer for the GoType. 74 | func (s ProtoValueScanner[T]) Value(data *T) (driver.Value, error) { 75 | return s.v(data) 76 | } 77 | 78 | // ScanValue returns a new ValueScanner that functions as an 79 | // intermediate result between database value and GoType value. 80 | // For example, sql.NullString or sql.NullInt. 81 | func (s ProtoValueScanner[T]) ScanValue() field.ValueScanner { 82 | return new(sql.NullString) 83 | } 84 | 85 | // FromValue returns the field instance from the ScanValue 86 | // above after the database value was scanned. 87 | func (s ProtoValueScanner[T]) FromValue(value driver.Value) (vt *T, err error) { 88 | switch v := value.(type) { 89 | case *sql.NullString: 90 | return s.s(v) 91 | case *T: 92 | return v, nil 93 | case *any: 94 | return s.s(FromPtrAny[*sql.NullString](v)) 95 | case any: 96 | vFromAny, _ := v.(*sql.NullString) 97 | return s.s(vFromAny) 98 | } 99 | 100 | str, ok := value.(*sql.NullString) 101 | if !ok { 102 | return vt, fmt.Errorf("unexpected input for FromValue: %T", value) 103 | } 104 | 105 | return s.s(str) 106 | } 107 | -------------------------------------------------------------------------------- /ent_test.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | 7 | "github.com/nekomeowww/xo/protobufs/testpb" 8 | "github.com/samber/lo" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "google.golang.org/protobuf/encoding/protojson" 12 | ) 13 | 14 | func TestProtoValueScanner(t *testing.T) { 15 | t.Run("NULL", func(t *testing.T) { 16 | scanner := ProtoValueScanner[testpb.TestMessage]{} 17 | require.NotNil(t, scanner) 18 | 19 | val, err := scanner.Value(nil) 20 | require.NoError(t, err) 21 | assert.Equal(t, sql.NullString{}, val) 22 | 23 | nullStringValue, ok := val.(sql.NullString) 24 | require.True(t, ok) 25 | require.False(t, nullStringValue.Valid) 26 | 27 | value, err := nullStringValue.Value() 28 | require.NoError(t, err) 29 | require.Nil(t, value) 30 | 31 | pb, err := scanner.FromValue(&sql.NullString{String: "", Valid: false}) 32 | require.NoError(t, err) 33 | require.Nil(t, pb) 34 | 35 | nullStringValue, ok = val.(sql.NullString) 36 | require.True(t, ok) 37 | require.False(t, nullStringValue.Valid) 38 | 39 | pb, err = scanner.FromValue(lo.ToPtr(nullStringValue)) 40 | require.NoError(t, err) 41 | require.Nil(t, pb) 42 | }) 43 | 44 | t.Run("NonNULL", func(t *testing.T) { 45 | original := &testpb.TestMessage{ 46 | Property_1: "Hello, World!", 47 | Property_2: "John Doe", 48 | OneofField: &testpb.TestMessage_PossibleOne{ 49 | PossibleOne: &testpb.PossibleOne{ 50 | Property_1: "Hello, World!", 51 | Property_2: "John Doe", 52 | }, 53 | }, 54 | } 55 | 56 | scanner := ProtoValueScanner[testpb.TestMessage]{} 57 | require.NotNil(t, scanner) 58 | 59 | val, err := scanner.Value(original) 60 | require.NoError(t, err) 61 | 62 | str, ok := val.(*sql.NullString) 63 | require.True(t, ok) 64 | require.NotEmpty(t, str) 65 | 66 | bytes, err := protojson.Marshal(original) 67 | require.NoError(t, err) 68 | assert.Equal(t, string(bytes), str.String) 69 | 70 | pb, err := scanner.FromValue(&sql.NullString{String: str.String, Valid: true}) 71 | require.NoError(t, err) 72 | require.NotNil(t, pb) 73 | 74 | assert.Equal(t, original, pb) 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /env.go: -------------------------------------------------------------------------------- 1 | //go:build !release 2 | 3 | package xo 4 | 5 | import ( 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // IsInTestEnvironment determines whether the current environment is a test environment. 11 | func IsInTestEnvironment() bool { 12 | for _, arg := range os.Args { 13 | if strings.HasPrefix(arg, "-test.") { 14 | return true 15 | } 16 | } 17 | 18 | return false 19 | } 20 | -------------------------------------------------------------------------------- /env_test.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestIsInTestEnvironment(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | originalOSArgs := os.Args 14 | defer func() { 15 | os.Args = originalOSArgs 16 | }() 17 | 18 | os.Args = []string{"-test."} 19 | 20 | assert.True(IsInTestEnvironment()) 21 | 22 | os.Args = []string{"-v"} 23 | 24 | assert.False(IsInTestEnvironment()) 25 | } 26 | -------------------------------------------------------------------------------- /exp/README.md: -------------------------------------------------------------------------------- 1 | # xo/exp 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/nekomeowww/xo/exp.svg)](https://pkg.go.dev/github.com/nekomeowww/xo/exp) 4 | 5 | 🧪 Experimental coding patterns, designs 6 | 7 | > **Warning** 8 | > 9 | > This package is **meant to be used as a playground** for new ideas and patterns to be tested and refined, **therefore the API is not guaranteed to be stable**. 10 | > However, the codes are well tested and documented, if you think it's useful, feel free to use it in any way you want, even in production. 11 | 12 | ## 🤠 Spec 13 | 14 | GoDoc: [https://godoc.org/github.com/nekomeowww/xo/exp](https://godoc.org/github.com/nekomeowww/xo/exp) 15 | 16 | ## Experimental 17 | 18 | Channel helpers: 19 | 20 | - [`channelx.ChannelPuller`](https://pkg.go.dev/github.com/nekomeowww/xo@v1.0.0/exp/channelx#ChannelPuller) 21 | -------------------------------------------------------------------------------- /exp/channelx/example_test.go: -------------------------------------------------------------------------------- 1 | package channelx_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "sync" 8 | "time" 9 | 10 | "github.com/nekomeowww/xo/exp/channelx" 11 | "github.com/sourcegraph/conc" 12 | "github.com/sourcegraph/conc/panics" 13 | ) 14 | 15 | func ExamplePuller() { 16 | // Note that itemChan is un-buffered. 17 | itemChan := make(chan int) 18 | defer close(itemChan) 19 | 20 | wg := conc.NewWaitGroup() 21 | // Send 10 items to itemChan 22 | wg.Go(func() { 23 | for i := 0; i < 10; i++ { 24 | input := i 25 | // Since itemChan is un-buffered, this line will block until the item is pulled. 26 | itemChan <- input 27 | } 28 | }) 29 | 30 | handledItems := make([]int, 10) 31 | handlerFunc := func(item int) { 32 | // Simulate a time-consuming operation since we want to test 33 | // the max goroutine and handle the items asynchronously. 34 | time.Sleep(time.Millisecond * 100) 35 | // Pump the handled items. 36 | handledItems[item] = item 37 | } 38 | 39 | // Create a puller to pull items from itemChan and assign handlerFunc to handle the items. 40 | puller := channelx.NewPuller[int](). 41 | WithNotifyChannel(itemChan). 42 | WithHandler(handlerFunc). 43 | // Create a new worker pool with the size set the max goroutine to 10 internally 44 | // to handle the items asynchronously and elegantly. 45 | WithHandleAsynchronouslyMaxGoroutine(10). 46 | StartPull(context.Background()) 47 | 48 | // Wait for all items to be sent to itemChan. (which is picked by puller) 49 | wg.Wait() 50 | // Wait for the last item to be handled. 51 | time.Sleep(time.Millisecond*100 + time.Millisecond*20) 52 | 53 | // Let's print out the handled items. 54 | fmt.Println(handledItems) 55 | 56 | // You may want to stop pulling items from itemChan when 57 | // you don't want to pull items anymore. 58 | err := puller.StopPull(context.Background()) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | 63 | // Output: 64 | // [0 1 2 3 4 5 6 7 8 9] 65 | } 66 | 67 | func ExamplePuller_StartPull() { 68 | // Note that itemChan is un-buffered. 69 | itemChan := make(chan int) 70 | defer close(itemChan) 71 | 72 | wg := conc.NewWaitGroup() 73 | // Send 10 items to itemChan 74 | wg.Go(func() { 75 | for i := 0; i < 10; i++ { 76 | input := i 77 | // Since itemChan is un-buffered, this line will block until the item is pulled. 78 | itemChan <- input 79 | } 80 | }) 81 | 82 | var handledItemsMutex sync.Mutex 83 | handledItems := make([]int, 0) 84 | handlerFunc := func(item int) { 85 | handledItemsMutex.Lock() 86 | defer handledItemsMutex.Unlock() 87 | 88 | handledItems = append(handledItems, item) 89 | } 90 | 91 | // Create a puller to pull items from itemChan and assign handlerFunc to handle the items. 92 | puller := channelx.NewPuller[int](). 93 | WithNotifyChannel(itemChan). 94 | WithHandler(handlerFunc). 95 | StartPull(context.Background()) 96 | 97 | // Wait for all items to be sent to itemChan (which is picked by puller). 98 | wg.Wait() 99 | // Wait for the last item to be handled. 100 | time.Sleep(time.Millisecond) 101 | 102 | // Let's print out the handled items. 103 | // Since we didn't specify the puller to handle items asynchronously, 104 | // the handled items should be in order just the same as the items sent to itemChan 105 | // even though we use `append(...)` to modify the slice. 106 | fmt.Println(handledItems) 107 | 108 | // You may want to stop pulling items from itemChan when 109 | // you don't want to pull items anymore. 110 | err := puller.StopPull(context.Background()) 111 | if err != nil { 112 | log.Fatal(err) 113 | } 114 | 115 | // Output: 116 | // [0 1 2 3 4 5 6 7 8 9] 117 | } 118 | 119 | func ExamplePuller_WithHandleAsynchronouslyMaxGoroutine() { 120 | // Note that itemChan is un-buffered. 121 | itemChan := make(chan int) 122 | defer close(itemChan) 123 | 124 | wg := conc.NewWaitGroup() 125 | // Send 10 items to itemChan 126 | wg.Go(func() { 127 | for i := 0; i < 10; i++ { 128 | input := i 129 | // Since itemChan is un-buffered, this line will block until the item is pulled. 130 | itemChan <- input 131 | } 132 | }) 133 | 134 | handledItems := make([]int, 10) 135 | handlerFunc := func(item int) { 136 | // Simulate a time-consuming operation since we want to test 137 | // the max goroutine and handle the items asynchronously. 138 | time.Sleep(time.Millisecond * 100) 139 | // Pump the handled items. 140 | handledItems[item] = item 141 | } 142 | 143 | // Create a puller to pull items from itemChan and assign handlerFunc to handle the items. 144 | puller := channelx.NewPuller[int](). 145 | WithNotifyChannel(itemChan). 146 | WithHandler(handlerFunc). 147 | // Create a new worker pool with the size set the max goroutine to 10 internally 148 | // to handle the items asynchronously and elegantly. 149 | WithHandleAsynchronouslyMaxGoroutine(10). 150 | StartPull(context.Background()) 151 | 152 | // Wait for all items to be sent to itemChan. (which is picked by puller) 153 | wg.Wait() 154 | // Wait for the last item to be handled. 155 | time.Sleep(time.Millisecond*100 + time.Millisecond*20) 156 | 157 | // Let's print out the handled items. 158 | fmt.Println(handledItems) 159 | 160 | // You may want to stop pulling items from itemChan when 161 | // you don't want to pull items anymore. 162 | err := puller.StopPull(context.Background()) 163 | if err != nil { 164 | log.Fatal(err) 165 | } 166 | 167 | // Output: 168 | // [0 1 2 3 4 5 6 7 8 9] 169 | } 170 | 171 | func ExamplePuller_WithHandleAsynchronously() { 172 | // Note that itemChan is un-buffered. 173 | itemChan := make(chan int) 174 | defer close(itemChan) 175 | 176 | wg := conc.NewWaitGroup() 177 | // Send 10 items to itemChan 178 | wg.Go(func() { 179 | for i := 0; i < 10; i++ { 180 | input := i 181 | // Since itemChan is un-buffered, this line will block until the item is pulled. 182 | itemChan <- input 183 | } 184 | }) 185 | 186 | handledItems := make([]int, 10) 187 | handlerFunc := func(item int) { 188 | // Simulate a time-consuming operation since we want to test 189 | // the max goroutine and handle the items asynchronously. 190 | time.Sleep(time.Millisecond * 100) 191 | // Pump the handled items. 192 | handledItems[item] = item 193 | } 194 | 195 | // Create a puller to pull items from itemChan and assign handlerFunc to handle the items. 196 | puller := channelx.NewPuller[int](). 197 | WithNotifyChannel(itemChan). 198 | WithHandler(handlerFunc). 199 | // Handle the items asynchronously without a worker pool. 200 | WithHandleAsynchronously(). 201 | StartPull(context.Background()) 202 | 203 | // Wait for all items to be sent to itemChan. (which is picked by puller) 204 | wg.Wait() 205 | // Wait for the last item to be handled. 206 | time.Sleep(time.Millisecond*100 + time.Millisecond*20) 207 | 208 | // Let's print out the handled items. 209 | fmt.Println(handledItems) 210 | 211 | // You may want to stop pulling items from itemChan when 212 | // you don't want to pull items anymore. 213 | err := puller.StopPull(context.Background()) 214 | if err != nil { 215 | log.Fatal(err) 216 | } 217 | 218 | // Output: 219 | // [0 1 2 3 4 5 6 7 8 9] 220 | } 221 | 222 | func ExamplePuller_WithPanicHandler() { 223 | // Note that itemChan is un-buffered. 224 | itemChan := make(chan int) 225 | defer close(itemChan) 226 | 227 | wg := conc.NewWaitGroup() 228 | // Send 10 items to itemChan 229 | wg.Go(func() { 230 | for i := 0; i < 10; i++ { 231 | input := i 232 | // Since itemChan is un-buffered, this line will block until the item is pulled. 233 | itemChan <- input 234 | } 235 | }) 236 | 237 | handledItems := make([]int, 10) 238 | handlerFunc := func(item int) { 239 | if item == 9 { 240 | panic("panicked on item 9") 241 | } 242 | 243 | // Simulate a time-consuming operation since we want to test 244 | // the max goroutine and handle the items asynchronously. 245 | time.Sleep(time.Millisecond * 100) 246 | // Pump the handled items. 247 | handledItems[item] = item 248 | } 249 | 250 | panicHandlerFunc := func(panicValue *panics.Recovered) { 251 | fmt.Println(panicValue.Value) 252 | } 253 | 254 | // Create a puller to pull items from itemChan and assign handlerFunc to handle the items. 255 | puller := channelx.NewPuller[int](). 256 | WithNotifyChannel(itemChan). 257 | WithHandler(handlerFunc). 258 | // Assign panicHandlerFunc to handle the panic. 259 | WithPanicHandler(panicHandlerFunc). 260 | // Handle the items asynchronously without a worker pool. 261 | WithHandleAsynchronously(). 262 | StartPull(context.Background()) 263 | 264 | // Wait for all items to be sent to itemChan. (which is picked by puller) 265 | wg.Wait() 266 | // Wait for the last item to be handled. 267 | time.Sleep(time.Millisecond*100 + time.Millisecond*20) 268 | 269 | // Let's print out the handled items. 270 | fmt.Println(handledItems) 271 | 272 | // You may want to stop pulling items from itemChan when 273 | // you don't want to pull items anymore. 274 | err := puller.StopPull(context.Background()) 275 | if err != nil { 276 | log.Fatal(err) 277 | } 278 | 279 | // Output: 280 | // panicked on item 9 281 | // [0 1 2 3 4 5 6 7 8 0] 282 | } 283 | -------------------------------------------------------------------------------- /exp/channelx/puller.go: -------------------------------------------------------------------------------- 1 | package channelx 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/nekomeowww/fo" 8 | "github.com/sourcegraph/conc/panics" 9 | "github.com/sourcegraph/conc/pool" 10 | ) 11 | 12 | // Puller is a generic long-running puller to pull items from a channel. 13 | type Puller[T any] struct { 14 | notifyChan <-chan T 15 | tickerChan <-chan time.Time 16 | ticker *time.Ticker 17 | 18 | updateFromFunc func(time.Time) T 19 | updateHandlerFunc func(item T) (shouldContinue, shouldReturn bool) 20 | updateHandleAsynchronously bool 21 | updateHandlePool *pool.Pool 22 | panicHandlerFunc func(panicValue *panics.Recovered) 23 | 24 | alreadyStarted bool 25 | alreadyClosed bool 26 | contextCancelFunc context.CancelFunc 27 | } 28 | 29 | // New creates a new long-running puller to pull items. 30 | func NewPuller[T any]() *Puller[T] { 31 | return new(Puller[T]) 32 | } 33 | 34 | // WithChannel assigns channel to pull items from. 35 | func (p *Puller[T]) WithNotifyChannel(updateChan <-chan T) *Puller[T] { 36 | p.notifyChan = updateChan 37 | 38 | return p 39 | } 40 | 41 | // WithTickerChannel assigns channel to pull items from with a ticker. 42 | func (p *Puller[T]) WithTickerChannel(tickerChan <-chan time.Time, pullFromFunc func(time.Time) T) *Puller[T] { 43 | p.tickerChan = tickerChan 44 | p.updateFromFunc = pullFromFunc 45 | 46 | return p 47 | } 48 | 49 | // WithTickerInterval assigns ticker interval to pull items from with a ticker. 50 | func (p *Puller[T]) WithTickerInterval(interval time.Duration, pullFromFunc func(time.Time) T) *Puller[T] { 51 | p.ticker = time.NewTicker(interval) 52 | p.tickerChan = p.ticker.C 53 | p.updateFromFunc = pullFromFunc 54 | 55 | return p 56 | } 57 | 58 | // WithHandler assigns handler to handle the items pulled from the channel. 59 | func (p *Puller[T]) WithHandler(handler func(item T)) *Puller[T] { 60 | p.updateHandlerFunc = func(item T) (bool, bool) { 61 | handler(item) 62 | return false, false 63 | } 64 | 65 | return p 66 | } 67 | 68 | // WithHandlerWithShouldContinue assigns handler to handle the items pulled from the channel but 69 | // the handler can return a bool to indicate whether the puller should skip the current for loop 70 | // iteration and continue to move on to the next iteration. 71 | // 72 | // NOTICE: If the puller has been set to handle the items asynchronously, therefore the 73 | // shouldContinue boolean value that the handler returns will be ignored. 74 | func (p *Puller[T]) WithHandlerWithShouldContinue(handler func(item T) bool) *Puller[T] { 75 | p.updateHandlerFunc = func(item T) (bool, bool) { 76 | return handler(item), false 77 | } 78 | 79 | return p 80 | } 81 | 82 | // WithHandlerWithShouldReturn assigns handler to handle the items pulled from the channel but 83 | // the handler can return a bool to indicate whether the puller should stop pulling items. 84 | // 85 | // NOTICE: If the puller has been set to handle the items asynchronously, therefore the 86 | // shouldReturn boolean value that the handler returns will be ignored. 87 | func (p *Puller[T]) WithHandlerWithShouldReturn(handler func(item T) bool) *Puller[T] { 88 | p.updateHandlerFunc = func(item T) (bool, bool) { 89 | return false, handler(item) 90 | } 91 | 92 | return p 93 | } 94 | 95 | // WithHandlerWithShouldContinueAndShouldReturn assigns handler to handle the items pulled from the channel but 96 | // the handler can return two bool values to indicate whether the puller should stop pulling items and 97 | // whether the puller should skip the current for loop iteration and continue to move on to the next iteration. 98 | // 99 | // NOTICE: If the puller has been set to handle the items asynchronously, therefore the 100 | // shouldContinue and shouldReturn boolean values that the handler returns will be ignored. 101 | func (p *Puller[T]) WithHandlerWithShouldContinueAndShouldReturn(handler func(item T) (shouldBreak, shouldContinue bool)) *Puller[T] { 102 | p.updateHandlerFunc = handler 103 | 104 | return p 105 | } 106 | 107 | // WithHandleAsynchronously makes the handler to be handled asynchronously. 108 | func (p *Puller[T]) WithHandleAsynchronously() *Puller[T] { 109 | p.updateHandleAsynchronously = true 110 | 111 | return p 112 | } 113 | 114 | // WithHandleAsynchronouslyMaxGoroutine makes the handler to be handled asynchronously with a worker pool that 115 | // the size of the pool set to maxGoroutine. This is useful when you want to limit the number of goroutines 116 | // that handle the items to prevent the goroutines from consuming too much memory when lots of items are pumped 117 | // to the channel (or request). 118 | func (p *Puller[T]) WithHandleAsynchronouslyMaxGoroutine(maxGoroutine int) *Puller[T] { 119 | p.WithHandleAsynchronously() 120 | 121 | p.updateHandlePool = pool.New().WithMaxGoroutines(maxGoroutine) 122 | 123 | return p 124 | } 125 | 126 | // WithPanicHandler assigns panic handler to handle the panic that the handlerFunc panics. 127 | func (p *Puller[T]) WithPanicHandler(handlerFunc func(panicValue *panics.Recovered)) *Puller[T] { 128 | p.panicHandlerFunc = handlerFunc 129 | 130 | return p 131 | } 132 | 133 | // StartPull starts pulling items from the channel. You may pass a context to signal the puller to stop pulling 134 | // items from the channel. 135 | func (c *Puller[T]) StartPull(ctx context.Context) *Puller[T] { 136 | if c.alreadyStarted { 137 | return c 138 | } 139 | 140 | c.alreadyStarted = true 141 | if c.tickerChan != nil { 142 | c.contextCancelFunc = runWithTicker( 143 | ctx, 144 | c.updateHandleAsynchronously, 145 | c.updateHandlePool, 146 | c.tickerChan, 147 | c.updateFromFunc, 148 | c.updateHandlerFunc, 149 | c.panicHandlerFunc, 150 | ) 151 | 152 | return c 153 | } 154 | if c.notifyChan != nil { 155 | c.contextCancelFunc = run( 156 | ctx, 157 | c.updateHandleAsynchronously, 158 | c.updateHandlePool, 159 | c.notifyChan, 160 | c.updateHandlerFunc, 161 | c.panicHandlerFunc, 162 | ) 163 | 164 | return c 165 | } 166 | 167 | c.contextCancelFunc = func() {} 168 | 169 | return c 170 | } 171 | 172 | // StopPull stops pulling items from the channel. You may pass a context to restrict the deadline or 173 | // call timeout to the action to stop the puller. 174 | func (c *Puller[T]) StopPull(ctx context.Context) error { 175 | if c.alreadyClosed { 176 | return nil 177 | } 178 | 179 | c.alreadyClosed = true 180 | if c.ticker != nil { 181 | c.ticker.Stop() 182 | } 183 | if c.contextCancelFunc != nil { 184 | return fo.Invoke0(ctx, func() error { 185 | c.contextCancelFunc() 186 | 187 | return nil 188 | }) 189 | } 190 | 191 | return nil 192 | } 193 | 194 | func runHandle[T any]( 195 | item T, 196 | updateHandleAsynchronously bool, 197 | updateHandlePool *pool.Pool, 198 | handlerFunc func(item T) (shouldContinue, shouldReturn bool), 199 | panicHandlerFunc func(panicValue *panics.Recovered), 200 | ) (bool, bool) { 201 | if handlerFunc == nil { 202 | return false, false 203 | } 204 | 205 | if updateHandleAsynchronously { 206 | runInGoroutine := func() { 207 | var pc panics.Catcher 208 | 209 | pc.Try(func() { 210 | _, _ = handlerFunc(item) 211 | }) 212 | 213 | if pc.Recovered() != nil && panicHandlerFunc != nil { 214 | panicHandlerFunc(pc.Recovered()) 215 | } 216 | } 217 | 218 | if updateHandlePool != nil { 219 | updateHandlePool.Go(runInGoroutine) 220 | return false, false 221 | } 222 | 223 | go runInGoroutine() 224 | 225 | return false, false 226 | } 227 | 228 | return handlerFunc(item) 229 | } 230 | 231 | func run[T any]( 232 | ctx context.Context, 233 | updateHandleAsynchronously bool, 234 | updateHandlePool *pool.Pool, 235 | notifyChannel <-chan T, 236 | handlerFunc func(item T) (shouldContinue, shouldBreak bool), 237 | panicHandlerFunc func(panicValue *panics.Recovered), 238 | ) context.CancelFunc { 239 | ctx, cancel := context.WithCancel(ctx) 240 | 241 | go func() { 242 | for { 243 | select { 244 | case <-ctx.Done(): 245 | return 246 | case item, ok := <-notifyChannel: 247 | if !ok { 248 | break 249 | } 250 | 251 | shouldContinue, shouldReturn := runHandle( //nolint: staticcheck 252 | item, 253 | updateHandleAsynchronously, 254 | updateHandlePool, 255 | handlerFunc, 256 | panicHandlerFunc, 257 | ) 258 | if shouldReturn { 259 | return 260 | } 261 | if shouldContinue { 262 | continue 263 | } 264 | } 265 | } 266 | }() 267 | 268 | return cancel 269 | } 270 | 271 | func runWithTicker[T any]( 272 | ctx context.Context, 273 | updateHandleAsynchronously bool, 274 | updateHandlePool *pool.Pool, 275 | tickerChannel <-chan time.Time, 276 | fromFunc func(time.Time) T, 277 | handlerFunc func(item T) (shouldContinue, shouldBreak bool), 278 | panicHandlerFunc func(panicValue *panics.Recovered), 279 | ) context.CancelFunc { 280 | ctx, cancel := context.WithCancel(ctx) 281 | 282 | go func() { 283 | for { 284 | select { 285 | case <-ctx.Done(): 286 | return 287 | case <-tickerChannel: 288 | item := fromFunc(time.Now()) 289 | 290 | shouldContinue, shouldReturn := runHandle( //nolint: staticcheck 291 | item, 292 | updateHandleAsynchronously, 293 | updateHandlePool, 294 | handlerFunc, 295 | panicHandlerFunc, 296 | ) 297 | if shouldReturn { 298 | return 299 | } 300 | if shouldContinue { 301 | continue 302 | } 303 | } 304 | } 305 | }() 306 | 307 | return cancel 308 | } 309 | -------------------------------------------------------------------------------- /exp/channelx/puller_test.go: -------------------------------------------------------------------------------- 1 | package channelx 2 | 3 | import ( 4 | "context" 5 | "runtime" 6 | "testing" 7 | "time" 8 | 9 | "github.com/sourcegraph/conc" 10 | "github.com/sourcegraph/conc/panics" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestPuller_WithNotifyChannel(t *testing.T) { 16 | t.Parallel() 17 | 18 | t.Run("WithoutHandler", func(t *testing.T) { 19 | t.Parallel() 20 | 21 | itemChan := make(chan int) 22 | defer close(itemChan) 23 | 24 | puller := NewPuller[int]().WithNotifyChannel(itemChan) 25 | puller.StartPull(context.Background()) 26 | 27 | err := puller.StopPull(context.Background()) 28 | require.NoError(t, err) 29 | }) 30 | 31 | t.Run("WithHandler", func(t *testing.T) { 32 | t.Parallel() 33 | 34 | itemChan := make(chan int) 35 | defer close(itemChan) 36 | 37 | wg := conc.NewWaitGroup() 38 | wg.Go(func() { 39 | for i := 0; i < 10; i++ { 40 | input := i 41 | itemChan <- input 42 | } 43 | }) 44 | 45 | handledItems := make([]int, 0) 46 | handlerFunc := func(item int) { 47 | time.Sleep(time.Millisecond * 100) 48 | handledItems = append(handledItems, item) 49 | } 50 | 51 | puller := NewPuller[int](). 52 | WithNotifyChannel(itemChan). 53 | WithHandler(handlerFunc) 54 | puller.StartPull(context.Background()) 55 | 56 | // wait for all items to be sent to itemChan. (which is picked by puller) 57 | wg.Wait() 58 | // wait for the last item to be handled. 59 | time.Sleep(time.Millisecond*100 + time.Millisecond*10) 60 | 61 | err := puller.StopPull(context.Background()) 62 | require.NoError(t, err) 63 | 64 | assert.Len(t, handledItems, 10) 65 | assert.ElementsMatch(t, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, handledItems) 66 | }) 67 | 68 | t.Run("WithHandlerWithShouldReturn", func(t *testing.T) { 69 | t.Parallel() 70 | 71 | itemChan := make(chan int, 10) 72 | defer close(itemChan) 73 | 74 | wg := conc.NewWaitGroup() 75 | wg.Go(func() { 76 | for i := 0; i < 10; i++ { 77 | input := i 78 | itemChan <- input 79 | } 80 | }) 81 | 82 | handledItems := make([]int, 5) 83 | handlerFunc := func(item int) (shouldReturn bool) { 84 | time.Sleep(time.Millisecond * 100) 85 | handledItems[item] = item 86 | 87 | return item == 4 88 | } 89 | 90 | puller := NewPuller[int](). 91 | WithNotifyChannel(itemChan). 92 | WithHandlerWithShouldReturn(handlerFunc) 93 | puller.StartPull(context.Background()) 94 | 95 | // wait for all items to be sent to itemChan. (which is picked by puller) 96 | wg.Wait() 97 | // wait for the last item to be handled. 98 | time.Sleep(5*time.Millisecond*100 + time.Millisecond*10) 99 | 100 | err := puller.StopPull(context.Background()) 101 | require.NoError(t, err) 102 | 103 | assert.Len(t, handledItems, 5) 104 | assert.ElementsMatch(t, []int{0, 1, 2, 3, 4}, handledItems) 105 | }) 106 | 107 | t.Run("WithHandlerWithShouldContinue", func(t *testing.T) { 108 | t.Parallel() 109 | 110 | itemChan := make(chan int) 111 | defer close(itemChan) 112 | 113 | wg := conc.NewWaitGroup() 114 | wg.Go(func() { 115 | for i := 0; i < 10; i++ { 116 | input := i 117 | itemChan <- input 118 | } 119 | }) 120 | 121 | handledItems := make([]int, 10) 122 | handlerFunc := func(item int) (shouldContinue bool) { 123 | if item%2 == 0 { 124 | return true 125 | } 126 | 127 | time.Sleep(time.Millisecond * 100) 128 | handledItems[item] = item 129 | 130 | return false 131 | } 132 | 133 | puller := NewPuller[int](). 134 | WithNotifyChannel(itemChan). 135 | WithHandlerWithShouldContinue(handlerFunc) 136 | puller.StartPull(context.Background()) 137 | 138 | // wait for all items to be sent to itemChan. (which is picked by puller) 139 | wg.Wait() 140 | // wait for the last item to be handled. 141 | time.Sleep(time.Millisecond*100 + time.Millisecond*10) 142 | 143 | err := puller.StopPull(context.Background()) 144 | require.NoError(t, err) 145 | 146 | assert.Len(t, handledItems, 10) 147 | assert.ElementsMatch(t, []int{0, 1, 0, 3, 0, 5, 0, 7, 0, 9}, handledItems) 148 | }) 149 | 150 | t.Run("WithHandlerWithShouldContinueAndShouldReturn", func(t *testing.T) { 151 | t.Parallel() 152 | 153 | // since the handler will no longer handle items after 7, we need to make sure that the channel is not full. 154 | itemChan := make(chan int, 3) 155 | defer close(itemChan) 156 | 157 | wg := conc.NewWaitGroup() 158 | wg.Go(func() { 159 | for i := 0; i < 10; i++ { 160 | input := i 161 | itemChan <- input 162 | } 163 | }) 164 | 165 | handledItems := make([]int, 10) 166 | handlerFunc := func(item int) (shouldContinue, shouldReturn bool) { 167 | if item%2 == 0 { 168 | return true, false // skip even items 169 | } 170 | 171 | time.Sleep(time.Millisecond * 100) 172 | handledItems[item] = item 173 | 174 | return false, item == 7 // stop at 7 175 | } 176 | 177 | puller := NewPuller[int](). 178 | WithNotifyChannel(itemChan). 179 | WithHandlerWithShouldContinueAndShouldReturn(handlerFunc) 180 | puller.StartPull(context.Background()) 181 | 182 | // wait for all items to be sent to itemChan. (which is picked by puller) 183 | wg.Wait() 184 | // wait for the last item to be handled. 185 | time.Sleep(time.Millisecond*100 + time.Millisecond*10) 186 | 187 | err := puller.StopPull(context.Background()) 188 | require.NoError(t, err) 189 | 190 | assert.Len(t, handledItems, 10) 191 | assert.ElementsMatch(t, []int{0, 1, 0, 3, 0, 5, 0, 7, 0, 0}, handledItems) 192 | }) 193 | 194 | t.Run("WithHandleAsynchronously", func(t *testing.T) { 195 | t.Parallel() 196 | 197 | itemChan := make(chan int) 198 | defer close(itemChan) 199 | 200 | wg := conc.NewWaitGroup() 201 | wg.Go(func() { 202 | for i := 0; i < 10; i++ { 203 | input := i 204 | itemChan <- input 205 | } 206 | }) 207 | 208 | handledItems := make([]int, 10) 209 | handlerFunc := func(item int) { 210 | time.Sleep(time.Millisecond * 100) 211 | handledItems[item] = item 212 | } 213 | 214 | puller := NewPuller[int](). 215 | WithNotifyChannel(itemChan). 216 | WithHandler(handlerFunc). 217 | WithHandleAsynchronously() 218 | 219 | puller.StartPull(context.Background()) 220 | 221 | // wait for all items to be sent to itemChan. (which is picked by puller) 222 | wg.Wait() 223 | // wait for the last item to be handled. 224 | time.Sleep(time.Millisecond*100 + time.Millisecond*10) 225 | 226 | err := puller.StopPull(context.Background()) 227 | require.NoError(t, err) 228 | 229 | assert.Len(t, handledItems, 10) 230 | assert.ElementsMatch(t, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, handledItems) 231 | }) 232 | 233 | t.Run("WithHandleAsynchronouslyMaxGoroutine", func(t *testing.T) { 234 | t.Parallel() 235 | 236 | itemChan := make(chan int) 237 | defer close(itemChan) 238 | 239 | wg := conc.NewWaitGroup() 240 | wg.Go(func() { 241 | for i := 0; i < 10; i++ { 242 | input := i 243 | itemChan <- input 244 | } 245 | }) 246 | 247 | handledItems := make([]int, 10) 248 | handlerFunc := func(item int) { 249 | time.Sleep(time.Millisecond * 100) 250 | handledItems[item] = item 251 | } 252 | 253 | puller := NewPuller[int](). 254 | WithNotifyChannel(itemChan). 255 | WithHandler(handlerFunc). 256 | WithHandleAsynchronouslyMaxGoroutine(5) 257 | 258 | now := time.Now() 259 | 260 | puller.StartPull(context.Background()) 261 | 262 | // wait for all items to be sent to itemChan. (which is picked by puller) 263 | wg.Wait() 264 | // wait for the last item to be handled. 265 | time.Sleep(time.Millisecond*100 + time.Millisecond*10) 266 | 267 | elapsed := time.Since(now) 268 | assert.True(t, elapsed > time.Millisecond*100*2) 269 | 270 | err := puller.StopPull(context.Background()) 271 | require.NoError(t, err) 272 | 273 | assert.Equal(t, 10, len(handledItems)) 274 | assert.ElementsMatch(t, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, handledItems) 275 | }) 276 | 277 | t.Run("WithPanicHandler", func(t *testing.T) { 278 | t.Parallel() 279 | 280 | itemChan := make(chan int) 281 | defer close(itemChan) 282 | 283 | wg := conc.NewWaitGroup() 284 | wg.Go(func() { 285 | itemChan <- 1 286 | }) 287 | 288 | handlerFunc := func(item int) { 289 | panic("panic") 290 | } 291 | 292 | var panicValue *panics.Recovered 293 | panicHandlerFunc := func(recovered *panics.Recovered) { 294 | panicValue = recovered 295 | } 296 | 297 | puller := NewPuller[int](). 298 | WithNotifyChannel(itemChan). 299 | WithHandler(handlerFunc). 300 | WithHandleAsynchronously(). 301 | WithPanicHandler(panicHandlerFunc) 302 | 303 | puller.StartPull(context.Background()) 304 | 305 | // wait for all items to be sent to itemChan. (which is picked by puller) 306 | wg.Wait() 307 | // wait for the last item to be handled. 308 | time.Sleep(time.Millisecond) 309 | 310 | err := puller.StopPull(context.Background()) 311 | require.NoError(t, err) 312 | 313 | require.NotNil(t, panicValue) 314 | assert.Equal(t, "panic", panicValue.Value) 315 | 316 | funcObj := runtime.FuncForPC(panicValue.Callers[2]) 317 | assert.Equal(t, "github.com/nekomeowww/xo/exp/channelx.TestPuller_WithNotifyChannel.func8.2", funcObj.Name()) 318 | }) 319 | 320 | t.Run("StartPullCalledTwice", func(t *testing.T) { 321 | t.Parallel() 322 | 323 | itemChan := make(chan int) 324 | defer close(itemChan) 325 | 326 | wg := conc.NewWaitGroup() 327 | wg.Go(func() { 328 | for i := 0; i < 10; i++ { 329 | input := i 330 | itemChan <- input 331 | } 332 | }) 333 | 334 | handledItems := make([]int, 10) 335 | handleFunc := func(item int) { 336 | time.Sleep(time.Millisecond * 100) 337 | handledItems[item] = item 338 | } 339 | 340 | puller := NewPuller[int](). 341 | WithNotifyChannel(itemChan). 342 | WithHandler(handleFunc). 343 | WithHandleAsynchronously() 344 | 345 | puller.StartPull(context.Background()) 346 | puller.StartPull(context.Background()) // should be ignored 347 | 348 | // wait for all items to be sent to itemChan. (which is picked by puller) 349 | wg.Wait() 350 | // wait for the last item to be handled. 351 | time.Sleep(time.Millisecond*100 + time.Millisecond*10) 352 | 353 | err := puller.StopPull(context.Background()) 354 | require.NoError(t, err) 355 | 356 | assert.Len(t, handledItems, 10) 357 | assert.ElementsMatch(t, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, handledItems) 358 | }) 359 | 360 | t.Run("StopPullCalledTwice", func(t *testing.T) { 361 | t.Parallel() 362 | 363 | itemChan := make(chan int) 364 | defer close(itemChan) 365 | 366 | wg := conc.NewWaitGroup() 367 | wg.Go(func() { 368 | for i := 0; i < 10; i++ { 369 | input := i 370 | itemChan <- input 371 | } 372 | }) 373 | 374 | handledItems := make([]int, 10) 375 | handleFunc := func(item int) { 376 | time.Sleep(time.Millisecond * 100) 377 | handledItems[item] = item 378 | } 379 | 380 | puller := NewPuller[int](). 381 | WithNotifyChannel(itemChan). 382 | WithHandler(handleFunc). 383 | WithHandleAsynchronously() 384 | 385 | puller.StartPull(context.Background()) 386 | 387 | // wait for all items to be sent to itemChan. (which is picked by puller) 388 | wg.Wait() 389 | // wait for the last item to be handled. 390 | time.Sleep(time.Millisecond*100 + time.Millisecond*10) 391 | 392 | err := puller.StopPull(context.Background()) 393 | require.NoError(t, err) 394 | 395 | err = puller.StopPull(context.Background()) // should be ignored 396 | require.NoError(t, err) 397 | 398 | assert.Len(t, handledItems, 10) 399 | assert.ElementsMatch(t, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, handledItems) 400 | }) 401 | 402 | t.Run("CancelFuncEmpty", func(t *testing.T) { 403 | t.Parallel() 404 | 405 | itemChan := make(chan int) 406 | 407 | handledItems := make([]int, 0) 408 | 409 | puller := NewPuller[int](). 410 | WithNotifyChannel(itemChan). 411 | WithHandler(func(item int) { 412 | handledItems = append(handledItems, item) 413 | }) 414 | 415 | puller.StartPull(context.Background()) 416 | puller.contextCancelFunc = nil 417 | 418 | close(itemChan) 419 | 420 | err := puller.StopPull(context.Background()) 421 | require.NoError(t, err) 422 | 423 | assert.Empty(t, handledItems) 424 | }) 425 | } 426 | 427 | func TestPuller_WithTickerChannel(t *testing.T) { 428 | t.Parallel() 429 | 430 | t.Run("WithoutHandler", func(t *testing.T) { 431 | t.Parallel() 432 | 433 | ticker := time.NewTicker(time.Millisecond * 100) 434 | defer ticker.Stop() 435 | 436 | index := -1 437 | puller := NewPuller[int](). 438 | WithTickerChannel(ticker.C, func(_ time.Time) int { 439 | index++ 440 | return index 441 | }) 442 | 443 | puller.StartPull(context.Background()) 444 | 445 | err := puller.StopPull(context.Background()) 446 | require.NoError(t, err) 447 | }) 448 | 449 | t.Run("WithHandler", func(t *testing.T) { 450 | t.Parallel() 451 | 452 | ticker := time.NewTicker(time.Millisecond * 10) 453 | defer ticker.Stop() 454 | 455 | handledItems := make([]int, 0) 456 | handlerFunc := func(item int) { 457 | time.Sleep(time.Millisecond * 100) 458 | handledItems = append(handledItems, item) 459 | } 460 | 461 | index := -1 462 | updateFromFunc := func(_ time.Time) int { 463 | index++ 464 | return index 465 | } 466 | 467 | puller := NewPuller[int](). 468 | WithTickerChannel(ticker.C, updateFromFunc). 469 | WithHandler(handlerFunc) 470 | puller.StartPull(context.Background()) 471 | 472 | // wait for the last item to be handled. 473 | time.Sleep(time.Millisecond*100*10 + time.Millisecond*100) 474 | 475 | err := puller.StopPull(context.Background()) 476 | require.NoError(t, err) 477 | 478 | assert.Len(t, handledItems, 10) 479 | assert.ElementsMatch(t, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, handledItems) 480 | }) 481 | 482 | t.Run("WithHandlerWithShouldReturn", func(t *testing.T) { 483 | t.Parallel() 484 | 485 | ticker := time.NewTicker(time.Millisecond * 10) 486 | defer ticker.Stop() 487 | 488 | handledItems := make([]int, 5) 489 | handlerFunc := func(item int) (shouldReturn bool) { 490 | time.Sleep(time.Millisecond * 100) 491 | handledItems[item] = item 492 | 493 | return item == 4 494 | } 495 | 496 | index := -1 497 | updateFromFunc := func(_ time.Time) int { 498 | index++ 499 | return index 500 | } 501 | 502 | puller := NewPuller[int](). 503 | WithTickerChannel(ticker.C, updateFromFunc). 504 | WithHandlerWithShouldReturn(handlerFunc) 505 | puller.StartPull(context.Background()) 506 | 507 | // wait for the last item to be handled. 508 | time.Sleep(5*time.Millisecond*100 + time.Millisecond*10) 509 | 510 | err := puller.StopPull(context.Background()) 511 | require.NoError(t, err) 512 | 513 | assert.Len(t, handledItems, 5) 514 | assert.ElementsMatch(t, []int{0, 1, 2, 3, 0}, handledItems) 515 | }) 516 | 517 | t.Run("WithHandlerWithShouldContinue", func(t *testing.T) { 518 | t.Parallel() 519 | 520 | ticker := time.NewTicker(time.Millisecond * 10) 521 | defer ticker.Stop() 522 | 523 | handledItems := make([]int, 10) 524 | handlerFunc := func(item int) (shouldContinue bool) { 525 | if item%2 == 0 || item > 10 { 526 | return true 527 | } 528 | 529 | time.Sleep(time.Millisecond * 100) 530 | handledItems[item] = item 531 | 532 | return false 533 | } 534 | 535 | index := -1 536 | updateFromFunc := func(_ time.Time) int { 537 | index++ 538 | return index 539 | } 540 | 541 | puller := NewPuller[int](). 542 | WithTickerChannel(ticker.C, updateFromFunc). 543 | WithHandlerWithShouldContinue(handlerFunc) 544 | puller.StartPull(context.Background()) 545 | 546 | // wait for the last item to be handled. 547 | time.Sleep(time.Millisecond*100*5 + time.Millisecond*100) 548 | 549 | err := puller.StopPull(context.Background()) 550 | require.NoError(t, err) 551 | 552 | assert.Len(t, handledItems, 10) 553 | assert.ElementsMatch(t, []int{0, 1, 0, 3, 0, 5, 0, 7, 0, 9}, handledItems) 554 | }) 555 | 556 | t.Run("WithHandlerWithShouldContinueAndShouldReturn", func(t *testing.T) { 557 | t.Parallel() 558 | 559 | ticker := time.NewTicker(time.Millisecond * 10) 560 | defer ticker.Stop() 561 | 562 | handledItems := make([]int, 10) 563 | handlerFunc := func(item int) (shouldContinue, shouldReturn bool) { 564 | if item%2 == 0 { 565 | return true, false // skip even items 566 | } 567 | 568 | time.Sleep(time.Millisecond * 100) 569 | handledItems[item] = item 570 | 571 | return false, item == 7 // stop at 7 572 | } 573 | 574 | index := -1 575 | updateFromFunc := func(_ time.Time) int { 576 | index++ 577 | return index 578 | } 579 | 580 | puller := NewPuller[int](). 581 | WithTickerChannel(ticker.C, updateFromFunc). 582 | WithHandlerWithShouldContinueAndShouldReturn(handlerFunc) 583 | puller.StartPull(context.Background()) 584 | 585 | // wait for the last item to be handled. 586 | time.Sleep(time.Millisecond*100*5 + time.Millisecond*100) 587 | 588 | err := puller.StopPull(context.Background()) 589 | require.NoError(t, err) 590 | 591 | assert.Len(t, handledItems, 10) 592 | assert.ElementsMatch(t, []int{0, 1, 0, 3, 0, 5, 0, 7, 0, 0}, handledItems) 593 | }) 594 | 595 | t.Run("WithHandleAsynchronously", func(t *testing.T) { 596 | t.Parallel() 597 | 598 | ticker := time.NewTicker(time.Millisecond * 10) 599 | defer ticker.Stop() 600 | 601 | handledItems := make([]int, 10) 602 | handlerFunc := func(item int) { 603 | time.Sleep(time.Millisecond * 100) 604 | handledItems[item] = item 605 | } 606 | 607 | index := -1 608 | updateFromFunc := func(_ time.Time) int { 609 | index++ 610 | return index 611 | } 612 | 613 | puller := NewPuller[int](). 614 | WithTickerChannel(ticker.C, updateFromFunc). 615 | WithHandler(handlerFunc). 616 | WithHandleAsynchronously() 617 | 618 | puller.StartPull(context.Background()) 619 | 620 | // wait for the last item to be handled. 621 | time.Sleep(time.Millisecond*100*10 + time.Millisecond*10) 622 | 623 | err := puller.StopPull(context.Background()) 624 | require.NoError(t, err) 625 | 626 | assert.Len(t, handledItems, 10) 627 | assert.ElementsMatch(t, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, handledItems) 628 | }) 629 | 630 | t.Run("WithHandleAsynchronouslyMaxGoroutine", func(t *testing.T) { 631 | t.Parallel() 632 | 633 | ticker := time.NewTicker(time.Millisecond * 10) 634 | defer ticker.Stop() 635 | 636 | handledItems := make([]int, 10) 637 | handlerFunc := func(item int) { 638 | time.Sleep(time.Millisecond * 100) 639 | handledItems[item] = item 640 | } 641 | 642 | index := -1 643 | updateFromFunc := func(_ time.Time) int { 644 | index++ 645 | return index 646 | } 647 | 648 | puller := NewPuller[int](). 649 | WithTickerChannel(ticker.C, updateFromFunc). 650 | WithHandler(handlerFunc). 651 | WithHandleAsynchronouslyMaxGoroutine(5) 652 | 653 | now := time.Now() 654 | 655 | puller.StartPull(context.Background()) 656 | 657 | // wait for the last item to be handled. 658 | time.Sleep(time.Millisecond*100*10 + time.Millisecond*10) 659 | 660 | elapsed := time.Since(now) 661 | assert.True(t, elapsed > time.Millisecond*100*2) 662 | 663 | err := puller.StopPull(context.Background()) 664 | require.NoError(t, err) 665 | 666 | assert.Equal(t, 10, len(handledItems)) 667 | assert.ElementsMatch(t, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, handledItems) 668 | }) 669 | 670 | t.Run("WithPanicHandler", func(t *testing.T) { 671 | t.Parallel() 672 | 673 | ticker := time.NewTicker(time.Millisecond * 10) 674 | defer ticker.Stop() 675 | 676 | handlerFunc := func(item int) { 677 | panic("panic") 678 | } 679 | 680 | index := -1 681 | updateFromFunc := func(_ time.Time) int { 682 | index++ 683 | return index 684 | } 685 | 686 | var panicValue *panics.Recovered 687 | panicHandlerFunc := func(recovered *panics.Recovered) { 688 | panicValue = recovered 689 | } 690 | 691 | puller := NewPuller[int](). 692 | WithTickerChannel(ticker.C, updateFromFunc). 693 | WithHandler(handlerFunc). 694 | WithHandleAsynchronously(). 695 | WithPanicHandler(panicHandlerFunc) 696 | 697 | puller.StartPull(context.Background()) 698 | 699 | // wait for the last item to be handled. 700 | time.Sleep(time.Millisecond * 15) 701 | 702 | err := puller.StopPull(context.Background()) 703 | require.NoError(t, err) 704 | 705 | require.NotNil(t, panicValue) 706 | assert.Equal(t, "panic", panicValue.Value) 707 | 708 | funcObj := runtime.FuncForPC(panicValue.Callers[2]) 709 | assert.Equal(t, "github.com/nekomeowww/xo/exp/channelx.TestPuller_WithTickerChannel.func8.1", funcObj.Name()) 710 | }) 711 | } 712 | -------------------------------------------------------------------------------- /exp/channelx/pusher.go: -------------------------------------------------------------------------------- 1 | package channelx 2 | -------------------------------------------------------------------------------- /float.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | ) 7 | 8 | func IsDecimalsPlacesValid(num float64, decimalPlaces int) bool { 9 | regex := `^(([1-9]\d*)|(0))(\.\d{0,` + strconv.Itoa(decimalPlaces) + `})?$` 10 | ok := regexp.MustCompile(regex).MatchString(strconv.FormatFloat(num, 'f', -1, 64)) 11 | 12 | return ok 13 | } 14 | -------------------------------------------------------------------------------- /float_test.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIsDecimalsPlacesValid(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | assert.True(IsDecimalsPlacesValid(1, 0)) 13 | assert.True(IsDecimalsPlacesValid(1.0, 0)) 14 | assert.True(IsDecimalsPlacesValid(1.0, 1)) 15 | assert.True(IsDecimalsPlacesValid(1.0, 2)) 16 | 17 | assert.True(IsDecimalsPlacesValid(10, 1)) 18 | assert.True(IsDecimalsPlacesValid(10, 2)) 19 | assert.True(IsDecimalsPlacesValid(10, 3)) 20 | assert.True(IsDecimalsPlacesValid(10, 4)) 21 | assert.True(IsDecimalsPlacesValid(10, 100)) 22 | 23 | assert.True(IsDecimalsPlacesValid(1.1, 1)) 24 | assert.True(IsDecimalsPlacesValid(1.11, 2)) 25 | assert.True(IsDecimalsPlacesValid(1.111, 3)) 26 | assert.True(IsDecimalsPlacesValid(1.111, 4)) 27 | assert.True(IsDecimalsPlacesValid(1.111, 100)) 28 | 29 | assert.False(IsDecimalsPlacesValid(1.1, 0)) 30 | assert.False(IsDecimalsPlacesValid(1.11, 1)) 31 | assert.False(IsDecimalsPlacesValid(1.111, 2)) 32 | } 33 | -------------------------------------------------------------------------------- /generic.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | // ToPtrAny returns a pointer to the given value. 4 | func ToPtrAny(v any) *any { 5 | return &v 6 | } 7 | 8 | // FromPtrAny returns the value from the given pointer. 9 | func FromPtrAny[T any](v *any) T { 10 | val, _ := (*v).(T) 11 | return val 12 | } 13 | -------------------------------------------------------------------------------- /generic_test.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestToPtrAny(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | input any 12 | want any 13 | }{ 14 | { 15 | name: "string value", 16 | input: "hello", 17 | want: "hello", 18 | }, 19 | { 20 | name: "integer value", 21 | input: 42, 22 | want: 42, 23 | }, 24 | { 25 | name: "struct value", 26 | input: struct{ Name string }{"test"}, 27 | want: struct{ Name string }{"test"}, 28 | }, 29 | { 30 | name: "nil value", 31 | input: nil, 32 | want: nil, 33 | }, 34 | } 35 | 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | got := ToPtrAny(tt.input) 39 | if got == nil { 40 | t.Fatal("expected non-nil pointer") 41 | } 42 | if !reflect.DeepEqual(*got, tt.want) { 43 | t.Errorf("ToPtrAny() = %v, want %v", *got, tt.want) 44 | } 45 | }) 46 | } 47 | } 48 | 49 | func TestFromPtrAny(t *testing.T) { 50 | tests := []struct { 51 | name string 52 | input any 53 | want any 54 | }{ 55 | { 56 | name: "string conversion", 57 | input: "hello", 58 | want: "hello", 59 | }, 60 | { 61 | name: "integer conversion", 62 | input: 42, 63 | want: 42, 64 | }, 65 | { 66 | name: "failed conversion returns zero value", 67 | input: "not an int", 68 | want: 0, // Testing conversion to int 69 | }, 70 | } 71 | 72 | for _, tt := range tests { 73 | t.Run(tt.name, func(t *testing.T) { 74 | ptr := ToPtrAny(tt.input) 75 | 76 | switch tt.want.(type) { 77 | case string: 78 | got := FromPtrAny[string](ptr) 79 | if got != tt.want { 80 | t.Errorf("FromPtrAny[string]() = %v, want %v", got, tt.want) 81 | } 82 | case int: 83 | got := FromPtrAny[int](ptr) 84 | if got != tt.want { 85 | t.Errorf("FromPtrAny[int]() = %v, want %v", got, tt.want) 86 | } 87 | } 88 | }) 89 | } 90 | } 91 | 92 | func TestRoundTrip(t *testing.T) { 93 | type Person struct { 94 | Name string 95 | Age int 96 | } 97 | 98 | tests := []struct { 99 | name string 100 | value any 101 | }{ 102 | { 103 | name: "string round trip", 104 | value: "hello world", 105 | }, 106 | { 107 | name: "integer round trip", 108 | value: 42, 109 | }, 110 | { 111 | name: "struct round trip", 112 | value: Person{Name: "John", Age: 30}, 113 | }, 114 | } 115 | 116 | for _, tt := range tests { 117 | t.Run(tt.name, func(t *testing.T) { 118 | ptr := ToPtrAny(tt.value) 119 | switch v := tt.value.(type) { 120 | case string: 121 | got := FromPtrAny[string](ptr) 122 | if got != v { 123 | t.Errorf("Round trip failed for string: got %v, want %v", got, v) 124 | } 125 | case int: 126 | got := FromPtrAny[int](ptr) 127 | if got != v { 128 | t.Errorf("Round trip failed for int: got %v, want %v", got, v) 129 | } 130 | case Person: 131 | got := FromPtrAny[Person](ptr) 132 | if !reflect.DeepEqual(got, v) { 133 | t.Errorf("Round trip failed for struct: got %v, want %v", got, v) 134 | } 135 | } 136 | }) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nekomeowww/xo 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | entgo.io/ent v0.14.4 7 | github.com/davecgh/go-spew v1.1.1 8 | github.com/google/uuid v1.6.0 9 | github.com/gookit/color v1.5.4 10 | github.com/nekomeowww/fo v1.6.0 11 | github.com/samber/lo v1.50.0 12 | github.com/shopspring/decimal v1.4.0 13 | github.com/sirupsen/logrus v1.9.3 14 | github.com/sourcegraph/conc v0.3.0 15 | github.com/stretchr/testify v1.10.0 16 | go.opentelemetry.io/contrib/instrumentation/runtime v0.61.0 17 | go.opentelemetry.io/otel v1.36.0 18 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 19 | go.opentelemetry.io/otel/log v0.12.2 20 | go.opentelemetry.io/otel/sdk v1.36.0 21 | go.opentelemetry.io/otel/trace v1.36.0 22 | go.uber.org/zap v1.27.0 23 | google.golang.org/protobuf v1.36.6 24 | ) 25 | 26 | require ( 27 | github.com/go-logr/logr v1.4.2 // indirect 28 | github.com/go-logr/stdr v1.2.2 // indirect 29 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 30 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 31 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 32 | go.opentelemetry.io/otel/metric v1.36.0 // indirect 33 | go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect 34 | go.uber.org/multierr v1.11.0 // indirect 35 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 36 | golang.org/x/sys v0.33.0 // indirect 37 | golang.org/x/text v0.22.0 // indirect 38 | gopkg.in/yaml.v3 v3.0.1 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | entgo.io/ent v0.14.4 h1:/DhDraSLXIkBhyiVoJeSshr4ZYi7femzhj6/TckzZuI= 2 | entgo.io/ent v0.14.4/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 7 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 8 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 9 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 10 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 11 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 12 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 13 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 14 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 15 | github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= 16 | github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= 17 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 18 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 19 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 20 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 21 | github.com/nekomeowww/fo v1.5.1 h1:P8orcwmWR+ErnIDvcjYrrNpnay+BJ6l0HDJFjMjIsNc= 22 | github.com/nekomeowww/fo v1.5.1/go.mod h1:8qtBFmIwI0R/Po4kbO0FkUVlD4puvIEgSwSOVZcmZgc= 23 | github.com/nekomeowww/fo v1.6.0 h1:pY3D7l/GY9aAEa/CvXKEAqOYqUn6HLPlread/f+Ai7Y= 24 | github.com/nekomeowww/fo v1.6.0/go.mod h1:/1m/LPES0Q9axFupU6odfli03h49QrS7ClBLlVSRtd8= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 27 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 29 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 30 | github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= 31 | github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= 32 | github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY= 33 | github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc= 34 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 35 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 36 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 37 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 38 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 39 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 40 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 41 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 42 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 43 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 44 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 45 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 46 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 47 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 48 | go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0 h1:0NgN/3SYkqYJ9NBlDfl/2lzVlwos/YQLvi8sUrzJRBE= 49 | go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0/go.mod h1:oxpUfhTkhgQaYIjtBt3T3w135dLoxq//qo3WPlPIKkE= 50 | go.opentelemetry.io/contrib/instrumentation/runtime v0.61.0 h1:oIZsTHd0YcrvvUCN2AaQqyOcd685NQ+rFmrajveCIhA= 51 | go.opentelemetry.io/contrib/instrumentation/runtime v0.61.0/go.mod h1:X4KSPIvxnY/G5c9UOGXtFoL91t1gmlHpDQzeK5Zc/Bw= 52 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 53 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 54 | go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= 55 | go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= 56 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= 57 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= 58 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0= 59 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= 60 | go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= 61 | go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc= 62 | go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc= 63 | go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E= 64 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 65 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 66 | go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= 67 | go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= 68 | go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 69 | go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 70 | go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= 71 | go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= 72 | go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= 73 | go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 74 | go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= 75 | go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= 76 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 77 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 78 | go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= 79 | go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= 80 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 81 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 82 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 83 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 84 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 85 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 86 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 87 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 88 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 89 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 90 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 91 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 92 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 93 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 94 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 95 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 96 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 97 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 98 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 99 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 100 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 101 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 102 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 103 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 104 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 105 | -------------------------------------------------------------------------------- /int.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "math" 5 | "math/big" 6 | "strconv" 7 | 8 | "github.com/shopspring/decimal" 9 | ) 10 | 11 | // DigitOf returns the n-th digit of val. 12 | // 13 | // Underlying calculation is based on (val % 10^n) . 14 | func DigitOf[I uint | uint16 | uint32 | uint64 | int | int16 | int32 | int64](val I, n int) I { 15 | if n < 0 { 16 | return 0 17 | } 18 | 19 | r := val % I(math.Pow10(n)) 20 | 21 | return r / I(math.Pow10(n-1)) 22 | } 23 | 24 | // ParseUint64FromDecimalString converts a string with a decimal point to a uint64. 25 | func ParseUint64FromDecimalString(decimalStr string, percision int) (uint64, error) { 26 | priceDecimal, err := decimal.NewFromString(decimalStr) 27 | if err != nil { 28 | return 0, err 29 | } 30 | 31 | priceDecimal = priceDecimal.Mul(decimal.NewFromFloat(math.Pow10(percision))) 32 | 33 | priceDecimalBigInt := priceDecimal.BigInt() 34 | if priceDecimalBigInt.Cmp(big.NewInt(0)) <= 0 { 35 | return 0, nil 36 | } 37 | 38 | return priceDecimalBigInt.Uint64(), nil 39 | } 40 | 41 | // ConvertUint64ToDecimalString formats a uint64 to a string with a decimal point. 42 | func ConvertUint64ToDecimalString(amount uint64, prec int) string { 43 | if amount == 0 { 44 | strconv.FormatFloat(0, 'f', prec, 64) 45 | } 46 | 47 | float64Number, _ := decimal. 48 | NewFromUint64(amount). 49 | Div(decimal.NewFromInt(100)). 50 | Float64() 51 | 52 | return strconv.FormatFloat(float64Number, 'f', prec, 64) 53 | } 54 | -------------------------------------------------------------------------------- /int_test.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestDigitOf(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | assert.Equal(0, DigitOf(0, 1)) 14 | assert.Equal(5, DigitOf(5, 1)) 15 | 16 | assert.Equal(1, DigitOf(11, 1)) 17 | assert.Equal(5, DigitOf(51, 2)) 18 | 19 | assert.Equal(8, DigitOf(128, 1)) 20 | assert.Equal(2, DigitOf(128, 2)) 21 | assert.Equal(1, DigitOf(128, 3)) 22 | } 23 | 24 | func TestParseDecimalStringIntoUint64(t *testing.T) { 25 | assert := assert.New(t) 26 | require := require.New(t) 27 | 28 | uintNumber, err := ParseUint64FromDecimalString("0.01", 2) 29 | require.NoError(err) 30 | assert.Equal(uint64(1), uintNumber) 31 | 32 | uintNumber, err = ParseUint64FromDecimalString("0.11", 2) 33 | require.NoError(err) 34 | assert.Equal(uint64(11), uintNumber) 35 | 36 | uintNumber, err = ParseUint64FromDecimalString("0.1111", 2) 37 | require.NoError(err) 38 | assert.Equal(uint64(11), uintNumber) 39 | 40 | uintNumber, err = ParseUint64FromDecimalString("10.00", 2) 41 | require.NoError(err) 42 | assert.Equal(uint64(1000), uintNumber) 43 | 44 | uintNumber, err = ParseUint64FromDecimalString("10.01", 2) 45 | require.NoError(err) 46 | assert.Equal(uint64(1001), uintNumber) 47 | 48 | uintNumber, err = ParseUint64FromDecimalString("10.11", 2) 49 | require.NoError(err) 50 | assert.Equal(uint64(1011), uintNumber) 51 | 52 | uintNumber, err = ParseUint64FromDecimalString("10.0000", 4) 53 | require.NoError(err) 54 | assert.Equal(uint64(100000), uintNumber) 55 | 56 | uintNumber, err = ParseUint64FromDecimalString("10.01", 4) 57 | require.NoError(err) 58 | assert.Equal(uint64(100100), uintNumber) 59 | 60 | uintNumber, err = ParseUint64FromDecimalString("10.1101", 4) 61 | require.NoError(err) 62 | assert.Equal(uint64(101101), uintNumber) 63 | 64 | uintNumber, err = ParseUint64FromDecimalString("1000000.0000001", 2) 65 | require.NoError(err) 66 | assert.Equal(uint64(100000000), uintNumber) 67 | 68 | uintNumber, err = ParseUint64FromDecimalString("1000000.01", 2) 69 | require.NoError(err) 70 | assert.Equal(uint64(100000001), uintNumber) 71 | 72 | uintNumber, err = ParseUint64FromDecimalString("0", 2) 73 | require.NoError(err) 74 | assert.Equal(uint64(0), uintNumber) 75 | 76 | uintNumber, err = ParseUint64FromDecimalString("0000000", 2) 77 | require.NoError(err) 78 | assert.Equal(uint64(0), uintNumber) 79 | 80 | uintNumber, err = ParseUint64FromDecimalString("0.00000", 2) 81 | require.NoError(err) 82 | assert.Equal(uint64(0), uintNumber) 83 | 84 | uintNumber, err = ParseUint64FromDecimalString("00000.00000", 2) 85 | require.NoError(err) 86 | assert.Equal(uint64(0), uintNumber) 87 | } 88 | 89 | func TestConvertUint64ToDecimalString(t *testing.T) { 90 | assert := assert.New(t) 91 | 92 | assert.Equal("0.00", ConvertUint64ToDecimalString(0, 2)) 93 | assert.Equal("0", ConvertUint64ToDecimalString(0, -1)) 94 | 95 | assert.Equal("19.90", ConvertUint64ToDecimalString(1990, 2)) 96 | assert.Equal("19.9", ConvertUint64ToDecimalString(1990, -1)) 97 | assert.Equal("19.99", ConvertUint64ToDecimalString(1999, 2)) 98 | 99 | assert.Equal("1.00", ConvertUint64ToDecimalString(100, 2)) 100 | assert.Equal("1", ConvertUint64ToDecimalString(100, -1)) 101 | } 102 | -------------------------------------------------------------------------------- /io.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // ReadFileAsBytesBuffer reads a file and returns a bytes.Buffer. 10 | func ReadFileAsBytesBuffer(path string) (*bytes.Buffer, error) { 11 | file, err := os.Open(path) 12 | if err != nil { 13 | return nil, err 14 | } 15 | defer file.Close() 16 | 17 | buf := new(bytes.Buffer) 18 | 19 | _, err = io.Copy(buf, file) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return buf, nil 25 | } 26 | 27 | type NopIoWriter struct{} 28 | type EmptyIoWriter = NopIoWriter 29 | 30 | func NewNopIoWriter() *NopIoWriter { 31 | return &NopIoWriter{} 32 | } 33 | 34 | func NewEmptyIoWriter() *NopIoWriter { 35 | return &NopIoWriter{} 36 | } 37 | 38 | func (w *NopIoWriter) Write(p []byte) (n int, err error) { 39 | return len(p), nil 40 | } 41 | 42 | type NopIoReader struct{} 43 | type EmptyIoReader = NopIoReader 44 | 45 | func NewNopIoReader() *NopIoReader { 46 | return &NopIoReader{} 47 | } 48 | 49 | func NewEmptyIoReader() *NopIoReader { 50 | return &NopIoReader{} 51 | } 52 | 53 | func (r *NopIoReader) Read(p []byte) (n int, err error) { 54 | return 0, io.EOF 55 | } 56 | -------------------------------------------------------------------------------- /io_test.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestNopIoWriter_Writer(t *testing.T) { 14 | buffer := new(bytes.Buffer) 15 | 16 | n, err := fmt.Fprint(buffer, "abcd") 17 | require.NoError(t, err) 18 | 19 | n2, err2 := fmt.Fprint(&NopIoWriter{}, "abcd") 20 | require.NoError(t, err2) 21 | 22 | assert.Equal(t, n, n2) 23 | } 24 | 25 | func TestNopIoReader_Read(t *testing.T) { 26 | buffer := new(bytes.Buffer) 27 | buffer.WriteString("abcd") 28 | 29 | content, err := io.ReadAll(buffer) 30 | require.NoError(t, err) 31 | require.NotEmpty(t, content) 32 | assert.Len(t, content, 4) 33 | 34 | content2, err2 := io.ReadAll(&NopIoReader{}) 35 | require.NoError(t, err2) 36 | require.Empty(t, content2) 37 | } 38 | -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import "encoding/json" 4 | 5 | // IsJSON determines whether the string is JSON. 6 | func IsJSON(str string) bool { 7 | if str == "" { 8 | return false 9 | } 10 | 11 | var js json.RawMessage 12 | 13 | return json.Unmarshal([]byte(str), &js) == nil 14 | } 15 | 16 | // IsJSONBytes determines whether the bytes is JSON. 17 | func IsJSONBytes(bytes []byte) bool { 18 | if len(bytes) == 0 { 19 | return false 20 | } 21 | 22 | var js json.RawMessage 23 | 24 | return json.Unmarshal(bytes, &js) == nil 25 | } 26 | -------------------------------------------------------------------------------- /json_test.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIsJSON(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | assert.True(IsJSON(`{"foo": "bar"}`)) 13 | assert.True(IsJSON(`["foo", "bar"]`)) 14 | assert.False(IsJSON(`{"foo": "bar"`)) 15 | assert.False(IsJSON(``)) 16 | } 17 | 18 | func TestIsJSONBytes(t *testing.T) { 19 | assert := assert.New(t) 20 | 21 | assert.True(IsJSONBytes([]byte(`{"foo": "bar"}`))) 22 | assert.True(IsJSONBytes([]byte(`["foo", "bar"]`))) 23 | assert.False(IsJSONBytes([]byte(`{"foo": "bar"`))) 24 | assert.False(IsJSONBytes([]byte(``))) 25 | } 26 | -------------------------------------------------------------------------------- /k8s.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var rfc1123ValidName = regexp.MustCompile("[^A-Za-z0-9-]") 9 | var rfc1123LeadingTrailingValidName = regexp.MustCompile("^[^A-Za-z0-9]+|[^A-Za-z0-9]+$") 10 | 11 | func NormalizeAsRFC1123Name(value string) string { 12 | // Replace "/" with "-" 13 | value = strings.ReplaceAll(value, "/", "-") 14 | 15 | // Replace invalid characters with hyphens 16 | value = rfc1123ValidName.ReplaceAllString(value, "-") 17 | 18 | // Trim hyphens, underscores, and dots from the beginning and end 19 | value = strings.Trim(value, "-") 20 | 21 | // Ensure it starts and ends with an alphanumeric character 22 | value = rfc1123LeadingTrailingValidName.ReplaceAllString(value, "") 23 | 24 | // Truncate to maximum length if necessary 25 | if len(value) > 256 { 26 | value = value[:256] 27 | } 28 | 29 | return value 30 | } 31 | -------------------------------------------------------------------------------- /k8s_test.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestNormalizeAsRFC1123Name(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | input string 14 | expected string 15 | }{ 16 | {"Underscore to hyphen", "before_after", "before-after"}, 17 | {"Dots to hyphens", "1.1.1", "1-1-1"}, 18 | {"Trim hyphens", "-middle-", "middle"}, 19 | {"Special chars", "_@invalid.com", "invalid-com"}, 20 | {"Special chars at end", "invalid.com@.", "invalid-com"}, 21 | {"Multiple special chars", "_@invalid.com@.", "invalid-com"}, 22 | {"Only dots", "...", ""}, 23 | {"Non-ASCII chars", "中文中文", ""}, 24 | {"Mixed ASCII and non-ASCII", "中文abcd中文", "abcd"}, 25 | {"Max length 255", strings.Repeat("a", 255), strings.Repeat("a", 255)}, 26 | {"Over max length 256", strings.Repeat("a", 256), strings.Repeat("a", 256)}, 27 | {"Over max length 257", strings.Repeat("a", 257), strings.Repeat("a", 256)}, 28 | } 29 | 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | result := NormalizeAsRFC1123Name(tt.input) 33 | assert.Equal(t, tt.expected, result) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /logger/format.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "runtime" 7 | "sort" 8 | "time" 9 | 10 | "github.com/gookit/color" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // LogPrettyFormatter defines the format for log file. 15 | type LogPrettyFormatter struct { 16 | logrus.TextFormatter 17 | MinimumCallerDepth int 18 | } 19 | 20 | // NewLogFileFormatter return the log format for log file. 21 | // 22 | // eg: 2023-06-01T12:00:00 [info] [controllers/some_controller/code_file.go:99] foo key=value 23 | func NewLogPrettyFormatter() *LogPrettyFormatter { 24 | return &LogPrettyFormatter{ 25 | TextFormatter: logrus.TextFormatter{ 26 | TimestampFormat: time.RFC3339Nano, 27 | FullTimestamp: true, 28 | }, 29 | MinimumCallerDepth: 0, 30 | } 31 | } 32 | 33 | // Format renders a single log entry for log file 34 | // 35 | // the original file log format is defined here: github.com/sirupsen/logrus/text_formatter.TextFormatter{}.Format(). 36 | func (f *LogPrettyFormatter) Format(entry *logrus.Entry) ([]byte, error) { 37 | data := make(map[string]any) 38 | for k, v := range entry.Data { 39 | data[k] = v 40 | } 41 | 42 | keys := make([]string, 0, len(data)) 43 | 44 | for k := range data { 45 | if k == "caller_file" { 46 | continue 47 | } 48 | 49 | keys = append(keys, k) 50 | } 51 | 52 | if !f.DisableSorting { 53 | if nil != f.SortingFunc { 54 | f.SortingFunc(keys) 55 | } else { 56 | sort.Strings(keys) 57 | } 58 | } 59 | 60 | timestampFormat := f.TimestampFormat 61 | if timestampFormat == "" { 62 | timestampFormat = time.RFC3339 63 | } 64 | 65 | var b *bytes.Buffer 66 | if entry.Buffer != nil { 67 | b = entry.Buffer 68 | } else { 69 | b = &bytes.Buffer{} 70 | } 71 | 72 | prefixStr := entry.Time.Format(timestampFormat) + " " 73 | var renderFunc func(a ...any) string 74 | 75 | switch entry.Level { 76 | case logrus.TraceLevel: 77 | renderFunc = color.FgGray.Render 78 | case logrus.DebugLevel: 79 | renderFunc = color.FgGreen.Render 80 | case logrus.InfoLevel: 81 | renderFunc = color.FgCyan.Render 82 | case logrus.WarnLevel: 83 | renderFunc = color.FgYellow.Render 84 | case logrus.ErrorLevel: 85 | renderFunc = color.FgRed.Render 86 | case logrus.FatalLevel: 87 | renderFunc = color.FgMagenta.Render 88 | case logrus.PanicLevel: 89 | renderFunc = color.FgMagenta.Render 90 | default: 91 | renderFunc = color.FgGray.Render 92 | } 93 | 94 | prefixStr += renderFunc("[", entry.Level.String(), "]") 95 | 96 | b.WriteString(prefixStr) 97 | 98 | if data["caller_file"] != nil { 99 | fmt.Fprintf(b, " [%s]", data["caller_file"]) 100 | delete(data, "file") 101 | } else if entry.Context != nil { 102 | caller, _ := entry.Context.Value(runtimeCaller).(*runtime.Frame) 103 | if caller != nil { 104 | fmt.Fprintf(b, " [%s:%d]", caller.File, caller.Line) 105 | } 106 | } 107 | 108 | if entry.Message != "" { 109 | b.WriteString(" " + entry.Message) 110 | } 111 | 112 | for _, key := range keys { 113 | value := data[key] 114 | appendKeyValue(b, key, value, f.QuoteEmptyFields) 115 | } 116 | 117 | b.WriteByte('\n') 118 | 119 | return b.Bytes(), nil 120 | } 121 | 122 | // appendKeyValue append value with key to data that to be appended to log file. 123 | func appendKeyValue(b *bytes.Buffer, key string, value interface{}, QuoteEmptyFields bool) { 124 | if b.Len() > 0 { 125 | b.WriteByte(' ') 126 | } 127 | 128 | b.WriteString(key) 129 | b.WriteByte('=') 130 | appendValue(b, value, QuoteEmptyFields) 131 | } 132 | 133 | // appendValue append value to data used for method appendKeyValue. 134 | func appendValue(b *bytes.Buffer, value interface{}, QuoteEmptyFields bool) { 135 | stringVal, ok := value.(string) 136 | if !ok { 137 | stringVal = fmt.Sprint(value) 138 | } 139 | 140 | if !needsQuoting(stringVal, QuoteEmptyFields) { 141 | b.WriteString(stringVal) 142 | } else { 143 | fmt.Fprintf(b, "%q", stringVal) 144 | } 145 | } 146 | 147 | // needsQuoting check where text needs to be quoted. 148 | func needsQuoting(text string, quoteEmptyFields bool) bool { 149 | if quoteEmptyFields && len(text) == 0 { 150 | return true 151 | } 152 | 153 | for _, ch := range text { 154 | if (ch >= 'a' && ch <= 'z') || 155 | (ch >= 'A' && ch <= 'Z') || 156 | (ch >= '0' && ch <= '9') || 157 | ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+' { 158 | continue 159 | } 160 | 161 | return true 162 | } 163 | 164 | return false 165 | } 166 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | // Package logger 2 | package logger 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | "time" 14 | 15 | "github.com/nekomeowww/xo/logger/loki" 16 | "github.com/nekomeowww/xo/logger/otelzap" 17 | "github.com/sirupsen/logrus" 18 | "go.opentelemetry.io/otel" 19 | "go.opentelemetry.io/otel/attribute" 20 | "go.opentelemetry.io/otel/codes" 21 | semconv "go.opentelemetry.io/otel/semconv/v1.17.0" 22 | "go.opentelemetry.io/otel/trace" 23 | "go.uber.org/zap" 24 | "go.uber.org/zap/zapcore" 25 | ) 26 | 27 | type ZapField zapcore.Field 28 | 29 | func (f ZapField) MatchValue() any { 30 | switch f.Type { 31 | case zapcore.UnknownType: // checked 32 | return f.Interface 33 | case zapcore.ArrayMarshalerType: // checked 34 | return f.Interface 35 | case zapcore.ObjectMarshalerType: // checked 36 | return f.Interface 37 | case zapcore.BinaryType: // checked 38 | return f.Interface 39 | case zapcore.BoolType: // checked 40 | return f.Integer != 0 41 | case zapcore.ByteStringType: // checked 42 | return f.Interface 43 | case zapcore.Complex128Type: // checked 44 | return f.Interface 45 | case zapcore.Complex64Type: // checked 46 | return f.Interface 47 | case zapcore.DurationType: // checked 48 | return time.Duration(f.Integer) 49 | case zapcore.Float64Type: // checked 50 | return f.Integer 51 | case zapcore.Float32Type: // checked 52 | return f.Integer 53 | case zapcore.Int64Type: // checked 54 | return f.Integer 55 | case zapcore.Int32Type: // checked 56 | return f.Integer 57 | case zapcore.Int16Type: // checked 58 | return f.Integer 59 | case zapcore.Int8Type: // checked 60 | return f.Integer 61 | case zapcore.StringType: // checked 62 | return f.String 63 | case zapcore.TimeType: // checked 64 | return time.Unix(0, f.Integer) 65 | case zapcore.TimeFullType: // checked 66 | return f.Interface 67 | case zapcore.Uint64Type: // checked 68 | return f.Integer 69 | case zapcore.Uint32Type: // checked 70 | return f.Integer 71 | case zapcore.Uint16Type: // checked 72 | return f.Integer 73 | case zapcore.Uint8Type: // checked 74 | return f.Integer 75 | case zapcore.UintptrType: // checked 76 | return f.Integer 77 | case zapcore.ReflectType: // checked 78 | return f.Interface 79 | case zapcore.NamespaceType: // checked 80 | return "" 81 | case zapcore.StringerType: // checked 82 | return f.Interface 83 | case zapcore.ErrorType: // checked 84 | return f.Interface 85 | case zapcore.SkipType: // checked 86 | return "" 87 | case zapcore.InlineMarshalerType: // checked 88 | return f.Interface 89 | } 90 | 91 | return f.Interface 92 | } 93 | 94 | type Logger struct { 95 | LogrusLogger *logrus.Entry 96 | ZapLogger *zap.Logger 97 | otelTracer trace.Tracer 98 | 99 | withAppendedFields []zap.Field 100 | openTelemetryDisabled bool 101 | namespace string 102 | skip int 103 | errorStatusLevel zapcore.Level 104 | caller bool 105 | stackTrace bool 106 | } 107 | 108 | // Debug logs a message at DebugLevel. The message includes any fields passed 109 | // at the log site, as well as any fields accumulated on the logger. 110 | func (l *Logger) Debug(msg string, fields ...zapcore.Field) { 111 | l.ZapLogger.Debug(msg, fields...) 112 | 113 | data := make(map[string]any) 114 | for k, v := range l.LogrusLogger.Data { 115 | data[k] = v 116 | } 117 | 118 | entry := logrus.NewEntry(l.LogrusLogger.Logger) 119 | SetCallFrame(entry, l.namespace, l.skip) 120 | 121 | for k, v := range data { 122 | entry = entry.WithField(k, v) 123 | } 124 | 125 | for _, v := range fields { 126 | entry = entry.WithField(v.Key, ZapField(v).MatchValue()) 127 | } 128 | 129 | entry.Debug(msg) 130 | } 131 | 132 | // DebugContext logs a message at DebugLevel. The message includes any fields passed 133 | // at the log site, as well as any fields accumulated on the logger. Besides that, it 134 | // also logs the message to the OpenTelemetry span. 135 | func (l *Logger) DebugContext(ctx context.Context, msg string, fields ...zapcore.Field) { 136 | if !l.openTelemetryDisabled { 137 | l.span(ctx, zapcore.DebugLevel, msg, fields...) 138 | } 139 | 140 | l.Debug(msg, fields...) 141 | } 142 | 143 | // Info logs a message at InfoLevel. The message includes any fields passed 144 | // at the log site, as well as any fields accumulated on the logger. 145 | func (l *Logger) Info(msg string, fields ...zapcore.Field) { 146 | l.ZapLogger.Info(msg, fields...) 147 | 148 | data := make(map[string]any) 149 | for k, v := range l.LogrusLogger.Data { 150 | data[k] = v 151 | } 152 | 153 | entry := logrus.NewEntry(l.LogrusLogger.Logger) 154 | SetCallFrame(entry, l.namespace, l.skip) 155 | 156 | for k, v := range data { 157 | entry = entry.WithField(k, v) 158 | } 159 | 160 | for _, v := range fields { 161 | entry = entry.WithField(v.Key, ZapField(v).MatchValue()) 162 | } 163 | 164 | entry.Info(msg) 165 | } 166 | 167 | // InfoContext logs a message at InfoLevel. The message includes any fields passed 168 | // at the log site, as well as any fields accumulated on the logger. Besides that, it 169 | // also logs the message to the OpenTelemetry span. 170 | func (l *Logger) InfoContext(ctx context.Context, msg string, fields ...zapcore.Field) { 171 | if !l.openTelemetryDisabled { 172 | l.span(ctx, zapcore.InfoLevel, msg, fields...) 173 | } 174 | 175 | l.Info(msg, fields...) 176 | } 177 | 178 | // Warn logs a message at WarnLevel. The message includes any fields passed 179 | // at the log site, as well as any fields accumulated on the logger. Besides that, it 180 | // also logs the message to the OpenTelemetry span. 181 | func (l *Logger) Warn(msg string, fields ...zapcore.Field) { 182 | l.ZapLogger.Warn(msg, fields...) 183 | 184 | data := make(map[string]any) 185 | for k, v := range l.LogrusLogger.Data { 186 | data[k] = v 187 | } 188 | 189 | entry := logrus.NewEntry(l.LogrusLogger.Logger) 190 | SetCallFrame(entry, l.namespace, l.skip) 191 | 192 | for k, v := range data { 193 | entry = entry.WithField(k, v) 194 | } 195 | 196 | for _, v := range fields { 197 | entry = entry.WithField(v.Key, ZapField(v).MatchValue()) 198 | } 199 | 200 | entry.Warn(msg) 201 | } 202 | 203 | // WarnContext logs a message at WarnLevel. The message includes any fields passed 204 | // at the log site, as well as any fields accumulated on the logger. Besides that, it 205 | // also logs the message to the OpenTelemetry span. 206 | func (l *Logger) WarnContext(ctx context.Context, msg string, fields ...zapcore.Field) { 207 | if !l.openTelemetryDisabled { 208 | l.span(ctx, zapcore.WarnLevel, msg, fields...) 209 | } 210 | 211 | l.Warn(msg, fields...) 212 | } 213 | 214 | // Error logs a message at ErrorLevel. The message includes any fields passed 215 | // at the log site, as well as any fields accumulated on the logger. 216 | func (l *Logger) Error(msg string, fields ...zapcore.Field) { 217 | l.ZapLogger.Error(msg, fields...) 218 | 219 | data := make(map[string]any) 220 | for k, v := range l.LogrusLogger.Data { 221 | data[k] = v 222 | } 223 | 224 | entry := logrus.NewEntry(l.LogrusLogger.Logger) 225 | SetCallFrame(entry, l.namespace, l.skip) 226 | 227 | for k, v := range data { 228 | entry = entry.WithField(k, v) 229 | } 230 | 231 | for _, v := range fields { 232 | entry = entry.WithField(v.Key, ZapField(v).MatchValue()) 233 | } 234 | 235 | entry.Error(msg) 236 | } 237 | 238 | // ErrorContext logs a message at ErrorLevel. The message includes any fields passed 239 | // at the log site, as well as any fields accumulated on the logger. Besides that, it 240 | // also logs the message to the OpenTelemetry span. 241 | func (l *Logger) ErrorContext(ctx context.Context, msg string, fields ...zapcore.Field) { 242 | l.span(ctx, zapcore.ErrorLevel, msg, fields...) 243 | l.Error(msg, fields...) 244 | } 245 | 246 | // Fatal logs a message at FatalLevel. The message includes any fields passed 247 | // at the log site, as well as any fields accumulated on the logger. 248 | // 249 | // NOTICE: This method calls os.Exit(1) to exit the program. It also prioritizes the execution of logrus' Fatal method over zap's Fatal method. 250 | func (l *Logger) Fatal(msg string, fields ...zapcore.Field) { 251 | data := make(map[string]any) 252 | for k, v := range l.LogrusLogger.Data { 253 | data[k] = v 254 | } 255 | 256 | entry := logrus.NewEntry(l.LogrusLogger.Logger) 257 | SetCallFrame(entry, l.namespace, l.skip) 258 | 259 | for k, v := range data { 260 | entry = entry.WithField(k, v) 261 | } 262 | 263 | for _, v := range fields { 264 | entry = entry.WithField(v.Key, ZapField(v).MatchValue()) 265 | } 266 | 267 | entry.Fatal(msg) 268 | l.ZapLogger.Fatal(msg, fields...) 269 | } 270 | 271 | // FatalContext logs a message at FatalLevel. The message includes any fields passed 272 | // at the log site, as well as any fields accumulated on the logger. Besides that, it 273 | // also logs the message to the OpenTelemetry span. 274 | func (l *Logger) FatalContext(ctx context.Context, msg string, fields ...zapcore.Field) { 275 | if !l.openTelemetryDisabled { 276 | l.span(ctx, zapcore.FatalLevel, msg, fields...) 277 | } 278 | 279 | l.Fatal(msg, fields...) 280 | } 281 | 282 | // With creates a new logger instance that inherits the context information from the current logger. 283 | // Fields added to the new logger instance do not affect the current logger instance. 284 | func (l *Logger) With(fields ...zapcore.Field) *Logger { 285 | newZapLogger := l.ZapLogger.With(fields...) 286 | 287 | data := make(map[string]any) 288 | for k, v := range l.LogrusLogger.Data { 289 | data[k] = v 290 | } 291 | 292 | entry := logrus.NewEntry(l.LogrusLogger.Logger) 293 | SetCallFrame(entry, l.namespace, l.skip) 294 | 295 | for k, v := range data { 296 | entry = entry.WithField(k, v) 297 | } 298 | 299 | for _, v := range fields { 300 | entry = entry.WithField(v.Key, ZapField(v).MatchValue()) 301 | } 302 | 303 | return &Logger{ 304 | ZapLogger: newZapLogger, 305 | withAppendedFields: append(l.withAppendedFields, fields...), 306 | LogrusLogger: entry, 307 | namespace: l.namespace, 308 | skip: l.skip, 309 | openTelemetryDisabled: l.openTelemetryDisabled, 310 | } 311 | } 312 | 313 | // WithAndSkip creates a new logger instance that inherits the context information from the current logger. 314 | // Fields added to the new logger instance do not affect the current logger instance. 315 | // The skip parameter is used to determine the number of stack frames to skip when retrieving the caller information. 316 | func (l *Logger) WithAndSkip(skip int, fields ...zapcore.Field) *Logger { 317 | newZapLogger := l.ZapLogger.WithOptions(zap.AddCallerSkip(1)).With(fields...) 318 | 319 | data := make(map[string]any) 320 | for k, v := range l.LogrusLogger.Data { 321 | data[k] = v 322 | } 323 | 324 | entry := logrus.NewEntry(l.LogrusLogger.Logger) 325 | SetCallFrame(entry, l.namespace, skip) 326 | 327 | for k, v := range data { 328 | entry = entry.WithField(k, v) 329 | } 330 | 331 | for _, v := range fields { 332 | entry = entry.WithField(v.Key, ZapField(v).MatchValue()) 333 | } 334 | 335 | return &Logger{ 336 | ZapLogger: newZapLogger, 337 | LogrusLogger: entry, 338 | withAppendedFields: append(l.withAppendedFields, fields...), 339 | namespace: l.namespace, 340 | skip: skip, 341 | openTelemetryDisabled: l.openTelemetryDisabled, 342 | } 343 | } 344 | 345 | func (l *Logger) span(ctx context.Context, lvl zapcore.Level, msg string, fields ...zap.Field) { 346 | span := trace.SpanFromContext(ctx) 347 | 348 | if lvl >= l.errorStatusLevel && span.IsRecording() { 349 | span.SetStatus(codes.Error, msg) 350 | } 351 | 352 | attrs := make([]attribute.KeyValue, 0, len(fields)+3) 353 | attrs = append(attrs, semconv.OtelLibraryName("github.com/nekomeowww/xo/logger")) 354 | attrs = append(attrs, semconv.OtelLibraryVersion("1.0.0")) 355 | attrs = append(attrs, attribute.String("log.severity", otelzap.LogSeverityFromZapLevel(lvl).String())) 356 | attrs = append(attrs, attribute.String("log.message", msg)) 357 | 358 | for _, field := range l.withAppendedFields { 359 | attrs = append(attrs, otelzap.AttributesFromZapField(field)...) 360 | } 361 | 362 | for _, field := range fields { 363 | attrs = append(attrs, otelzap.AttributesFromZapField(field)...) 364 | } 365 | 366 | if l.caller { 367 | if fn, file, line, ok := runtime.Caller(l.skip + 1); ok { 368 | fn := runtime.FuncForPC(fn).Name() 369 | if fn != "" { 370 | attrs = append(attrs, attribute.String("code.function", fn)) 371 | } 372 | if file != "" { 373 | attrs = append(attrs, attribute.String("code.filepath", file)) 374 | attrs = append(attrs, attribute.Int("code.lineno", line)) 375 | } 376 | } 377 | } 378 | 379 | if l.stackTrace { 380 | stackTrace := make([]byte, 2048) 381 | n := runtime.Stack(stackTrace, false) 382 | attrs = append(attrs, attribute.String("exception.stacktrace", string(stackTrace[:n]))) 383 | } 384 | 385 | span.AddEvent("log", trace.WithAttributes(attrs...)) 386 | } 387 | 388 | // SetCallFrame set the caller information for the log entry. 389 | func SetCallFrame(entry *logrus.Entry, namespace string, skip int) { 390 | _, file, line, _ := runtime.Caller(skip + 1) 391 | pc, _, _, _ := runtime.Caller(skip + 2) 392 | funcDetail := runtime.FuncForPC(pc) 393 | 394 | var funcName string 395 | if funcDetail != nil { 396 | funcName = funcDetail.Name() 397 | } 398 | 399 | SetCallerFrameWithFileAndLine(entry, namespace, funcName, file, line) 400 | } 401 | 402 | type contextKey string 403 | 404 | const ( 405 | runtimeCaller contextKey = "ContextKeyRuntimeCaller" 406 | ) 407 | 408 | // SetCallerFrameWithFileAndLine set the caller information for the log entry. 409 | func SetCallerFrameWithFileAndLine(entry *logrus.Entry, namespace, functionName, file string, line int) { 410 | splitTarget := filepath.FromSlash("/" + namespace + "/") 411 | 412 | filename := strings.SplitN(file, splitTarget, 2) 413 | if len(filename) < 2 { 414 | filename = []string{"", file} 415 | } 416 | 417 | entry.Context = context.WithValue(context.Background(), runtimeCaller, &runtime.Frame{ 418 | File: filename[1], 419 | Line: line, 420 | Function: functionName, 421 | }) 422 | } 423 | 424 | func zapCoreLevelToLogrusLevel(level zapcore.Level) logrus.Level { 425 | switch level { 426 | case zapcore.DebugLevel: 427 | return logrus.DebugLevel 428 | case zapcore.InfoLevel: 429 | return logrus.InfoLevel 430 | case zapcore.WarnLevel: 431 | return logrus.WarnLevel 432 | case zapcore.ErrorLevel: 433 | return logrus.ErrorLevel 434 | case zapcore.FatalLevel: 435 | return logrus.FatalLevel 436 | case zapcore.PanicLevel, zapcore.DPanicLevel: 437 | return logrus.PanicLevel 438 | case zapcore.InvalidLevel: 439 | return logrus.InfoLevel 440 | default: 441 | return logrus.InfoLevel 442 | } 443 | } 444 | 445 | func ReadLogLevelFromEnv() (zapcore.Level, error) { 446 | logLevelStr := os.Getenv("LOG_LEVEL") 447 | if logLevelStr == "" { 448 | return zapcore.InfoLevel, nil 449 | } 450 | 451 | logLevel, err := zapcore.ParseLevel(logLevelStr) 452 | if err != nil { 453 | logLevel = zapcore.InfoLevel 454 | return logLevel, errors.New("log level " + logLevelStr + " in environment variable LOG_LEVEL is invalid, fallbacks to default level: info") 455 | } 456 | if logLevel == zapcore.FatalLevel { 457 | logLevel = zapcore.InfoLevel 458 | return logLevel, fmt.Errorf("log level fatal in environment variable LOG_LEVEL is invalid, fallbacks to default level: info") 459 | } 460 | 461 | return logLevel, nil 462 | } 463 | 464 | func ReadLogFormatFromEnv() (Format, error) { 465 | logFormatStr := os.Getenv("LOG_FORMAT") 466 | if logFormatStr == "" { 467 | return FormatPretty, nil 468 | } 469 | 470 | switch logFormatStr { 471 | case "json": 472 | return FormatJSON, nil 473 | case "pretty": 474 | return FormatPretty, nil 475 | default: 476 | return FormatPretty, fmt.Errorf("log format %s in environment variable LOG_FORMAT is invalid, fallbacks to default format: pretty", logFormatStr) 477 | } 478 | } 479 | 480 | type newLoggerOptions struct { 481 | level zapcore.Level 482 | logFilePath string 483 | hook []logrus.Hook 484 | appName string 485 | namespace string 486 | initialFields map[string]any 487 | callFrameSkip int 488 | format Format 489 | lokiRemoteConfig *loki.Config 490 | openTelemetryDisabled bool 491 | } 492 | 493 | type NewLoggerCallOption func(*newLoggerOptions) 494 | 495 | func WithLevel(level zapcore.Level) NewLoggerCallOption { 496 | return func(o *newLoggerOptions) { 497 | o.level = level 498 | } 499 | } 500 | 501 | type Format string 502 | 503 | const ( 504 | FormatJSON Format = "json" 505 | FormatPretty Format = "pretty" 506 | ) 507 | 508 | func WithFormat(format Format) NewLoggerCallOption { 509 | return func(o *newLoggerOptions) { 510 | o.format = format 511 | } 512 | } 513 | 514 | func WithLogFilePath(logFilePath string) NewLoggerCallOption { 515 | return func(o *newLoggerOptions) { 516 | o.logFilePath = logFilePath 517 | } 518 | } 519 | 520 | func WithHook(hook logrus.Hook) NewLoggerCallOption { 521 | return func(o *newLoggerOptions) { 522 | o.hook = append(o.hook, hook) 523 | } 524 | } 525 | 526 | func WithAppName(appName string) NewLoggerCallOption { 527 | return func(o *newLoggerOptions) { 528 | o.appName = appName 529 | } 530 | } 531 | 532 | func WithNamespace(namespace string) NewLoggerCallOption { 533 | return func(o *newLoggerOptions) { 534 | o.namespace = namespace 535 | } 536 | } 537 | 538 | func WithInitialFields(fields map[string]any) NewLoggerCallOption { 539 | return func(o *newLoggerOptions) { 540 | o.initialFields = fields 541 | } 542 | } 543 | 544 | func WithCallFrameSkip(skip int) NewLoggerCallOption { 545 | return func(o *newLoggerOptions) { 546 | o.callFrameSkip = skip 547 | } 548 | } 549 | 550 | func WithLokiRemoteConfig(config *loki.Config) NewLoggerCallOption { 551 | return func(o *newLoggerOptions) { 552 | o.lokiRemoteConfig = config 553 | } 554 | } 555 | 556 | func WithOpenTelemetryDisabled() NewLoggerCallOption { 557 | return func(o *newLoggerOptions) { 558 | o.openTelemetryDisabled = true 559 | } 560 | } 561 | 562 | // NewLogger 按需创建 logger 实例。 563 | func NewLogger(callOpts ...NewLoggerCallOption) (*Logger, error) { 564 | opts := new(newLoggerOptions) 565 | opts.callFrameSkip = 2 566 | opts.format = FormatPretty 567 | 568 | for _, opt := range callOpts { 569 | opt(opts) 570 | } 571 | 572 | var err error 573 | if opts.logFilePath != "" { 574 | err = autoCreateLogFile(opts.logFilePath) 575 | if err != nil { 576 | return nil, err 577 | } 578 | } 579 | 580 | config := zap.NewProductionConfig() 581 | config.Level = zap.NewAtomicLevelAt(opts.level) 582 | config.EncoderConfig = zapcore.EncoderConfig{ 583 | TimeKey: "@timestamp", 584 | LevelKey: "level", 585 | MessageKey: "message", 586 | CallerKey: "caller", 587 | FunctionKey: "function", 588 | StacktraceKey: "stack", 589 | NameKey: "logger", 590 | EncodeLevel: zapcore.LowercaseLevelEncoder, 591 | EncodeCaller: zapcore.ShortCallerEncoder, 592 | SkipLineEnding: false, 593 | LineEnding: zapcore.DefaultLineEnding, 594 | EncodeDuration: zapcore.MillisDurationEncoder, 595 | EncodeName: zapcore.FullNameEncoder, 596 | EncodeTime: zapcore.RFC3339NanoTimeEncoder, 597 | } 598 | 599 | config.InitialFields = make(map[string]any) 600 | if opts.appName != "" { 601 | config.InitialFields["app_name"] = opts.appName 602 | } 603 | if opts.namespace != "" { 604 | config.InitialFields["namespace"] = opts.namespace 605 | } 606 | if len(opts.initialFields) > 0 { 607 | for k, v := range opts.initialFields { 608 | config.InitialFields[k] = v 609 | } 610 | } 611 | if opts.logFilePath != "" { 612 | config.OutputPaths = []string{opts.logFilePath} 613 | config.ErrorOutputPaths = []string{opts.logFilePath} 614 | 615 | if opts.format == FormatJSON { 616 | config.OutputPaths = append(config.OutputPaths, "stdout") 617 | config.ErrorOutputPaths = append(config.ErrorOutputPaths, "stderr") 618 | } 619 | } else { 620 | config.OutputPaths = []string{} 621 | config.ErrorOutputPaths = []string{} 622 | 623 | if opts.format == FormatJSON { 624 | config.OutputPaths = append(config.OutputPaths, "stdout") 625 | config.ErrorOutputPaths = append(config.ErrorOutputPaths, "stderr") 626 | } 627 | } 628 | if opts.lokiRemoteConfig != nil { 629 | if opts.appName != "" { 630 | opts.lokiRemoteConfig.Labels["app_name"] = opts.appName 631 | } 632 | if opts.namespace != "" { 633 | opts.lokiRemoteConfig.Labels["namespace"] = opts.namespace 634 | } 635 | 636 | loki := loki.New(context.Background(), *opts.lokiRemoteConfig) 637 | config = loki.ApplyConfig(config) 638 | } 639 | 640 | zapLogger, err := config.Build(zap.WithCaller(true)) 641 | if err != nil { 642 | return nil, err 643 | } 644 | 645 | logrusLogger := logrus.New() 646 | 647 | if len(opts.hook) > 0 { 648 | for _, h := range opts.hook { 649 | logrusLogger.Hooks.Add(h) 650 | } 651 | } 652 | 653 | if opts.format == FormatPretty { 654 | logrusLogger.SetFormatter(NewLogPrettyFormatter()) 655 | logrusLogger.SetOutput(os.Stdout) 656 | logrusLogger.SetReportCaller(true) 657 | logrusLogger.Level = zapCoreLevelToLogrusLevel(opts.level) 658 | } else { 659 | logrusLogger.SetOutput(io.Discard) 660 | } 661 | 662 | l := &Logger{ 663 | LogrusLogger: logrus.NewEntry(logrusLogger), 664 | ZapLogger: zapLogger.WithOptions(zap.AddCallerSkip(opts.callFrameSkip - 2)), 665 | namespace: opts.namespace, 666 | skip: opts.callFrameSkip, 667 | errorStatusLevel: zapcore.ErrorLevel, 668 | caller: true, 669 | stackTrace: false, 670 | openTelemetryDisabled: opts.openTelemetryDisabled, 671 | } 672 | if !opts.openTelemetryDisabled { 673 | l.otelTracer = otel.Tracer("github.com/nekomeowww/xo/logger") 674 | } 675 | 676 | l.Debug("logger init successfully for both logrus and zap", 677 | zap.String("log_file_path", opts.logFilePath), 678 | zap.String("log_level", opts.level.String()), 679 | ) 680 | 681 | return l, nil 682 | } 683 | 684 | func autoCreateLogFile(logFilePathStr string) error { 685 | if logFilePathStr == "" { 686 | return nil 687 | } 688 | 689 | logDir := filepath.Dir(logFilePathStr) 690 | 691 | _, err := os.Stat(logDir) 692 | if err != nil { 693 | if os.IsNotExist(err) { 694 | err2 := os.MkdirAll(logDir, 0755) 695 | if err2 != nil { 696 | return fmt.Errorf("failed to create %s log directory: %w", logDir, err) 697 | } 698 | } else { 699 | return err 700 | } 701 | } 702 | 703 | stat, err := os.Stat(logFilePathStr) 704 | if err != nil { 705 | if os.IsNotExist(err) { 706 | _, err2 := os.Create(logFilePathStr) 707 | if err2 != nil { 708 | return fmt.Errorf("failed to create %s log file: %w", logFilePathStr, err) 709 | } 710 | } else { 711 | return err 712 | } 713 | } 714 | if stat != nil && stat.IsDir() { 715 | return errors.New("path exists but it is a directory") 716 | } 717 | 718 | return nil 719 | } 720 | -------------------------------------------------------------------------------- /logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/google/uuid" 9 | "github.com/nekomeowww/xo" 10 | "github.com/stretchr/testify/require" 11 | "go.opentelemetry.io/contrib/instrumentation/runtime" 12 | "go.opentelemetry.io/otel" 13 | "go.opentelemetry.io/otel/attribute" 14 | "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" 15 | "go.opentelemetry.io/otel/sdk/trace" 16 | "go.opentelemetry.io/otel/sdk/trace/tracetest" 17 | "go.uber.org/zap" 18 | "go.uber.org/zap/zapcore" 19 | ) 20 | 21 | func TestWith(t *testing.T) { 22 | t.Parallel() 23 | t.Run("Debug", func(t *testing.T) { 24 | t.Parallel() 25 | 26 | logger, err := NewLogger( 27 | WithLevel(zapcore.DebugLevel), 28 | WithNamespace("xo/logger"), 29 | WithLogFilePath(xo.RelativePathOf("./logs/test.debug.log")), 30 | WithCallFrameSkip(1), 31 | ) 32 | require.NoError(t, err) 33 | require.NotNil(t, logger) 34 | 35 | newLogger := logger.With(zap.String("some_test_field", "some_test_value")) 36 | require.NotNil(t, newLogger) 37 | 38 | logger.Debug("debug message") 39 | newLogger.Debug("debug message with with") 40 | }) 41 | t.Run("Info", func(t *testing.T) { 42 | t.Parallel() 43 | 44 | logger, err := NewLogger( 45 | WithLevel(zapcore.DebugLevel), 46 | WithNamespace("xo/logger"), 47 | WithLogFilePath(xo.RelativePathOf("./logs/test.info.log")), 48 | WithCallFrameSkip(1), 49 | ) 50 | require.NoError(t, err) 51 | require.NotNil(t, logger) 52 | 53 | newLogger := logger.With(zap.String("some_test_field", "some_test_value")) 54 | require.NotNil(t, newLogger) 55 | 56 | logger.Info("info message") 57 | newLogger.Info("info message with with") 58 | }) 59 | t.Run("Warn", func(t *testing.T) { 60 | t.Parallel() 61 | 62 | logger, err := NewLogger( 63 | WithLevel(zapcore.DebugLevel), 64 | WithNamespace("xo/logger"), 65 | WithLogFilePath(xo.RelativePathOf("./logs/test.warn.log")), 66 | WithCallFrameSkip(1), 67 | ) 68 | require.NoError(t, err) 69 | require.NotNil(t, logger) 70 | 71 | newLogger := logger.With(zap.String("some_test_field", "some_test_value")) 72 | require.NotNil(t, newLogger) 73 | 74 | logger.Warn("warn message") 75 | newLogger.Warn("warn message with with") 76 | }) 77 | t.Run("Error", func(t *testing.T) { 78 | t.Parallel() 79 | 80 | logger, err := NewLogger( 81 | WithLevel(zapcore.DebugLevel), 82 | WithNamespace("xo/logger"), 83 | WithLogFilePath(xo.RelativePathOf("./logs/test.error.log")), 84 | WithCallFrameSkip(1), 85 | ) 86 | require.NoError(t, err) 87 | require.NotNil(t, logger) 88 | 89 | newLogger := logger.With(zap.String("some_test_field", "some_test_value")) 90 | require.NotNil(t, newLogger) 91 | 92 | logger.Error("error message") 93 | newLogger.Error("error message with with") 94 | }) 95 | } 96 | 97 | func TestFormat(t *testing.T) { 98 | t.Run("WithLogFilePath", func(t *testing.T) { 99 | logger, err := NewLogger( 100 | WithFormat(FormatJSON), 101 | WithLogFilePath(xo.RelativePathOf("./logs/test.json.log")), 102 | ) 103 | require.NoError(t, err) 104 | require.NotNil(t, logger) 105 | 106 | logger.Info("info message", zap.String("some_test_field", "some_test_value")) 107 | }) 108 | 109 | t.Run("WithoutLogFilePath", func(t *testing.T) { 110 | logger, err := NewLogger( 111 | WithFormat(FormatJSON), 112 | ) 113 | require.NoError(t, err) 114 | require.NotNil(t, logger) 115 | 116 | logger.Info("info message", zap.String("some_test_field", "some_test_value")) 117 | }) 118 | } 119 | 120 | func TestSimpleSpan(t *testing.T) { 121 | spanExporter, _ := stdouttrace.New(stdouttrace.WithPrettyPrint()) 122 | tp := trace.NewTracerProvider( 123 | trace.WithBatcher(spanExporter), 124 | trace.WithSampler(trace.AlwaysSample()), 125 | ) 126 | otel.SetTracerProvider(tp) 127 | 128 | ctx, span := tp.Tracer("test").Start(context.Background(), "test-span") 129 | span.SetAttributes(attribute.String("key", "value")) 130 | span.End() 131 | 132 | tp.ForceFlush(ctx) 133 | } 134 | 135 | func TestOpenTelemetryAndContextual(t *testing.T) { 136 | spanRecorder := tracetest.NewSpanRecorder() 137 | tracerProvider := trace.NewTracerProvider( 138 | trace.WithSpanProcessor(spanRecorder), 139 | trace.WithSampler(trace.AlwaysSample()), 140 | ) 141 | otel.SetTracerProvider(tracerProvider) 142 | 143 | err := runtime.Start(runtime.WithMinimumReadMemStatsInterval(time.Second)) 144 | require.NoError(t, err) 145 | 146 | t.Parallel() 147 | t.Run("Debug", func(t *testing.T) { 148 | t.Parallel() 149 | 150 | tracer := otel.Tracer("test-tracer") 151 | 152 | ctx, span := tracer.Start(context.Background(), "test-span") 153 | defer span.End() 154 | 155 | // Add some attributes to the span to make it easier to identify 156 | span.SetAttributes(attribute.String("test", "OpenTelemetryAndContextual")) 157 | 158 | logger, err := NewLogger( 159 | WithLevel(zapcore.DebugLevel), 160 | WithNamespace("xo/logger"), 161 | WithLogFilePath(xo.RelativePathOf("./logs/test.debug.log")), 162 | WithCallFrameSkip(1), 163 | ) 164 | require.NoError(t, err) 165 | require.NotNil(t, logger) 166 | 167 | newLogger := logger.With(zap.String("some_test_field", "some_test_value")) 168 | require.NotNil(t, newLogger) 169 | 170 | var uuid *uuid.UUID 171 | 172 | logger.DebugContext(ctx, "debug message", zap.Stringer("uuid", uuid)) 173 | newLogger.DebugContext(ctx, "debug message with with") 174 | 175 | // End the span and force flush 176 | span.End() 177 | 178 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 179 | defer cancel() 180 | 181 | err = tracerProvider.ForceFlush(ctx) 182 | require.NoError(t, err, "Failed to flush trace provider") 183 | 184 | // Retrieve and inspect the recorded spans 185 | spans := spanRecorder.Ended() 186 | require.NotEmpty(t, spans, "Expected at least one span") 187 | 188 | for _, recordedSpan := range spans { 189 | t.Logf("Span Name: %s", recordedSpan.Name()) 190 | t.Logf("Trace ID: %s", recordedSpan.SpanContext().TraceID()) 191 | t.Logf("Span ID: %s", recordedSpan.SpanContext().SpanID()) 192 | 193 | for _, event := range recordedSpan.Events() { 194 | t.Logf("Event: %s", event.Name) 195 | 196 | for _, attr := range event.Attributes { 197 | t.Logf(" Attribute: %s = %v", attr.Key, attr.Value) 198 | } 199 | } 200 | } 201 | }) 202 | } 203 | -------------------------------------------------------------------------------- /logger/loki/loki.go: -------------------------------------------------------------------------------- 1 | package loki 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | "net/url" 12 | "strconv" 13 | "strings" 14 | "sync" 15 | "time" 16 | 17 | "go.uber.org/zap" 18 | "go.uber.org/zap/zapcore" 19 | ) 20 | 21 | type ZapLoki interface { 22 | Hook(e zapcore.Entry) error 23 | Sink(u *url.URL) (zap.Sink, error) 24 | Stop() 25 | WithCreateLogger(zap.Config) (*zap.Logger, error) 26 | ApplyConfig(zap.Config) zap.Config 27 | } 28 | 29 | type Config struct { 30 | // Url of the loki server including http:// or https:// 31 | Url string 32 | // BatchMaxSize is the maximum number of log lines that are sent in one request 33 | BatchMaxSize int 34 | // BatchMaxWait is the maximum time to wait before sending a request 35 | BatchMaxWait time.Duration 36 | // Labels that are added to all log lines 37 | Labels map[string]string 38 | Username string 39 | Password string 40 | PrintErrors bool 41 | } 42 | 43 | type lokiPusher struct { 44 | config *Config 45 | ctx context.Context //nolint 46 | cancel context.CancelFunc 47 | client *http.Client 48 | quit chan struct{} 49 | entry chan logEntry 50 | waitGroup sync.WaitGroup 51 | logsBatch []streamValue 52 | } 53 | 54 | type lokiPushRequest struct { 55 | Streams []stream `json:"streams"` 56 | } 57 | 58 | type stream struct { 59 | Stream map[string]string `json:"stream"` 60 | Values []streamValue `json:"values"` 61 | } 62 | 63 | type streamValue []string 64 | 65 | type logEntry struct { 66 | Level string `json:"level"` 67 | Timestamp string `json:"@timestamp"` 68 | Message string `json:"message"` 69 | Caller string `json:"caller"` 70 | Function string `json:"function"` 71 | Stack string `json:"stack"` 72 | raw string 73 | } 74 | 75 | func New(ctx context.Context, cfg Config) ZapLoki { 76 | cfg.Url = fmt.Sprintf("%s/loki/api/v1/push", strings.TrimSuffix(cfg.Url, "/")) 77 | 78 | ctx, cancel := context.WithCancel(ctx) 79 | lp := &lokiPusher{ 80 | config: &cfg, 81 | ctx: ctx, 82 | cancel: cancel, 83 | client: &http.Client{}, 84 | quit: make(chan struct{}), 85 | entry: make(chan logEntry), 86 | logsBatch: make([]streamValue, 0, cfg.BatchMaxSize), 87 | } 88 | 89 | lp.waitGroup.Add(1) 90 | go lp.run() 91 | 92 | return lp 93 | } 94 | 95 | // Hook is a function that can be used as a zap hook to write log lines to loki. 96 | func (lp *lokiPusher) Hook(e zapcore.Entry) error { 97 | lp.entry <- logEntry{ 98 | Level: e.Level.String(), 99 | Timestamp: e.Time.Format(time.RFC3339Nano), 100 | Message: e.Message, 101 | Caller: e.Caller.TrimmedPath(), 102 | } 103 | 104 | return nil 105 | } 106 | 107 | // Sink returns a new loki zap sink. 108 | func (lp *lokiPusher) Sink(_ *url.URL) (zap.Sink, error) { 109 | return newSink(lp), nil 110 | } 111 | 112 | // Stop stops the loki pusher. 113 | func (lp *lokiPusher) Stop() { 114 | close(lp.quit) 115 | lp.waitGroup.Wait() 116 | lp.cancel() 117 | } 118 | 119 | // WithCreateLogger creates a new zap logger with a loki sink from a zap config. 120 | func (lp *lokiPusher) WithCreateLogger(cfg zap.Config) (*zap.Logger, error) { 121 | err := zap.RegisterSink(lokiSinkKey, lp.Sink) 122 | if err != nil { 123 | log.Fatal(err) 124 | } 125 | 126 | fullSinkKey := fmt.Sprintf("%s://", lokiSinkKey) 127 | 128 | if cfg.OutputPaths == nil { 129 | cfg.OutputPaths = []string{fullSinkKey} 130 | } else { 131 | cfg.OutputPaths = append(cfg.OutputPaths, fullSinkKey) 132 | } 133 | 134 | return cfg.Build() 135 | } 136 | 137 | func (lp *lokiPusher) ApplyConfig(cfg zap.Config) zap.Config { 138 | err := zap.RegisterSink(lokiSinkKey, lp.Sink) 139 | if err != nil { 140 | log.Fatal(err) 141 | } 142 | 143 | fullSinkKey := fmt.Sprintf("%s://", lokiSinkKey) 144 | 145 | if cfg.OutputPaths == nil { 146 | cfg.OutputPaths = []string{fullSinkKey} 147 | } else { 148 | cfg.OutputPaths = append(cfg.OutputPaths, fullSinkKey) 149 | } 150 | 151 | return cfg 152 | } 153 | 154 | func (lp *lokiPusher) run() { 155 | ticker := time.NewTimer(lp.config.BatchMaxWait) 156 | defer ticker.Stop() 157 | 158 | defer func() { 159 | if len(lp.logsBatch) > 0 { 160 | _ = lp.send() 161 | } 162 | 163 | lp.waitGroup.Done() 164 | }() 165 | 166 | for { 167 | select { 168 | case <-lp.ctx.Done(): 169 | return 170 | case <-lp.quit: 171 | return 172 | case entry := <-lp.entry: 173 | lp.logsBatch = append(lp.logsBatch, newLog(entry)) 174 | if len(lp.logsBatch) >= lp.config.BatchMaxSize { 175 | err := lp.send() 176 | if err != nil && lp.config.PrintErrors { 177 | log.Printf("failed to send logs to Loki: %v", err) 178 | } 179 | 180 | lp.logsBatch = lp.logsBatch[:0] 181 | } 182 | case <-ticker.C: 183 | if len(lp.logsBatch) > 0 { 184 | err := lp.send() 185 | if err != nil && lp.config.PrintErrors { 186 | log.Printf("failed to send logs to Loki: %v", err) 187 | } 188 | 189 | lp.logsBatch = lp.logsBatch[:0] 190 | } 191 | } 192 | } 193 | } 194 | 195 | func newLog(entry logEntry) streamValue { 196 | var ts time.Time 197 | 198 | if entry.Timestamp == "" { 199 | ts = time.Now() 200 | return []string{strconv.FormatInt(ts.UnixNano(), 10), entry.raw} 201 | } 202 | 203 | now, err := time.Parse(time.RFC3339Nano, entry.Timestamp) 204 | if err != nil { 205 | ts = time.Now() 206 | } else { 207 | ts = now 208 | } 209 | 210 | return []string{strconv.FormatInt(ts.UnixNano(), 10), entry.raw} 211 | } 212 | 213 | func (lp *lokiPusher) send() error { 214 | buf := bytes.NewBuffer([]byte{}) 215 | gz := gzip.NewWriter(buf) 216 | 217 | if err := json.NewEncoder(gz).Encode(lokiPushRequest{Streams: []stream{{ 218 | Stream: lp.config.Labels, 219 | Values: lp.logsBatch, 220 | }}}); err != nil { 221 | return err 222 | } 223 | 224 | if err := gz.Close(); err != nil { 225 | return err 226 | } 227 | 228 | req, err := http.NewRequestWithContext(lp.ctx, http.MethodPost, lp.config.Url, buf) 229 | if err != nil { 230 | return fmt.Errorf("failed to create request: %w", err) 231 | } 232 | 233 | req.Header.Set("Content-Type", "application/json") 234 | req.Header.Set("Content-Encoding", "gzip") 235 | 236 | if lp.config.Username != "" && lp.config.Password != "" { 237 | req.SetBasicAuth(lp.config.Username, lp.config.Password) 238 | } 239 | 240 | resp, err := lp.client.Do(req) 241 | if err != nil { 242 | return fmt.Errorf("failed to send request: %w", err) 243 | } 244 | 245 | defer resp.Body.Close() 246 | 247 | if resp.StatusCode != http.StatusNoContent { 248 | return fmt.Errorf("recieved unexpected response code from Loki: %s", resp.Status) 249 | } 250 | 251 | return nil 252 | } 253 | -------------------------------------------------------------------------------- /logger/loki/sink.go: -------------------------------------------------------------------------------- 1 | package loki 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | const lokiSinkKey = "loki" 8 | 9 | type lokiSink interface { 10 | Sync() error 11 | Close() error 12 | Write(p []byte) (int, error) 13 | } 14 | 15 | // type lokiSink struct{}. 16 | type sink struct { 17 | lokiPusher *lokiPusher 18 | } 19 | 20 | func newSink(lp *lokiPusher) lokiSink { 21 | return sink{ 22 | lokiPusher: lp, 23 | } 24 | } 25 | 26 | func (s sink) Sync() error { 27 | if len(s.lokiPusher.logsBatch) > 0 { 28 | return s.lokiPusher.send() 29 | } 30 | 31 | return nil 32 | } 33 | func (s sink) Close() error { 34 | s.lokiPusher.Stop() 35 | return nil 36 | } 37 | 38 | func (s sink) Write(p []byte) (int, error) { 39 | var entry logEntry 40 | 41 | err := json.Unmarshal(p, &entry) 42 | if err != nil { 43 | return 0, err 44 | } 45 | 46 | entry.raw = string(p) 47 | s.lokiPusher.entry <- entry 48 | 49 | return len(p), nil 50 | } 51 | -------------------------------------------------------------------------------- /logger/otelzap/arrayencoder.go: -------------------------------------------------------------------------------- 1 | package otelzap 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "go.uber.org/zap/zapcore" 8 | ) 9 | 10 | // BufferArrayEncoder implements zapcore.BufferArrayEncoder. 11 | // It represents all added objects to their string values and 12 | // adds them to the stringsSlice buffer. 13 | type BufferArrayEncoder struct { 14 | stringsSlice []string 15 | } 16 | 17 | var _ zapcore.ArrayEncoder = (*BufferArrayEncoder)(nil) 18 | 19 | func NewBufferArrayEncoder() *BufferArrayEncoder { 20 | return &BufferArrayEncoder{ 21 | stringsSlice: make([]string, 0), 22 | } 23 | } 24 | 25 | func (t *BufferArrayEncoder) Result() []string { 26 | return t.stringsSlice 27 | } 28 | 29 | func (t *BufferArrayEncoder) AppendComplex128(v complex128) { 30 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 31 | } 32 | 33 | func (t *BufferArrayEncoder) AppendComplex64(v complex64) { 34 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 35 | } 36 | 37 | func (t *BufferArrayEncoder) AppendArray(v zapcore.ArrayMarshaler) error { 38 | enc := &BufferArrayEncoder{} 39 | err := v.MarshalLogArray(enc) 40 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", enc.stringsSlice)) 41 | 42 | return err 43 | } 44 | 45 | func (t *BufferArrayEncoder) AppendObject(v zapcore.ObjectMarshaler) error { 46 | m := zapcore.NewMapObjectEncoder() 47 | err := v.MarshalLogObject(m) 48 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", m.Fields)) 49 | 50 | return err 51 | } 52 | 53 | func (t *BufferArrayEncoder) AppendReflected(v interface{}) error { 54 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 55 | return nil 56 | } 57 | 58 | func (t *BufferArrayEncoder) AppendBool(v bool) { 59 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 60 | } 61 | 62 | func (t *BufferArrayEncoder) AppendByteString(v []byte) { 63 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 64 | } 65 | 66 | func (t *BufferArrayEncoder) AppendDuration(v time.Duration) { 67 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 68 | } 69 | 70 | func (t *BufferArrayEncoder) AppendFloat64(v float64) { 71 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 72 | } 73 | 74 | func (t *BufferArrayEncoder) AppendFloat32(v float32) { 75 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 76 | } 77 | 78 | func (t *BufferArrayEncoder) AppendInt(v int) { 79 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 80 | } 81 | 82 | func (t *BufferArrayEncoder) AppendInt64(v int64) { 83 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 84 | } 85 | 86 | func (t *BufferArrayEncoder) AppendInt32(v int32) { 87 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 88 | } 89 | 90 | func (t *BufferArrayEncoder) AppendInt16(v int16) { 91 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 92 | } 93 | 94 | func (t *BufferArrayEncoder) AppendInt8(v int8) { 95 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 96 | } 97 | 98 | func (t *BufferArrayEncoder) AppendString(v string) { 99 | t.stringsSlice = append(t.stringsSlice, v) 100 | } 101 | 102 | func (t *BufferArrayEncoder) AppendTime(v time.Time) { 103 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 104 | } 105 | 106 | func (t *BufferArrayEncoder) AppendUint(v uint) { 107 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 108 | } 109 | 110 | func (t *BufferArrayEncoder) AppendUint64(v uint64) { 111 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 112 | } 113 | 114 | func (t *BufferArrayEncoder) AppendUint32(v uint32) { 115 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 116 | } 117 | 118 | func (t *BufferArrayEncoder) AppendUint16(v uint16) { 119 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 120 | } 121 | 122 | func (t *BufferArrayEncoder) AppendUint8(v uint8) { 123 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 124 | } 125 | 126 | func (t *BufferArrayEncoder) AppendUintptr(v uintptr) { 127 | t.stringsSlice = append(t.stringsSlice, fmt.Sprintf("%v", v)) 128 | } 129 | -------------------------------------------------------------------------------- /logger/otelzap/otelzap.go: -------------------------------------------------------------------------------- 1 | package otelzap 2 | -------------------------------------------------------------------------------- /logger/otelzap/utils.go: -------------------------------------------------------------------------------- 1 | package otelzap 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "math" 7 | "reflect" 8 | "time" 9 | 10 | "github.com/samber/lo" 11 | "go.opentelemetry.io/otel/attribute" 12 | "go.opentelemetry.io/otel/log" 13 | semconv "go.opentelemetry.io/otel/semconv/v1.17.0" 14 | "go.uber.org/zap" 15 | "go.uber.org/zap/zapcore" 16 | ) 17 | 18 | func LogSeverityFromZapLevel(level zapcore.Level) log.Severity { 19 | switch level { 20 | case zapcore.DebugLevel: 21 | return log.SeverityDebug 22 | case zapcore.InfoLevel: 23 | return log.SeverityInfo 24 | case zapcore.WarnLevel: 25 | return log.SeverityWarn 26 | case zapcore.ErrorLevel: 27 | return log.SeverityError 28 | case zapcore.DPanicLevel: 29 | return log.SeverityFatal1 30 | case zapcore.PanicLevel: 31 | return log.SeverityFatal2 32 | case zapcore.FatalLevel: 33 | return log.SeverityFatal3 34 | case zapcore.InvalidLevel: 35 | return log.SeverityUndefined 36 | default: 37 | return log.SeverityUndefined 38 | } 39 | } 40 | 41 | func attributeKey(k string) string { 42 | return "log.fields." + k 43 | } 44 | 45 | func AttributesFromZapField(f zap.Field) []attribute.KeyValue { 46 | switch f.Type { 47 | case zapcore.BoolType: 48 | return []attribute.KeyValue{ 49 | attribute.Bool(attributeKey(f.Key), f.Integer == 1), 50 | } 51 | case zapcore.Int8Type, zapcore.Int16Type, zapcore.Int32Type, zapcore.Int64Type, 52 | zapcore.Uint32Type, zapcore.Uint8Type, zapcore.Uint16Type, zapcore.Uint64Type, 53 | zapcore.UintptrType: 54 | return []attribute.KeyValue{ 55 | attribute.Int64(attributeKey(f.Key), f.Integer), 56 | } 57 | case zapcore.Float32Type, zapcore.Float64Type: 58 | return []attribute.KeyValue{ 59 | attribute.Float64(attributeKey(f.Key), math.Float64frombits(uint64(f.Integer))), //nolint 60 | } 61 | case zapcore.Complex64Type, zapcore.Complex128Type: 62 | return []attribute.KeyValue{ 63 | attribute.String(attributeKey(f.Key), fmt.Sprintf("%v", f.Interface)), 64 | } 65 | case zapcore.StringType: 66 | return []attribute.KeyValue{ 67 | attribute.String(attributeKey(f.Key), f.String), 68 | } 69 | case zapcore.StringerType: 70 | val, ok := f.Interface.(fmt.Stringer) 71 | if !ok { 72 | return []attribute.KeyValue{ 73 | attribute.String(attributeKey(f.Key), fmt.Sprintf("expected fmt.Stringer, got %T, v: %v", f.Interface, f.Interface)), 74 | } 75 | } 76 | if lo.IsNil(val) { 77 | return []attribute.KeyValue{ 78 | attribute.String(attributeKey(f.Key), ""), 79 | } 80 | } 81 | 82 | return []attribute.KeyValue{ 83 | attribute.String(attributeKey(f.Key), fmt.Sprint(val)), 84 | } 85 | case zapcore.BinaryType, zapcore.ByteStringType: 86 | val, ok := f.Interface.([]byte) 87 | if !ok { 88 | return []attribute.KeyValue{ 89 | attribute.String(attributeKey(f.Key), fmt.Sprintf("expected []byte, got %T, v: %v", f.Interface, f.Interface)), 90 | } 91 | } 92 | if lo.IsNil(val) { 93 | return []attribute.KeyValue{ 94 | attribute.String(attributeKey(f.Key), ""), 95 | } 96 | } 97 | 98 | return []attribute.KeyValue{ 99 | attribute.String(attributeKey(f.Key), base64.StdEncoding.EncodeToString(val)), 100 | } 101 | case zapcore.DurationType: 102 | val, ok := f.Interface.(time.Duration) 103 | if !ok { 104 | return []attribute.KeyValue{ 105 | attribute.String(attributeKey(f.Key), fmt.Sprintf("expected time.Duration, got %T, v: %v", f.Interface, f.Interface)), 106 | } 107 | } 108 | 109 | return []attribute.KeyValue{ 110 | attribute.String(attributeKey(f.Key), val.String()), 111 | } 112 | case zapcore.TimeType, zapcore.TimeFullType: 113 | val, ok := f.Interface.(time.Time) 114 | if !ok { 115 | return []attribute.KeyValue{ 116 | attribute.String(attributeKey(f.Key), fmt.Sprintf("expected time.Time, got %T, v: %v", f.Interface, f.Interface)), 117 | } 118 | } 119 | 120 | return []attribute.KeyValue{ 121 | attribute.String(attributeKey(f.Key), val.Format(time.RFC3339Nano)), 122 | } 123 | case zapcore.ErrorType: 124 | err, ok := f.Interface.(error) 125 | if !ok { 126 | return []attribute.KeyValue{attribute.String(attributeKey(f.Key), fmt.Sprintf("expected error, got %T", f.Interface))} 127 | } 128 | if lo.IsNil(err) { 129 | return []attribute.KeyValue{ 130 | attribute.String(attributeKey(f.Key), ""), 131 | } 132 | } 133 | 134 | return []attribute.KeyValue{ 135 | semconv.ExceptionTypeKey.String(reflect.TypeOf(err).String()), 136 | semconv.ExceptionMessageKey.String(err.Error()), 137 | } 138 | case zapcore.ObjectMarshalerType, zapcore.InlineMarshalerType: 139 | if marshaler, ok := f.Interface.(zapcore.ObjectMarshaler); ok { 140 | if lo.IsNil(marshaler) { 141 | return []attribute.KeyValue{ 142 | attribute.String(attributeKey(f.Key), ""), 143 | } 144 | } 145 | 146 | encoder := zapcore.NewMapObjectEncoder() 147 | if err := marshaler.MarshalLogObject(encoder); err == nil { 148 | return flattenMap(attributeKey(f.Key), encoder.Fields) 149 | } 150 | } 151 | 152 | return []attribute.KeyValue{ 153 | attribute.String(attributeKey(f.Key), fmt.Sprintf("%v", f.Interface)), 154 | } 155 | case zapcore.ArrayMarshalerType: 156 | if marshaler, ok := f.Interface.(zapcore.ArrayMarshaler); ok { 157 | if lo.IsNil(marshaler) { 158 | return []attribute.KeyValue{ 159 | attribute.String(attributeKey(f.Key), ""), 160 | } 161 | } 162 | 163 | encoder := NewBufferArrayEncoder() 164 | if err := marshaler.MarshalLogArray(encoder); err == nil { 165 | return []attribute.KeyValue{ 166 | attribute.StringSlice(attributeKey(f.Key), encoder.Result()), 167 | } 168 | } 169 | } 170 | 171 | return []attribute.KeyValue{ 172 | attribute.String(attributeKey(f.Key), fmt.Sprintf("%v", f.Interface)), 173 | } 174 | case zapcore.ReflectType: 175 | str := fmt.Sprint(f.Interface) 176 | 177 | return []attribute.KeyValue{ 178 | attribute.String(attributeKey(f.Key), str), 179 | } 180 | case zapcore.NamespaceType: 181 | return []attribute.KeyValue{} 182 | case zapcore.SkipType: 183 | return []attribute.KeyValue{} 184 | case zapcore.UnknownType: 185 | return []attribute.KeyValue{ 186 | attribute.String(attributeKey(f.Key), fmt.Sprintf("%v", f.Interface)), 187 | } 188 | default: 189 | return []attribute.KeyValue{ 190 | attribute.String(attributeKey(f.Key), fmt.Sprintf("%v", f.Interface)), 191 | } 192 | } 193 | } 194 | 195 | func flattenMap(prefix string, m map[string]any) []attribute.KeyValue { 196 | if m == nil { 197 | return make([]attribute.KeyValue, 0) 198 | } 199 | 200 | attrs := make([]attribute.KeyValue, 0, len(m)) 201 | 202 | for k, v := range m { 203 | key := prefix + "." + k 204 | switch val := v.(type) { 205 | case bool: 206 | attrs = append(attrs, attribute.Bool(key, val)) 207 | case int: 208 | attrs = append(attrs, attribute.Int(key, val)) 209 | case int64: 210 | attrs = append(attrs, attribute.Int64(key, val)) 211 | case float64: 212 | attrs = append(attrs, attribute.Float64(key, val)) 213 | case string: 214 | attrs = append(attrs, attribute.String(key, val)) 215 | case []interface{}: 216 | attrs = append(attrs, attribute.StringSlice(key, interfaceSliceToStringSlice(val))) 217 | case map[string]interface{}: 218 | attrs = append(attrs, flattenMap(key, val)...) 219 | default: 220 | attrs = append(attrs, attribute.String(key, fmt.Sprintf("%v", val))) 221 | } 222 | } 223 | 224 | return attrs 225 | } 226 | 227 | func interfaceSliceToStringSlice(slice []any) []string { 228 | if slice == nil { 229 | return make([]string, 0) 230 | } 231 | 232 | result := make([]string, len(slice)) 233 | for i, v := range slice { 234 | result[i] = fmt.Sprintf("%v", v) 235 | } 236 | 237 | return result 238 | } 239 | -------------------------------------------------------------------------------- /opqcursor/opqcursor.go: -------------------------------------------------------------------------------- 1 | package opqcursor 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/samber/lo" 10 | ) 11 | 12 | // OpaqueCursor is a struct that contains the filters and sorters for a query. 13 | // Generally used in GraphQL based APIs to pass the cursor as a string. But the scenarios 14 | // are not limited to GraphQL only, you could use it in any API that requires a cursor. 15 | type OpaqueCursor[F any, S any] struct { 16 | Filters F `json:"filters"` 17 | Sorters S `json:"sorters"` 18 | } 19 | 20 | // IsValid checks if the cursor is valid or not. It checks if the sorters are valid or not. 21 | // 22 | // NOTICE: currently the sorters are checked for ASC and DESC only. 23 | func (oc OpaqueCursor[F, S]) IsValid() bool { 24 | refValue := reflect.ValueOf(oc.Sorters) 25 | if refValue.Kind() == reflect.Struct { 26 | for i := 0; i < refValue.NumField(); i++ { 27 | if lo.Contains([]string{"ASC", "DESC"}, strings.ToUpper(refValue.Field(i).String())) { 28 | return true 29 | } 30 | } 31 | } 32 | 33 | return false 34 | } 35 | 36 | // Unmarshal decodes the cursor string and returns the OpaqueCursor struct. 37 | func Unmarshal[Filters any, Sorters any](cursor string) (*OpaqueCursor[Filters, Sorters], error) { 38 | var cursorData OpaqueCursor[Filters, Sorters] 39 | 40 | base64Data, err := base64.StdEncoding.DecodeString(cursor) 41 | if err != nil { 42 | return &cursorData, err 43 | } 44 | 45 | err = json.Unmarshal(base64Data, &cursorData) 46 | if err != nil { 47 | return &cursorData, err 48 | } 49 | 50 | return &cursorData, nil 51 | } 52 | 53 | // UnmarshalWithDefaults decodes the cursor string and returns the OpaqueCursor struct with defaults additionally. 54 | func UnmarshalWithDefaults[Filters any, Sorters any](cursor string, defaults OpaqueCursor[Filters, Sorters]) (*OpaqueCursor[Filters, Sorters], error) { 55 | if cursor == "" { 56 | return &defaults, nil 57 | } 58 | 59 | var cursorData OpaqueCursor[Filters, Sorters] 60 | 61 | base64Data, err := base64.StdEncoding.DecodeString(cursor) 62 | if err != nil { 63 | return &cursorData, err 64 | } 65 | 66 | err = json.Unmarshal(base64Data, &cursorData) 67 | if err != nil { 68 | return &defaults, err 69 | } 70 | 71 | return &cursorData, nil 72 | } 73 | 74 | // Marshal encodes the OpaqueCursor struct and returns the cursor string. 75 | func Marshal[Filters any, Sorters any](filters Filters, sorters Sorters) (string, error) { 76 | cursorData := OpaqueCursor[Filters, Sorters]{ 77 | Filters: filters, 78 | Sorters: sorters, 79 | } 80 | 81 | cursor, err := json.Marshal(cursorData) 82 | if err != nil { 83 | return "{}", err 84 | } 85 | 86 | return base64.StdEncoding.EncodeToString(cursor), nil 87 | } 88 | -------------------------------------------------------------------------------- /pagination/pagination.go: -------------------------------------------------------------------------------- 1 | package pagination 2 | 3 | const ( 4 | // DefaultPageSize is the default page size. 5 | DefaultPageSize = 20 6 | // MaxPageSize is the max page size. 7 | MaxPageSize = 99 8 | ) 9 | 10 | /* 11 | Pagination is a struct for pagination. 12 | 13 | NOTICE: Make sure that Pagination.Valid() returns true before querying. 14 | */ 15 | type Pagination struct { 16 | Page int64 `form:"page" json:"page" schema:"page" example:"1"` 17 | PageSize int64 `form:"pageSize" json:"pageSize" schema:"pageSize" example:"20"` 18 | Count int64 `form:"count" json:"count" schema:"count" example:"100"` 19 | MaxPage int64 `form:"maxPage" json:"maxPage" schema:"maxPage" example:"5"` 20 | } 21 | 22 | // Valid determines whether the pagination is valid. 23 | func (pa Pagination) Valid() bool { 24 | return 1 <= pa.Page && pa.Page <= pa.MaxPage 25 | } 26 | 27 | // Offset returns the offset of the query. 28 | // 29 | // NOTICE: Make sure that pa.Valid() returns true. 30 | func (pa Pagination) Offset() int64 { 31 | return pa.PageSize * (pa.Page - 1) 32 | } 33 | 34 | // Limit returns the limit of the query. 35 | // 36 | // NOTICE: Make sure that pa.Valid() returns true. 37 | func (pa Pagination) Limit() int64 { 38 | remain := pa.Count - pa.Offset() 39 | if pa.PageSize < remain { 40 | return pa.PageSize 41 | } 42 | 43 | return remain 44 | } 45 | 46 | // calcMaxPage calculates the max page. 47 | func (pa *Pagination) calcMaxPage() { 48 | pa.MaxPage = pa.Count / pa.PageSize 49 | if pa.Count%pa.PageSize != 0 { 50 | pa.MaxPage++ 51 | } 52 | } 53 | 54 | // New creates a new pagination. 55 | func New(page, pageSize, count int64) Pagination { 56 | if pageSize <= 0 { 57 | pageSize = DefaultPageSize 58 | } 59 | if pageSize > MaxPageSize { 60 | pageSize = MaxPageSize 61 | } 62 | 63 | pa := Pagination{Count: count, Page: page, PageSize: pageSize} 64 | pa.calcMaxPage() 65 | 66 | return pa 67 | } 68 | 69 | // Logically logically paginates the slice. 70 | func Logically[T any](s []T, page, pageSize int64) []T { 71 | start := (page - 1) * pageSize 72 | if page <= 0 { 73 | start = 0 74 | } 75 | 76 | end := start + pageSize 77 | newSlice := make([]T, 0) 78 | 79 | if page <= 0 { 80 | return make([]T, 0) 81 | } 82 | if start >= int64(len(s)) { 83 | return make([]T, 0) 84 | } 85 | 86 | for i := start; i < int64(len(s)) && i < end; i++ { 87 | newSlice = append(newSlice, s[i]) 88 | } 89 | 90 | return newSlice 91 | } 92 | -------------------------------------------------------------------------------- /pagination/pagination_test.go: -------------------------------------------------------------------------------- 1 | package pagination 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPagination(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | pagination := New(1, 20, 101) 14 | assert.EqualValues(0, pagination.Offset()) 15 | 16 | pagination = New(6, 20, 101) 17 | assert.EqualValues(6, pagination.MaxPage) 18 | assert.EqualValues(100, pagination.Offset()) 19 | assert.True(pagination.Valid()) 20 | 21 | pagination = New(7, 20, 101) 22 | assert.EqualValues(6, pagination.MaxPage) 23 | assert.False(pagination.Valid()) 24 | 25 | pagination = New(1, 20, 0) 26 | assert.False(pagination.Valid()) 27 | } 28 | 29 | func TestPaginationLimit(t *testing.T) { 30 | assert := assert.New(t) 31 | 32 | pa := New(1, 20, 10) 33 | assert.True(pa.Valid() && pa.Limit() == 10) 34 | 35 | pa = New(4, 30, 100) 36 | assert.Equal(int64(10), pa.Limit()) 37 | assert.True(pa.Valid() && pa.Limit() == 10) 38 | } 39 | 40 | func TestLogically(t *testing.T) { 41 | t.Run("DefaultInt64", func(t *testing.T) { 42 | assert := assert.New(t) 43 | 44 | list := make([]int64, 100) 45 | for i := range list { 46 | list[i] = int64(i) 47 | } 48 | 49 | slicedList := Logically(list, 1, 10) 50 | assert.Len(slicedList, 10) 51 | assert.ElementsMatch(list[:10], slicedList) 52 | }) 53 | 54 | t.Run("DefaultString", func(t *testing.T) { 55 | assert := assert.New(t) 56 | 57 | list := make([]string, 100) 58 | for i := range list { 59 | list[i] = fmt.Sprintf("%d", i) 60 | } 61 | 62 | slicedList := Logically(list, 1, 10) 63 | assert.Len(slicedList, 10) 64 | assert.ElementsMatch(list[:10], slicedList) 65 | }) 66 | 67 | t.Run("DefaultStruct", func(t *testing.T) { 68 | assert := assert.New(t) 69 | 70 | type T struct { 71 | ID string 72 | } 73 | 74 | list := make([]*T, 100) 75 | 76 | for i := range list { 77 | list[i] = &T{ 78 | ID: fmt.Sprintf("%d", i), 79 | } 80 | } 81 | 82 | slicedList := Logically(list, 1, 10) 83 | assert.Len(slicedList, 10) 84 | assert.ElementsMatch(list[:10], slicedList) 85 | }) 86 | 87 | t.Run("InvalidPagination", func(t *testing.T) { 88 | assert := assert.New(t) 89 | 90 | slicedList := Logically([]int64{}, 0, 10) 91 | assert.Len(slicedList, 0) 92 | 93 | slicedList = Logically([]int64{}, 1, 10) 94 | assert.Len(slicedList, 0) 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | ) 8 | 9 | func RelativePathOf(fp string) string { 10 | _, file, _, ok := runtime.Caller(1) 11 | if !ok { 12 | return "" 13 | } 14 | 15 | callerDir := filepath.Dir(filepath.FromSlash(file)) 16 | 17 | return filepath.FromSlash(filepath.Join(callerDir, fp)) 18 | } 19 | 20 | func RelativePathBasedOnPwdOf(fp string) string { 21 | dir, err := os.Getwd() 22 | if err != nil { 23 | return "" 24 | } 25 | 26 | return filepath.FromSlash(filepath.Join(dir, fp)) 27 | } 28 | -------------------------------------------------------------------------------- /protobufs/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | plugins: 3 | - remote: buf.build/grpc/go:v1.3.0 4 | out: . 5 | opt: paths=source_relative 6 | 7 | - remote: buf.build/protocolbuffers/go:v1.34.0 8 | out: . 9 | opt: paths=source_relative 10 | -------------------------------------------------------------------------------- /protobufs/buf.lock: -------------------------------------------------------------------------------- 1 | # Generated by buf. DO NOT EDIT. 2 | version: v2 3 | -------------------------------------------------------------------------------- /protobufs/buf.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | lint: 3 | use: 4 | - DEFAULT 5 | disallow_comment_ignores: true 6 | breaking: 7 | use: 8 | - FILE 9 | except: 10 | - EXTENSION_NO_DELETE 11 | - FIELD_SAME_DEFAULT 12 | -------------------------------------------------------------------------------- /protobufs/testpb/test.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.34.0 4 | // protoc (unknown) 5 | // source: testpb/test.proto 6 | 7 | package testpb 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type PossibleOne struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Property_1 string `protobuf:"bytes,1,opt,name=property_1,json=property1,proto3" json:"property_1,omitempty"` 29 | Property_2 string `protobuf:"bytes,2,opt,name=property_2,json=property2,proto3" json:"property_2,omitempty"` 30 | } 31 | 32 | func (x *PossibleOne) Reset() { 33 | *x = PossibleOne{} 34 | if protoimpl.UnsafeEnabled { 35 | mi := &file_testpb_test_proto_msgTypes[0] 36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 37 | ms.StoreMessageInfo(mi) 38 | } 39 | } 40 | 41 | func (x *PossibleOne) String() string { 42 | return protoimpl.X.MessageStringOf(x) 43 | } 44 | 45 | func (*PossibleOne) ProtoMessage() {} 46 | 47 | func (x *PossibleOne) ProtoReflect() protoreflect.Message { 48 | mi := &file_testpb_test_proto_msgTypes[0] 49 | if protoimpl.UnsafeEnabled && x != nil { 50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 51 | if ms.LoadMessageInfo() == nil { 52 | ms.StoreMessageInfo(mi) 53 | } 54 | return ms 55 | } 56 | return mi.MessageOf(x) 57 | } 58 | 59 | // Deprecated: Use PossibleOne.ProtoReflect.Descriptor instead. 60 | func (*PossibleOne) Descriptor() ([]byte, []int) { 61 | return file_testpb_test_proto_rawDescGZIP(), []int{0} 62 | } 63 | 64 | func (x *PossibleOne) GetProperty_1() string { 65 | if x != nil { 66 | return x.Property_1 67 | } 68 | return "" 69 | } 70 | 71 | func (x *PossibleOne) GetProperty_2() string { 72 | if x != nil { 73 | return x.Property_2 74 | } 75 | return "" 76 | } 77 | 78 | type PossibleTwo struct { 79 | state protoimpl.MessageState 80 | sizeCache protoimpl.SizeCache 81 | unknownFields protoimpl.UnknownFields 82 | 83 | Property_1 bool `protobuf:"varint,1,opt,name=property_1,json=property1,proto3" json:"property_1,omitempty"` 84 | Property_2 bool `protobuf:"varint,2,opt,name=property_2,json=property2,proto3" json:"property_2,omitempty"` 85 | } 86 | 87 | func (x *PossibleTwo) Reset() { 88 | *x = PossibleTwo{} 89 | if protoimpl.UnsafeEnabled { 90 | mi := &file_testpb_test_proto_msgTypes[1] 91 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 92 | ms.StoreMessageInfo(mi) 93 | } 94 | } 95 | 96 | func (x *PossibleTwo) String() string { 97 | return protoimpl.X.MessageStringOf(x) 98 | } 99 | 100 | func (*PossibleTwo) ProtoMessage() {} 101 | 102 | func (x *PossibleTwo) ProtoReflect() protoreflect.Message { 103 | mi := &file_testpb_test_proto_msgTypes[1] 104 | if protoimpl.UnsafeEnabled && x != nil { 105 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 106 | if ms.LoadMessageInfo() == nil { 107 | ms.StoreMessageInfo(mi) 108 | } 109 | return ms 110 | } 111 | return mi.MessageOf(x) 112 | } 113 | 114 | // Deprecated: Use PossibleTwo.ProtoReflect.Descriptor instead. 115 | func (*PossibleTwo) Descriptor() ([]byte, []int) { 116 | return file_testpb_test_proto_rawDescGZIP(), []int{1} 117 | } 118 | 119 | func (x *PossibleTwo) GetProperty_1() bool { 120 | if x != nil { 121 | return x.Property_1 122 | } 123 | return false 124 | } 125 | 126 | func (x *PossibleTwo) GetProperty_2() bool { 127 | if x != nil { 128 | return x.Property_2 129 | } 130 | return false 131 | } 132 | 133 | type PossibleThree struct { 134 | state protoimpl.MessageState 135 | sizeCache protoimpl.SizeCache 136 | unknownFields protoimpl.UnknownFields 137 | 138 | Property_1 float64 `protobuf:"fixed64,1,opt,name=property_1,json=property1,proto3" json:"property_1,omitempty"` 139 | Property_2 float64 `protobuf:"fixed64,2,opt,name=property_2,json=property2,proto3" json:"property_2,omitempty"` 140 | } 141 | 142 | func (x *PossibleThree) Reset() { 143 | *x = PossibleThree{} 144 | if protoimpl.UnsafeEnabled { 145 | mi := &file_testpb_test_proto_msgTypes[2] 146 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 147 | ms.StoreMessageInfo(mi) 148 | } 149 | } 150 | 151 | func (x *PossibleThree) String() string { 152 | return protoimpl.X.MessageStringOf(x) 153 | } 154 | 155 | func (*PossibleThree) ProtoMessage() {} 156 | 157 | func (x *PossibleThree) ProtoReflect() protoreflect.Message { 158 | mi := &file_testpb_test_proto_msgTypes[2] 159 | if protoimpl.UnsafeEnabled && x != nil { 160 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 161 | if ms.LoadMessageInfo() == nil { 162 | ms.StoreMessageInfo(mi) 163 | } 164 | return ms 165 | } 166 | return mi.MessageOf(x) 167 | } 168 | 169 | // Deprecated: Use PossibleThree.ProtoReflect.Descriptor instead. 170 | func (*PossibleThree) Descriptor() ([]byte, []int) { 171 | return file_testpb_test_proto_rawDescGZIP(), []int{2} 172 | } 173 | 174 | func (x *PossibleThree) GetProperty_1() float64 { 175 | if x != nil { 176 | return x.Property_1 177 | } 178 | return 0 179 | } 180 | 181 | func (x *PossibleThree) GetProperty_2() float64 { 182 | if x != nil { 183 | return x.Property_2 184 | } 185 | return 0 186 | } 187 | 188 | type TestMessage struct { 189 | state protoimpl.MessageState 190 | sizeCache protoimpl.SizeCache 191 | unknownFields protoimpl.UnknownFields 192 | 193 | Property_1 string `protobuf:"bytes,1,opt,name=property_1,json=property1,proto3" json:"property_1,omitempty"` 194 | Property_2 string `protobuf:"bytes,2,opt,name=property_2,json=property2,proto3" json:"property_2,omitempty"` 195 | // Types that are assignable to OneofField: 196 | // 197 | // *TestMessage_PossibleOne 198 | // *TestMessage_PossibleTwo 199 | // *TestMessage_PossibleThree 200 | OneofField isTestMessage_OneofField `protobuf_oneof:"oneof_field"` 201 | } 202 | 203 | func (x *TestMessage) Reset() { 204 | *x = TestMessage{} 205 | if protoimpl.UnsafeEnabled { 206 | mi := &file_testpb_test_proto_msgTypes[3] 207 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 208 | ms.StoreMessageInfo(mi) 209 | } 210 | } 211 | 212 | func (x *TestMessage) String() string { 213 | return protoimpl.X.MessageStringOf(x) 214 | } 215 | 216 | func (*TestMessage) ProtoMessage() {} 217 | 218 | func (x *TestMessage) ProtoReflect() protoreflect.Message { 219 | mi := &file_testpb_test_proto_msgTypes[3] 220 | if protoimpl.UnsafeEnabled && x != nil { 221 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 222 | if ms.LoadMessageInfo() == nil { 223 | ms.StoreMessageInfo(mi) 224 | } 225 | return ms 226 | } 227 | return mi.MessageOf(x) 228 | } 229 | 230 | // Deprecated: Use TestMessage.ProtoReflect.Descriptor instead. 231 | func (*TestMessage) Descriptor() ([]byte, []int) { 232 | return file_testpb_test_proto_rawDescGZIP(), []int{3} 233 | } 234 | 235 | func (x *TestMessage) GetProperty_1() string { 236 | if x != nil { 237 | return x.Property_1 238 | } 239 | return "" 240 | } 241 | 242 | func (x *TestMessage) GetProperty_2() string { 243 | if x != nil { 244 | return x.Property_2 245 | } 246 | return "" 247 | } 248 | 249 | func (m *TestMessage) GetOneofField() isTestMessage_OneofField { 250 | if m != nil { 251 | return m.OneofField 252 | } 253 | return nil 254 | } 255 | 256 | func (x *TestMessage) GetPossibleOne() *PossibleOne { 257 | if x, ok := x.GetOneofField().(*TestMessage_PossibleOne); ok { 258 | return x.PossibleOne 259 | } 260 | return nil 261 | } 262 | 263 | func (x *TestMessage) GetPossibleTwo() *PossibleTwo { 264 | if x, ok := x.GetOneofField().(*TestMessage_PossibleTwo); ok { 265 | return x.PossibleTwo 266 | } 267 | return nil 268 | } 269 | 270 | func (x *TestMessage) GetPossibleThree() *PossibleThree { 271 | if x, ok := x.GetOneofField().(*TestMessage_PossibleThree); ok { 272 | return x.PossibleThree 273 | } 274 | return nil 275 | } 276 | 277 | type isTestMessage_OneofField interface { 278 | isTestMessage_OneofField() 279 | } 280 | 281 | type TestMessage_PossibleOne struct { 282 | PossibleOne *PossibleOne `protobuf:"bytes,3,opt,name=possible_one,json=possibleOne,proto3,oneof"` 283 | } 284 | 285 | type TestMessage_PossibleTwo struct { 286 | PossibleTwo *PossibleTwo `protobuf:"bytes,4,opt,name=possible_two,json=possibleTwo,proto3,oneof"` 287 | } 288 | 289 | type TestMessage_PossibleThree struct { 290 | PossibleThree *PossibleThree `protobuf:"bytes,5,opt,name=possible_three,json=possibleThree,proto3,oneof"` 291 | } 292 | 293 | func (*TestMessage_PossibleOne) isTestMessage_OneofField() {} 294 | 295 | func (*TestMessage_PossibleTwo) isTestMessage_OneofField() {} 296 | 297 | func (*TestMessage_PossibleThree) isTestMessage_OneofField() {} 298 | 299 | var File_testpb_test_proto protoreflect.FileDescriptor 300 | 301 | var file_testpb_test_proto_rawDesc = []byte{ 302 | 0x0a, 0x11, 0x74, 0x65, 0x73, 0x74, 0x70, 0x62, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 303 | 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x74, 0x65, 0x73, 0x74, 0x70, 0x62, 0x22, 0x4b, 0x0a, 0x0b, 0x50, 304 | 0x6f, 0x73, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x4f, 0x6e, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 305 | 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x5f, 0x31, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 306 | 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x31, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 307 | 0x70, 0x65, 0x72, 0x74, 0x79, 0x5f, 0x32, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 308 | 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x32, 0x22, 0x4b, 0x0a, 0x0b, 0x50, 0x6f, 0x73, 0x73, 309 | 0x69, 0x62, 0x6c, 0x65, 0x54, 0x77, 0x6f, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 310 | 0x72, 0x74, 0x79, 0x5f, 0x31, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x70, 0x72, 0x6f, 311 | 0x70, 0x65, 0x72, 0x74, 0x79, 0x31, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 312 | 0x74, 0x79, 0x5f, 0x32, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x70, 313 | 0x65, 0x72, 0x74, 0x79, 0x32, 0x22, 0x4d, 0x0a, 0x0d, 0x50, 0x6f, 0x73, 0x73, 0x69, 0x62, 0x6c, 314 | 0x65, 0x54, 0x68, 0x72, 0x65, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 315 | 0x74, 0x79, 0x5f, 0x31, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x70, 316 | 0x65, 0x72, 0x74, 0x79, 0x31, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 317 | 0x79, 0x5f, 0x32, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x70, 0x65, 318 | 0x72, 0x74, 0x79, 0x32, 0x22, 0x8e, 0x02, 0x0a, 0x0b, 0x54, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x73, 319 | 0x73, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 320 | 0x5f, 0x31, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 321 | 0x74, 0x79, 0x31, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x5f, 322 | 0x32, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 323 | 0x79, 0x32, 0x12, 0x38, 0x0a, 0x0c, 0x70, 0x6f, 0x73, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x6f, 324 | 0x6e, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 325 | 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x4f, 0x6e, 0x65, 0x48, 0x00, 0x52, 326 | 0x0b, 0x70, 0x6f, 0x73, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x4f, 0x6e, 0x65, 0x12, 0x38, 0x0a, 0x0c, 327 | 0x70, 0x6f, 0x73, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x74, 0x77, 0x6f, 0x18, 0x04, 0x20, 0x01, 328 | 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x73, 329 | 0x69, 0x62, 0x6c, 0x65, 0x54, 0x77, 0x6f, 0x48, 0x00, 0x52, 0x0b, 0x70, 0x6f, 0x73, 0x73, 0x69, 330 | 0x62, 0x6c, 0x65, 0x54, 0x77, 0x6f, 0x12, 0x3e, 0x0a, 0x0e, 0x70, 0x6f, 0x73, 0x73, 0x69, 0x62, 331 | 0x6c, 0x65, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 332 | 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x73, 0x69, 0x62, 0x6c, 0x65, 333 | 0x54, 0x68, 0x72, 0x65, 0x65, 0x48, 0x00, 0x52, 0x0d, 0x70, 0x6f, 0x73, 0x73, 0x69, 0x62, 0x6c, 334 | 0x65, 0x54, 0x68, 0x72, 0x65, 0x65, 0x42, 0x0d, 0x0a, 0x0b, 0x6f, 0x6e, 0x65, 0x6f, 0x66, 0x5f, 335 | 0x66, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x28, 0x5a, 0x26, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 336 | 0x63, 0x6f, 0x6d, 0x2f, 0x6e, 0x65, 0x6b, 0x6f, 0x6d, 0x65, 0x6f, 0x77, 0x77, 0x77, 0x2f, 0x70, 337 | 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x73, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x70, 0x62, 0x62, 338 | 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 339 | } 340 | 341 | var ( 342 | file_testpb_test_proto_rawDescOnce sync.Once 343 | file_testpb_test_proto_rawDescData = file_testpb_test_proto_rawDesc 344 | ) 345 | 346 | func file_testpb_test_proto_rawDescGZIP() []byte { 347 | file_testpb_test_proto_rawDescOnce.Do(func() { 348 | file_testpb_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_testpb_test_proto_rawDescData) 349 | }) 350 | return file_testpb_test_proto_rawDescData 351 | } 352 | 353 | var file_testpb_test_proto_msgTypes = make([]protoimpl.MessageInfo, 4) 354 | var file_testpb_test_proto_goTypes = []interface{}{ 355 | (*PossibleOne)(nil), // 0: testpb.PossibleOne 356 | (*PossibleTwo)(nil), // 1: testpb.PossibleTwo 357 | (*PossibleThree)(nil), // 2: testpb.PossibleThree 358 | (*TestMessage)(nil), // 3: testpb.TestMessage 359 | } 360 | var file_testpb_test_proto_depIdxs = []int32{ 361 | 0, // 0: testpb.TestMessage.possible_one:type_name -> testpb.PossibleOne 362 | 1, // 1: testpb.TestMessage.possible_two:type_name -> testpb.PossibleTwo 363 | 2, // 2: testpb.TestMessage.possible_three:type_name -> testpb.PossibleThree 364 | 3, // [3:3] is the sub-list for method output_type 365 | 3, // [3:3] is the sub-list for method input_type 366 | 3, // [3:3] is the sub-list for extension type_name 367 | 3, // [3:3] is the sub-list for extension extendee 368 | 0, // [0:3] is the sub-list for field type_name 369 | } 370 | 371 | func init() { file_testpb_test_proto_init() } 372 | func file_testpb_test_proto_init() { 373 | if File_testpb_test_proto != nil { 374 | return 375 | } 376 | if !protoimpl.UnsafeEnabled { 377 | file_testpb_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 378 | switch v := v.(*PossibleOne); i { 379 | case 0: 380 | return &v.state 381 | case 1: 382 | return &v.sizeCache 383 | case 2: 384 | return &v.unknownFields 385 | default: 386 | return nil 387 | } 388 | } 389 | file_testpb_test_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 390 | switch v := v.(*PossibleTwo); i { 391 | case 0: 392 | return &v.state 393 | case 1: 394 | return &v.sizeCache 395 | case 2: 396 | return &v.unknownFields 397 | default: 398 | return nil 399 | } 400 | } 401 | file_testpb_test_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 402 | switch v := v.(*PossibleThree); i { 403 | case 0: 404 | return &v.state 405 | case 1: 406 | return &v.sizeCache 407 | case 2: 408 | return &v.unknownFields 409 | default: 410 | return nil 411 | } 412 | } 413 | file_testpb_test_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { 414 | switch v := v.(*TestMessage); i { 415 | case 0: 416 | return &v.state 417 | case 1: 418 | return &v.sizeCache 419 | case 2: 420 | return &v.unknownFields 421 | default: 422 | return nil 423 | } 424 | } 425 | } 426 | file_testpb_test_proto_msgTypes[3].OneofWrappers = []interface{}{ 427 | (*TestMessage_PossibleOne)(nil), 428 | (*TestMessage_PossibleTwo)(nil), 429 | (*TestMessage_PossibleThree)(nil), 430 | } 431 | type x struct{} 432 | out := protoimpl.TypeBuilder{ 433 | File: protoimpl.DescBuilder{ 434 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 435 | RawDescriptor: file_testpb_test_proto_rawDesc, 436 | NumEnums: 0, 437 | NumMessages: 4, 438 | NumExtensions: 0, 439 | NumServices: 0, 440 | }, 441 | GoTypes: file_testpb_test_proto_goTypes, 442 | DependencyIndexes: file_testpb_test_proto_depIdxs, 443 | MessageInfos: file_testpb_test_proto_msgTypes, 444 | }.Build() 445 | File_testpb_test_proto = out.File 446 | file_testpb_test_proto_rawDesc = nil 447 | file_testpb_test_proto_goTypes = nil 448 | file_testpb_test_proto_depIdxs = nil 449 | } 450 | -------------------------------------------------------------------------------- /protobufs/testpb/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package testpb; 3 | 4 | option go_package = "github.com/nekomeowww/protobufs/testpb"; // golang 5 | 6 | message PossibleOne { 7 | string property_1 = 1; 8 | string property_2 = 2; 9 | } 10 | 11 | message PossibleTwo { 12 | bool property_1 = 1; 13 | bool property_2 = 2; 14 | } 15 | 16 | message PossibleThree { 17 | double property_1 = 1; 18 | double property_2 = 2; 19 | } 20 | 21 | message TestMessage { 22 | string property_1 = 1; 23 | string property_2 = 2; 24 | oneof oneof_field { 25 | PossibleOne possible_one = 3; 26 | PossibleTwo possible_two = 4; 27 | PossibleThree possible_three = 5; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /random.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "fmt" 8 | "math/big" 9 | "strings" 10 | ) 11 | 12 | // RandBytes generates bytes according to the given length, defaults to 32. 13 | func RandBytes(length ...int) ([]byte, error) { 14 | b := make([]byte, 32) 15 | if len(length) != 0 { 16 | b = make([]byte, length[0]) 17 | } 18 | 19 | _, err := rand.Read(b) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return b, nil 25 | } 26 | 27 | // RandomBase64Token generates the URL-safe Base64 string based on the given byte length, 28 | // the length is 32 by default, the length is the length of the original byte data, not 29 | // the actual length of the Base64 string, the actual length is about 44 by default in 30 | // the case of 32. 31 | func RandomBase64Token(length ...int) (string, error) { 32 | b, err := RandBytes(length...) 33 | if err != nil { 34 | return "", err 35 | } 36 | 37 | return base64.URLEncoding.EncodeToString(b), nil 38 | } 39 | 40 | // RandomHashString generates a random SHA256 string with the maximum length of 64. 41 | func RandomHashString(length ...int) string { 42 | b, _ := RandBytes(1024) 43 | if len(length) != 0 { 44 | sliceLength := length[0] 45 | if length[0] > 64 { 46 | sliceLength = 64 47 | } 48 | if length[0] <= 0 { 49 | sliceLength = 64 50 | } 51 | 52 | return fmt.Sprintf("%x", sha256.Sum256(b))[:sliceLength] 53 | } 54 | 55 | return fmt.Sprintf("%x", sha256.Sum256(b)) 56 | } 57 | 58 | // RandomInt64 generates a random integer. 59 | func RandomInt64(maxVal ...int64) int64 { 60 | innerMax := int64(0) 61 | if len(maxVal) == 0 || (len(maxVal) > 0 && maxVal[0] <= 0) { 62 | innerMax = 9999999999 63 | } else { 64 | innerMax = maxVal[0] 65 | } 66 | 67 | nBig, _ := rand.Int(rand.Reader, big.NewInt(innerMax)) 68 | n := nBig.Int64() 69 | 70 | return n 71 | } 72 | 73 | // RandomInt64InRange generates a random integer in the range. 74 | func RandomInt64InRange(minVal, maxVal int64) int64 { 75 | if minVal >= maxVal { 76 | panic("min must be less than max") 77 | } 78 | if maxVal <= 0 { 79 | panic("max must be greater than 0") 80 | } 81 | 82 | nBig, _ := rand.Int(rand.Reader, big.NewInt(maxVal-minVal)) 83 | n := nBig.Int64() 84 | 85 | return n + minVal 86 | } 87 | 88 | // RandomInt64String generates a random integer string. 89 | func RandomInt64String(digits int64) string { 90 | maxBig := big.NewInt(0) 91 | minBig := big.NewInt(0) 92 | 93 | maxBig.SetString(strings.Repeat("9", int(digits)), 10) 94 | minBig.SetString(fmt.Sprintf("%s%s", "1", strings.Repeat("0", int(digits)-1)), 10) 95 | 96 | nBig, _ := rand.Int(rand.Reader, new(big.Int).Sub(maxBig, minBig)) 97 | 98 | return new(big.Int).Add(nBig, minBig).String() 99 | } 100 | -------------------------------------------------------------------------------- /random_test.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestRandomBytes(t *testing.T) { 12 | t.Run("no args", func(t *testing.T) { 13 | assert := assert.New(t) 14 | 15 | rand1, _ := RandBytes() 16 | rand2, _ := RandBytes() 17 | 18 | assert.NotEqual(rand1, rand2) 19 | assert.Equal(len(rand1), 32) 20 | assert.Equal(len(rand2), 32) 21 | }) 22 | t.Run("with args", func(t *testing.T) { 23 | assert := assert.New(t) 24 | 25 | arg := 123 26 | rand1, _ := RandBytes(arg) 27 | rand2, _ := RandBytes(arg) 28 | 29 | assert.NotEqual(rand1, rand2) 30 | assert.Equal(len(rand1), arg) 31 | assert.Equal(len(rand2), arg) 32 | }) 33 | } 34 | 35 | func TestRandomBase64Token(t *testing.T) { 36 | t.Run("no args", func(t *testing.T) { 37 | assert := assert.New(t) 38 | 39 | rand1, _ := RandomBase64Token() 40 | rand2, _ := RandomBase64Token() 41 | 42 | assert.NotEqual(rand1, rand2) 43 | assert.Equal(len(rand1), 44) 44 | assert.Equal(len(rand2), 44) 45 | }) 46 | t.Run("with args", func(t *testing.T) { 47 | assert := assert.New(t) 48 | 49 | arg := 123 50 | rand1, _ := RandomBase64Token(arg) 51 | rand2, _ := RandomBase64Token(arg) 52 | 53 | assert.NotEqual(rand1, rand2) 54 | }) 55 | } 56 | 57 | func TestRandomHashString(t *testing.T) { 58 | assert := assert.New(t) 59 | 60 | hashString := RandomHashString() 61 | assert.NotEmpty(hashString) 62 | assert.Len(hashString, 64) 63 | 64 | hashString2 := RandomHashString(32) 65 | assert.NotEmpty(hashString2) 66 | assert.Len(hashString2, 32) 67 | } 68 | 69 | func TestRandomString(t *testing.T) { 70 | data, err := RandBytes(32) 71 | assert.NoError(t, err) 72 | assert.Len(t, data, 32) 73 | 74 | assert.Len(t, fmt.Sprintf("%x", data), 64) 75 | } 76 | 77 | func TestRandomInt64(t *testing.T) { 78 | assert := assert.New(t) 79 | 80 | assert.NotZero(RandomInt64()) 81 | } 82 | 83 | func TestRandomInt64InRange(t *testing.T) { 84 | assert := assert.New(t) 85 | 86 | randomNumber := RandomInt64InRange(100000, 999999) 87 | assert.NotZero(randomNumber) 88 | assert.Len(strconv.FormatInt(randomNumber, 10), 6) 89 | } 90 | 91 | func TestRandomInt64String(t *testing.T) { 92 | assert := assert.New(t) 93 | 94 | for i := 0; i < 100; i++ { 95 | t.Run(fmt.Sprintf("Try#%d", i+1), func(t *testing.T) { 96 | numberStr := RandomInt64String(6) 97 | assert.NotEmpty(numberStr) 98 | assert.Len(numberStr, 6) 99 | 100 | numberStr = RandomInt64String(32) 101 | assert.NotEmpty(numberStr) 102 | assert.Len(numberStr, 32) 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /slice.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/samber/lo" 7 | ) 8 | 9 | // ToMap converts a slice to a map with key from key getter func and pairs with value. 10 | func ToMap[T any, K comparable](t []T, keyGetter func(T) K) map[K]T { 11 | grouped := lo.GroupBy(t, keyGetter) 12 | 13 | return lo.MapValues(grouped, func(values []T, key K) T { 14 | return values[0] 15 | }) 16 | } 17 | 18 | // Join returns a string contains items joined with sep by using fmt.Sprintf. 19 | func Join[T any](from []T, sep string) string { 20 | slice := make([]string, len(from)) 21 | 22 | for i, v := range from { 23 | slice[i] = Stringify(v) 24 | } 25 | 26 | return strings.Join(slice, sep) 27 | } 28 | 29 | // Clone returns a new slice contains items cloned from collection. 30 | func Clone[T any, Slice ~[]T](collection Slice) Slice { 31 | return lo.Map(collection, func(item T, _ int) T { 32 | return item 33 | }) 34 | } 35 | 36 | // JoinWithConverter returns a string contains converted items joined with sep. 37 | // 38 | // Deprecated: Use JoinBy instead. 39 | func JoinWithConverter[T any](from []T, sep string, convertFunc func(item T) string) string { 40 | slice := make([]string, len(from)) 41 | for i, v := range from { 42 | slice[i] = convertFunc(v) 43 | } 44 | 45 | return strings.Join(slice, sep) 46 | } 47 | 48 | // JoinBy returns a string contains converted items joined with sep. 49 | func JoinBy[T any](from []T, sep string, convertFunc func(item T) string) string { 50 | slice := make([]string, len(from)) 51 | for i, v := range from { 52 | slice[i] = convertFunc(v) 53 | } 54 | 55 | return strings.Join(slice, sep) 56 | } 57 | 58 | // MapString returns a new slice contains stringified items. 59 | func MapString[T any](from []T, sep string) []string { 60 | slice := make([]string, len(from)) 61 | 62 | for i, v := range from { 63 | slice[i] = Stringify(v) 64 | } 65 | 66 | return slice 67 | } 68 | 69 | // MapStringBy returns a new slice contains converted items. 70 | func MapStringBy[T any](from []T, sep string, convertFunc func(item T) string) []string { 71 | slice := make([]string, len(from)) 72 | 73 | for i, v := range from { 74 | slice[i] = convertFunc(v) 75 | } 76 | 77 | return slice 78 | } 79 | 80 | // SliceSlices returns a new slice contains slices with maximum length each. 81 | func SliceSlices[T any](from []T, each int) [][]T { 82 | result := make([][]T, 0, len(from)/each+1) 83 | 84 | for n := 0; ; n += each { 85 | if n+each >= len(from) { 86 | result = append(result, from[n:]) 87 | break 88 | } 89 | result = append(result, from[n:n+each]) 90 | } 91 | 92 | return result 93 | } 94 | 95 | // Intersection returns a new slice contains items that are in both a and b. 96 | func Intersection[T comparable](a, b []T) []T { 97 | pendingChecks := make(map[T]int) 98 | 99 | for _, v := range a { 100 | pendingChecks[v] = 1 101 | } 102 | 103 | for _, v := range b { 104 | pendingChecks[v] |= 2 105 | } 106 | 107 | intersectionResult := make([]T, 0, len(pendingChecks)) 108 | 109 | for k, v := range pendingChecks { 110 | if v == 3 { 111 | intersectionResult = append(intersectionResult, k) 112 | } 113 | } 114 | 115 | return intersectionResult 116 | } 117 | 118 | // FindDuplicates returns a new slice contains items that are duplicated in a. 119 | func FindDuplicates[T comparable](a []T) []T { 120 | pendingChecks := make(map[T]int) 121 | 122 | for _, v := range a { 123 | pendingChecks[v]++ 124 | } 125 | 126 | duplicates := make([]T, 0, len(pendingChecks)) 127 | 128 | for k, v := range pendingChecks { 129 | if v > 1 { 130 | duplicates = append(duplicates, k) 131 | } 132 | } 133 | 134 | return duplicates 135 | } 136 | -------------------------------------------------------------------------------- /slice_test.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestToMap(t *testing.T) { 14 | t.Parallel() 15 | 16 | assert := assert.New(t) 17 | 18 | type foo struct { 19 | ID int 20 | Name string 21 | } 22 | 23 | inputs := make([]*foo, 2) 24 | for i := 0; i < 2; i++ { 25 | inputs[i] = &foo{ 26 | ID: i, 27 | Name: "A", 28 | } 29 | } 30 | 31 | result := ToMap(inputs, func(item *foo) int { return item.ID }) 32 | for _, elem := range inputs { 33 | v, ok := result[elem.ID] 34 | assert.True(ok) 35 | assert.Equal(elem, v) 36 | } 37 | } 38 | 39 | func TestSliceSlices(t *testing.T) { 40 | t.Parallel() 41 | 42 | t.Run("Int64", func(t *testing.T) { 43 | t.Parallel() 44 | 45 | assert := assert.New(t) 46 | require := require.New(t) 47 | 48 | var s1 []int64 49 | length := 2 50 | result := SliceSlices(s1, length) 51 | require.Len(result, 1) 52 | assert.Empty(result[0]) 53 | 54 | s1 = make([]int64, 0) 55 | result = SliceSlices(s1, length) 56 | require.Len(result, 1) 57 | assert.Empty(result[0]) 58 | 59 | s1 = []int64{1, 2, 3} 60 | result = SliceSlices(s1, length) 61 | require.Len(result, 2) 62 | assert.Equal(result, [][]int64{{1, 2}, {3}}) 63 | 64 | s1 = []int64{1, 2, 3, 4} 65 | result = SliceSlices(s1, length) 66 | require.Len(result, 2) 67 | assert.Equal(result, [][]int64{{1, 2}, {3, 4}}) 68 | 69 | s1 = make([]int64, 0) 70 | for i := 0; i < 10000; i++ { 71 | s1 = append(s1, int64(i+1)) 72 | } 73 | 74 | length = 10 75 | result = SliceSlices(s1, length) 76 | require.Len(result, 10000/length) 77 | 78 | for _, v := range result { 79 | require.Len(v, length) 80 | } 81 | 82 | s1 = make([]int64, 0) 83 | for i := 0; i < 10000; i++ { 84 | s1 = append(s1, int64(i+1)) 85 | } 86 | 87 | length = 100000 88 | result = SliceSlices(s1, length) 89 | 90 | actualLength := 10000 / length 91 | if actualLength <= 0 { 92 | actualLength = 1 93 | } 94 | 95 | require.Len(result, actualLength) 96 | 97 | for _, v := range result { 98 | require.Len(v, len(s1)) 99 | } 100 | }) 101 | 102 | t.Run("Struct", func(t *testing.T) { 103 | t.Parallel() 104 | 105 | assert := assert.New(t) 106 | require := require.New(t) 107 | 108 | type testType struct { 109 | id int64 110 | } 111 | 112 | var s1 []testType 113 | length := 2 114 | result := SliceSlices(s1, length) 115 | require.Len(result, 1) 116 | assert.Empty(result[0]) 117 | 118 | s1 = make([]testType, 0) 119 | result = SliceSlices(s1, length) 120 | require.Len(result, 1) 121 | assert.Empty(result[0]) 122 | 123 | s1 = []testType{ 124 | {id: 1}, 125 | {id: 2}, 126 | {id: 3}, 127 | } 128 | result = SliceSlices(s1, length) 129 | require.Len(result, 2) 130 | assert.Equal(result, [][]testType{ 131 | {{id: 1}, {id: 2}}, 132 | {{id: 3}}, 133 | }) 134 | 135 | s1 = []testType{ 136 | {id: 1}, 137 | {id: 2}, 138 | {id: 3}, 139 | {id: 4}, 140 | } 141 | result = SliceSlices(s1, length) 142 | require.Len(result, 2) 143 | assert.Equal(result, [][]testType{ 144 | {{id: 1}, {id: 2}}, 145 | {{id: 3}, {id: 4}}, 146 | }) 147 | 148 | s1 = make([]testType, 0) 149 | for i := 0; i < 10000; i++ { 150 | s1 = append(s1, testType{id: int64(i + 1)}) 151 | } 152 | 153 | length = 10 154 | result = SliceSlices(s1, length) 155 | require.Len(result, 10000/length) 156 | 157 | for _, v := range result { 158 | require.Len(v, length) 159 | } 160 | }) 161 | } 162 | 163 | func TestJoin(t *testing.T) { 164 | t.Parallel() 165 | 166 | t.Run("Int64", func(t *testing.T) { 167 | t.Parallel() 168 | 169 | assert := assert.New(t) 170 | 171 | a := []int64{1, 2, 3} 172 | assert.Equal("1,2,3", Join(a, ",")) 173 | }) 174 | 175 | t.Run("Float64", func(t *testing.T) { 176 | t.Parallel() 177 | 178 | assert := assert.New(t) 179 | 180 | a := []float64{1.2, 1.0, 1} 181 | assert.Equal("1.2,1,1", Join(a, ",")) 182 | }) 183 | 184 | t.Run("Error", func(t *testing.T) { 185 | t.Parallel() 186 | 187 | assert := assert.New(t) 188 | 189 | a := []error{errors.New("1"), errors.New("2"), errors.New("3")} 190 | assert.Equal("1,2,3", Join(a, ",")) 191 | }) 192 | } 193 | 194 | func TestJoinWithConverter(t *testing.T) { 195 | t.Parallel() 196 | 197 | t.Run("Int64", func(t *testing.T) { 198 | t.Parallel() 199 | 200 | t.Run("fmt", func(t *testing.T) { 201 | t.Parallel() 202 | 203 | assert := assert.New(t) 204 | 205 | a := []int64{1, 2, 3} 206 | assert.Equal("1,2,3", JoinWithConverter(a, ",", func(v int64) string { 207 | return fmt.Sprintf("%d", v) 208 | })) 209 | }) 210 | 211 | t.Run("strconv", func(t *testing.T) { 212 | t.Parallel() 213 | 214 | assert := assert.New(t) 215 | 216 | a := []int64{1, 2, 3} 217 | assert.Equal("1,2,3", JoinWithConverter(a, ",", func(v int64) string { 218 | return strconv.FormatInt(v, 10) 219 | })) 220 | }) 221 | }) 222 | 223 | t.Run("Float64", func(t *testing.T) { 224 | t.Parallel() 225 | 226 | t.Run("fmt", func(t *testing.T) { 227 | t.Parallel() 228 | 229 | assert := assert.New(t) 230 | 231 | a := []float64{1.2, 1.0, 1} 232 | assert.Equal("1.20,1.00,1.00", JoinWithConverter(a, ",", func(v float64) string { 233 | return fmt.Sprintf("%.2f", v) 234 | })) 235 | }) 236 | 237 | t.Run("strconv", func(t *testing.T) { 238 | t.Parallel() 239 | 240 | assert := assert.New(t) 241 | 242 | a := []float64{1.2, 1.0, 1} 243 | assert.Equal("1.2,1,1", JoinWithConverter(a, ",", func(v float64) string { 244 | return strconv.FormatFloat(v, 'f', -1, 64) 245 | })) 246 | }) 247 | }) 248 | } 249 | 250 | func TestIntersection(t *testing.T) { 251 | t.Parallel() 252 | 253 | assert := assert.New(t) 254 | require := require.New(t) 255 | 256 | t.Run("Number", func(t *testing.T) { 257 | t.Parallel() 258 | 259 | a1 := []int64{1, 1} 260 | a2 := []int64{2, 2} 261 | intersection := Intersection(a1, a2) 262 | require.Empty(intersection) 263 | 264 | a1 = []int64{1, 2} 265 | a2 = []int64{2, 3} 266 | intersection = Intersection(a1, a2) 267 | require.NotEmpty(intersection) 268 | assert.ElementsMatch([]int64{2}, Intersection(a1, a2)) 269 | 270 | a1 = []int64{1, 2} 271 | a2 = []int64{3, 4} 272 | intersection = Intersection(a1, a2) 273 | require.Empty(intersection) 274 | }) 275 | 276 | t.Run("String", func(t *testing.T) { 277 | t.Parallel() 278 | 279 | a1 := []string{"1", "1"} 280 | a2 := []string{"2", "2"} 281 | intersection := Intersection(a1, a2) 282 | require.Empty(intersection) 283 | 284 | a1 = []string{"1", "2"} 285 | a2 = []string{"2", "3"} 286 | intersection = Intersection(a1, a2) 287 | require.NotEmpty(intersection) 288 | assert.ElementsMatch([]string{"2"}, Intersection(a1, a2)) 289 | 290 | a1 = []string{"1", "2"} 291 | a2 = []string{"3", "4"} 292 | intersection = Intersection(a1, a2) 293 | require.Empty(intersection) 294 | }) 295 | 296 | t.Run("Struct", func(t *testing.T) { 297 | t.Parallel() 298 | 299 | type testStruct struct { 300 | id int64 301 | } 302 | 303 | a1 := []testStruct{{id: 1}, {id: 1}} 304 | a2 := []testStruct{{id: 2}, {id: 2}} 305 | intersection := Intersection(a1, a2) 306 | require.Empty(intersection) 307 | 308 | a1 = []testStruct{{id: 1}, {id: 2}} 309 | a2 = []testStruct{{id: 2}, {id: 3}} 310 | intersection = Intersection(a1, a2) 311 | require.NotEmpty(intersection) 312 | assert.ElementsMatch([]testStruct{{id: 2}}, Intersection(a1, a2)) 313 | 314 | a1 = []testStruct{{id: 1}, {id: 2}} 315 | a2 = []testStruct{{id: 3}, {id: 4}} 316 | intersection = Intersection(a1, a2) 317 | require.Empty(intersection) 318 | }) 319 | } 320 | 321 | func TestFindDuplicates(t *testing.T) { 322 | t.Parallel() 323 | 324 | t.Run("Number", func(t *testing.T) { 325 | t.Parallel() 326 | 327 | assert := assert.New(t) 328 | 329 | list := []int64{1, 2, 3, 4, 5} 330 | repeatList := FindDuplicates(list) 331 | assert.Len(repeatList, 0) 332 | 333 | list = []int64{1, 1, 2} 334 | repeatList = FindDuplicates(list) 335 | assert.ElementsMatch([]int64{1}, repeatList) 336 | 337 | list = []int64{1, 1, 2, 2} 338 | repeatList = FindDuplicates(list) 339 | assert.ElementsMatch([]int64{1, 2}, repeatList) 340 | }) 341 | 342 | t.Run("String", func(t *testing.T) { 343 | t.Parallel() 344 | 345 | assert := assert.New(t) 346 | 347 | list := []string{"1", "2", "3", "4", "5"} 348 | repeatList := FindDuplicates(list) 349 | assert.Len(repeatList, 0) 350 | 351 | list = []string{"1", "1", "2"} 352 | repeatList = FindDuplicates(list) 353 | assert.ElementsMatch([]string{"1"}, repeatList) 354 | 355 | list = []string{"1", "1", "2", "2"} 356 | repeatList = FindDuplicates(list) 357 | assert.ElementsMatch([]string{"1", "2"}, repeatList) 358 | }) 359 | } 360 | -------------------------------------------------------------------------------- /string.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "unicode" 8 | 9 | "github.com/google/uuid" 10 | "github.com/samber/lo" 11 | ) 12 | 13 | // IsStringPrintable determines whether a string is printable. 14 | func IsStringPrintable(str string) bool { 15 | for _, v := range str { 16 | if v == '\n' || v == '\r' || v == '\t' { 17 | continue 18 | } 19 | if !unicode.IsGraphic(v) { 20 | return false 21 | } 22 | } 23 | 24 | return true 25 | } 26 | 27 | // IsASCIIPrintable determines whether a string is printable ASCII. 28 | func IsASCIIPrintable(s string) bool { 29 | for _, r := range s { 30 | if r > unicode.MaxASCII || !unicode.IsPrint(r) { 31 | return false 32 | } 33 | } 34 | 35 | return true 36 | } 37 | 38 | // IsValidUUID determines whether a string is a valid UUID. 39 | func IsValidUUID(uuidStr string) bool { 40 | if _, err := uuid.Parse(uuidStr); err != nil { 41 | return false 42 | } 43 | 44 | return true 45 | } 46 | 47 | // ContainsCJKChar determines whether a string contains CJK characters. 48 | func ContainsCJKChar(s string) bool { 49 | for _, r := range s { 50 | if unicode.Is(unicode.Han, r) { 51 | return true 52 | } 53 | if unicode.Is(unicode.Hangul, r) { 54 | return true 55 | } 56 | if unicode.Is(unicode.Hiragana, r) { 57 | return true 58 | } 59 | if unicode.Is(unicode.Katakana, r) { 60 | return true 61 | } 62 | 63 | /* 64 | U+3001 、 65 | U+3002 。 66 | U+3003 〃 67 | U+3008 〈 68 | U+3009 〉 69 | U+300A 《 70 | U+300B 》 71 | U+300C 「 72 | U+300D 」 73 | U+300E 『 74 | U+300F 』 75 | U+3010 【 76 | U+3011 】 77 | U+3014 〔 78 | U+3015 〕 79 | U+3016 〖 80 | U+3017 〗 81 | U+3018 〘 82 | U+3019 〙 83 | U+301A 〚 84 | U+301B 〛 85 | U+301C 〜 86 | U+301D 〝 87 | U+301E 〞 88 | U+301F 〟 89 | U+3030 〰 90 | U+303D 〽 91 | */ 92 | if r >= 0x3001 && r <= 0x303D { 93 | return true 94 | } 95 | } 96 | 97 | return false 98 | } 99 | 100 | func Stringify(v any) string { 101 | if v == nil { 102 | return "" 103 | } 104 | if lo.IsNil(v) { 105 | return "" 106 | } 107 | 108 | switch val := v.(type) { 109 | case string: 110 | return val 111 | case fmt.Stringer: 112 | if lo.IsNil(val) { 113 | return "" 114 | } else if val == nil { 115 | return "" 116 | } else { 117 | return val.String() 118 | } 119 | case int: 120 | return strconv.FormatInt(int64(val), 10) 121 | case int8: 122 | return strconv.FormatInt(int64(val), 10) 123 | case int16: 124 | return strconv.FormatInt(int64(val), 10) 125 | case int32: 126 | return strconv.FormatInt(int64(val), 10) 127 | case int64: 128 | return strconv.FormatInt(val, 10) 129 | case uint: 130 | return strconv.FormatUint(uint64(val), 10) 131 | case uint8: 132 | return strconv.FormatUint(uint64(val), 10) 133 | case uint16: 134 | return strconv.FormatUint(uint64(val), 10) 135 | case uint32: 136 | return strconv.FormatUint(uint64(val), 10) 137 | case uint64: 138 | return strconv.FormatUint(val, 10) 139 | case float32: 140 | return strconv.FormatFloat(float64(val), 'f', -1, 32) 141 | case float64: 142 | return strconv.FormatFloat(val, 'f', -1, 64) 143 | case complex64: 144 | return strconv.FormatComplex(complex128(val), 'f', -1, 64) 145 | case complex128: 146 | return strconv.FormatComplex(val, 'f', -1, 128) 147 | case bool: 148 | return strconv.FormatBool(val) 149 | case []byte: 150 | return string(val) 151 | case []rune: 152 | return string(val) 153 | case strings.Builder: 154 | return val.String() 155 | default: 156 | return fmt.Sprintf("%v", val) 157 | } 158 | } 159 | 160 | var ( 161 | errFailedToConvertStringToType = func(t any, err error) error { return fmt.Errorf("failed to convert string to type %T: %w", t, err) } 162 | ) 163 | 164 | func FromString[T any](str string) (T, error) { 165 | var empty T 166 | if str == "" { 167 | switch any(empty).(type) { 168 | case []byte: 169 | val, _ := any(make([]byte, 0)).(T) 170 | return val, nil 171 | case []rune: 172 | val, _ := any(make([]rune, 0)).(T) 173 | return val, nil 174 | case *strings.Builder: 175 | val, _ := any(&strings.Builder{}).(T) 176 | return val, nil 177 | } 178 | 179 | return empty, nil 180 | } 181 | 182 | switch any(empty).(type) { 183 | case string: 184 | val, _ := any(str).(T) 185 | return val, nil 186 | case int: 187 | val, err := strconv.ParseInt(str, 10, 0) 188 | if err != nil { 189 | return empty, errFailedToConvertStringToType(empty, err) 190 | } 191 | 192 | typeVal, _ := any(int(val)).(T) 193 | 194 | return typeVal, nil 195 | case int8: 196 | val, err := strconv.ParseInt(str, 10, 8) 197 | if err != nil { 198 | return empty, errFailedToConvertStringToType(empty, err) 199 | } 200 | 201 | typeVal, _ := any(int8(val)).(T) 202 | 203 | return typeVal, nil 204 | case int16: 205 | val, err := strconv.ParseInt(str, 10, 16) 206 | if err != nil { 207 | return empty, errFailedToConvertStringToType(empty, err) 208 | } 209 | 210 | typeVal, _ := any(int16(val)).(T) 211 | 212 | return typeVal, nil 213 | case int32: 214 | val, err := strconv.ParseInt(str, 10, 32) 215 | if err != nil { 216 | return empty, errFailedToConvertStringToType(empty, err) 217 | } 218 | 219 | typeVal, _ := any(int32(val)).(T) 220 | 221 | return typeVal, nil 222 | case int64: 223 | val, err := strconv.ParseInt(str, 10, 64) 224 | if err != nil { 225 | return empty, errFailedToConvertStringToType(empty, err) 226 | } 227 | 228 | typeVal, _ := any(val).(T) 229 | 230 | return typeVal, nil 231 | case uint: 232 | val, err := strconv.ParseUint(str, 10, 0) 233 | if err != nil { 234 | return empty, errFailedToConvertStringToType(empty, err) 235 | } 236 | 237 | typeVal, _ := any(uint(val)).(T) 238 | 239 | return typeVal, nil 240 | case uint8: 241 | val, err := strconv.ParseUint(str, 10, 8) 242 | if err != nil { 243 | return empty, errFailedToConvertStringToType(empty, err) 244 | } 245 | 246 | typeVal, _ := any(uint8(val)).(T) 247 | 248 | return typeVal, nil 249 | case uint16: 250 | val, err := strconv.ParseUint(str, 10, 16) 251 | if err != nil { 252 | return empty, errFailedToConvertStringToType(empty, err) 253 | } 254 | 255 | typeVal, _ := any(uint16(val)).(T) 256 | 257 | return typeVal, nil 258 | case uint32: 259 | val, err := strconv.ParseUint(str, 10, 32) 260 | if err != nil { 261 | return empty, errFailedToConvertStringToType(empty, err) 262 | } 263 | 264 | typeVal, _ := any(uint32(val)).(T) 265 | 266 | return typeVal, nil 267 | case uint64: 268 | val, err := strconv.ParseUint(str, 10, 64) 269 | if err != nil { 270 | return empty, errFailedToConvertStringToType(empty, err) 271 | } 272 | 273 | typeVal, _ := any(val).(T) 274 | 275 | return typeVal, nil 276 | case float32: 277 | val, err := strconv.ParseFloat(str, 32) 278 | if err != nil { 279 | return empty, errFailedToConvertStringToType(empty, err) 280 | } 281 | 282 | typeVal, _ := any(float32(val)).(T) 283 | 284 | return typeVal, nil 285 | case float64: 286 | val, err := strconv.ParseFloat(str, 64) 287 | if err != nil { 288 | return empty, errFailedToConvertStringToType(empty, err) 289 | } 290 | 291 | typeVal, _ := any(val).(T) 292 | 293 | return typeVal, nil 294 | case complex64: 295 | val, err := strconv.ParseComplex(str, 64) 296 | if err != nil { 297 | return empty, errFailedToConvertStringToType(empty, err) 298 | } 299 | 300 | typeVal, _ := any(complex64(val)).(T) 301 | 302 | return typeVal, nil 303 | case complex128: 304 | val, err := strconv.ParseComplex(str, 128) 305 | if err != nil { 306 | return empty, errFailedToConvertStringToType(empty, err) 307 | } 308 | 309 | typeVal, _ := any(val).(T) 310 | 311 | return typeVal, nil 312 | case bool: 313 | val, err := strconv.ParseBool(str) 314 | if err != nil { 315 | return empty, errFailedToConvertStringToType(empty, err) 316 | } 317 | 318 | typeVal, _ := any(val).(T) 319 | 320 | return typeVal, nil 321 | case []byte: 322 | val, _ := any([]byte(str)).(T) 323 | return val, nil 324 | case []rune: 325 | val, _ := any([]rune(str)).(T) 326 | return val, nil 327 | case *strings.Builder: 328 | var sb strings.Builder 329 | 330 | sb.WriteString(str) 331 | val, _ := any(&sb).(T) 332 | 333 | return val, nil 334 | default: 335 | return empty, fmt.Errorf("unsupported type %T", empty) 336 | } 337 | } 338 | 339 | func FromStringOrEmpty[T any](str string) T { 340 | var empty T 341 | 342 | val, err := FromString[T](str) 343 | if err != nil { 344 | return empty 345 | } 346 | 347 | return val 348 | } 349 | 350 | func Substring(str string, start, end int) string { 351 | if start < 0 { 352 | start = 0 353 | } 354 | if end < 0 { 355 | end = len(str) 356 | } 357 | if start > len(str) { 358 | start = len(str) 359 | } 360 | if end > len(str) { 361 | end = len(str) 362 | } 363 | if end < start { 364 | start, end = end, start 365 | } 366 | 367 | return str[start:end] 368 | } 369 | -------------------------------------------------------------------------------- /string_test.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestIsASCIIPrintable(t *testing.T) { 12 | t.Run("default", func(t *testing.T) { 13 | assert := assert.New(t) 14 | 15 | assert.True(IsASCIIPrintable("abcd1234!?@#$%^&*()[]{}<>|\\/\"'`~,.")) 16 | assert.False(IsASCIIPrintable("abc😊")) 17 | assert.False(IsASCIIPrintable("😊abc")) 18 | assert.False(IsASCIIPrintable("abc中文")) 19 | assert.False(IsASCIIPrintable("abc\n")) 20 | }) 21 | t.Run("Empty still returns True", func(t *testing.T) { 22 | assert := assert.New(t) 23 | 24 | assert.True(IsASCIIPrintable("")) 25 | assert.True(IsASCIIPrintable(" ")) 26 | assert.True(IsASCIIPrintable("abc f k")) 27 | }) 28 | } 29 | 30 | func TestIsValidUUID(t *testing.T) { 31 | assert := assert.New(t) 32 | 33 | strOk := "93d3ea4c-c66b-47ac-8472-747a24ecc86b" 34 | strErr := "93d3ea4c-c66b-47ac-8472-747a24ecc86" 35 | strErr2 := "93d3ea4c-" 36 | 37 | assert.True(IsValidUUID(strOk)) 38 | assert.False(IsValidUUID(strErr)) 39 | assert.False(IsValidUUID(strErr2)) 40 | } 41 | 42 | func TestSubstring(t *testing.T) { 43 | abc := Substring("abc", 0, 0) 44 | assert.Equal(t, "", abc) 45 | 46 | abc = Substring("abc", 0, 1) 47 | assert.Equal(t, "a", abc) 48 | 49 | abc = Substring("abc", 0, 2) 50 | assert.Equal(t, "ab", abc) 51 | 52 | abc = Substring("abc", 0, 3) 53 | assert.Equal(t, "abc", abc) 54 | 55 | abc = Substring("abc", 0, 4) 56 | assert.Equal(t, "abc", abc) 57 | } 58 | 59 | func TestFromString(t *testing.T) { 60 | t.Run("Unsupported", func(t *testing.T) { 61 | funcVal, err := FromString[func()]("") 62 | require.NoError(t, err) 63 | assert.Nil(t, funcVal) 64 | 65 | mapVal, err := FromString[map[string]any]("") 66 | require.NoError(t, err) 67 | assert.Nil(t, mapVal) 68 | 69 | mapVal, err = FromString[map[string]any]("") 70 | require.NoError(t, err) 71 | assert.Zero(t, len(mapVal)) 72 | 73 | sliceVal, err := FromString[[]string]("") 74 | require.NoError(t, err) 75 | assert.Nil(t, sliceVal) 76 | 77 | sliceVal, err = FromString[[]string]("") 78 | require.NoError(t, err) 79 | assert.Len(t, sliceVal, 0) 80 | 81 | structVal, err := FromString[struct{}]("") 82 | require.NoError(t, err) 83 | assert.Empty(t, structVal) 84 | 85 | funcVal, err = FromString[func()]("abcd") 86 | require.Error(t, err) 87 | assert.EqualError(t, err, "unsupported type func()") 88 | assert.Nil(t, funcVal) 89 | 90 | mapVal, err = FromString[map[string]any]("abcd") 91 | require.Error(t, err) 92 | assert.EqualError(t, err, "unsupported type map[string]interface {}") 93 | assert.Nil(t, mapVal) 94 | 95 | mapVal, err = FromString[map[string]any]("abcd") 96 | require.Error(t, err) 97 | assert.EqualError(t, err, "unsupported type map[string]interface {}") 98 | assert.Zero(t, len(mapVal)) 99 | 100 | sliceVal, err = FromString[[]string]("abcd") 101 | require.Error(t, err) 102 | assert.EqualError(t, err, "unsupported type []string") 103 | assert.Nil(t, sliceVal) 104 | 105 | sliceVal, err = FromString[[]string]("abcd") 106 | require.Error(t, err) 107 | assert.EqualError(t, err, "unsupported type []string") 108 | assert.Len(t, sliceVal, 0) 109 | 110 | structVal, err = FromString[struct{}]("abcd") 111 | require.Error(t, err) 112 | assert.EqualError(t, err, "unsupported type struct {}") 113 | assert.Empty(t, structVal) 114 | }) 115 | 116 | t.Run("Empty", func(t *testing.T) { 117 | stringVal, err := FromString[string]("") 118 | require.NoError(t, err) 119 | assert.Equal(t, "", stringVal) 120 | 121 | intVal, err := FromString[int]("") 122 | require.NoError(t, err) 123 | assert.Zero(t, intVal) 124 | 125 | int8Val, err := FromString[int8]("") 126 | require.NoError(t, err) 127 | assert.Zero(t, int8Val) 128 | 129 | int16Val, err := FromString[int16]("") 130 | require.NoError(t, err) 131 | assert.Zero(t, int16Val) 132 | 133 | int32Val, err := FromString[int32]("") 134 | require.NoError(t, err) 135 | assert.Zero(t, int32Val) 136 | 137 | int64Val, err := FromString[int64]("") 138 | require.NoError(t, err) 139 | assert.Zero(t, int64Val) 140 | 141 | uintVal, err := FromString[uint]("") 142 | require.NoError(t, err) 143 | assert.Zero(t, uintVal) 144 | 145 | uint8Val, err := FromString[uint8]("") 146 | require.NoError(t, err) 147 | assert.Zero(t, uint8Val) 148 | 149 | uint16Val, err := FromString[uint16]("") 150 | require.NoError(t, err) 151 | assert.Zero(t, uint16Val) 152 | 153 | uint32Val, err := FromString[uint32]("") 154 | require.NoError(t, err) 155 | assert.Zero(t, uint32Val) 156 | 157 | uint64Val, err := FromString[uint64]("") 158 | require.NoError(t, err) 159 | assert.Zero(t, uint64Val) 160 | 161 | float32Val, err := FromString[float32]("") 162 | require.NoError(t, err) 163 | assert.Zero(t, float32Val) 164 | 165 | float64Val, err := FromString[float64]("") 166 | require.NoError(t, err) 167 | assert.Zero(t, float64Val) 168 | 169 | complex64Val, err := FromString[complex64]("") 170 | require.NoError(t, err) 171 | assert.Zero(t, complex64Val) 172 | 173 | complex128Val, err := FromString[complex128]("") 174 | require.NoError(t, err) 175 | assert.Zero(t, complex128Val) 176 | 177 | boolVal, err := FromString[bool]("") 178 | require.NoError(t, err) 179 | assert.False(t, boolVal) 180 | 181 | bytesVal, err := FromString[[]byte]("") 182 | require.NoError(t, err) 183 | assert.Empty(t, bytesVal) 184 | 185 | runesVal, err := FromString[[]rune]("") 186 | require.NoError(t, err) 187 | assert.Empty(t, runesVal) 188 | 189 | builderVal, err := FromString[*strings.Builder]("") 190 | require.NoError(t, err) 191 | assert.NotNil(t, builderVal) 192 | }) 193 | 194 | t.Run("Invalid", func(t *testing.T) { 195 | intVal, err := FromString[int]("invalid") 196 | require.Error(t, err) 197 | assert.EqualError(t, err, "failed to convert string to type int: strconv.ParseInt: parsing \"invalid\": invalid syntax") 198 | assert.Zero(t, intVal) 199 | 200 | int8Val, err := FromString[int8]("invalid") 201 | require.Error(t, err) 202 | assert.EqualError(t, err, "failed to convert string to type int8: strconv.ParseInt: parsing \"invalid\": invalid syntax") 203 | assert.Zero(t, int8Val) 204 | 205 | int16Val, err := FromString[int16]("invalid") 206 | require.Error(t, err) 207 | assert.EqualError(t, err, "failed to convert string to type int16: strconv.ParseInt: parsing \"invalid\": invalid syntax") 208 | assert.Zero(t, int16Val) 209 | 210 | int32Val, err := FromString[int32]("invalid") 211 | require.Error(t, err) 212 | assert.EqualError(t, err, "failed to convert string to type int32: strconv.ParseInt: parsing \"invalid\": invalid syntax") 213 | assert.Zero(t, int32Val) 214 | 215 | int64Val, err := FromString[int64]("invalid") 216 | require.Error(t, err) 217 | assert.EqualError(t, err, "failed to convert string to type int64: strconv.ParseInt: parsing \"invalid\": invalid syntax") 218 | assert.Zero(t, int64Val) 219 | 220 | uintVal, err := FromString[uint]("invalid") 221 | require.Error(t, err) 222 | assert.EqualError(t, err, "failed to convert string to type uint: strconv.ParseUint: parsing \"invalid\": invalid syntax") 223 | assert.Zero(t, uintVal) 224 | 225 | uint8Val, err := FromString[uint8]("invalid") 226 | require.Error(t, err) 227 | assert.EqualError(t, err, "failed to convert string to type uint8: strconv.ParseUint: parsing \"invalid\": invalid syntax") 228 | assert.Zero(t, uint8Val) 229 | 230 | uint16Val, err := FromString[uint16]("invalid") 231 | require.Error(t, err) 232 | assert.EqualError(t, err, "failed to convert string to type uint16: strconv.ParseUint: parsing \"invalid\": invalid syntax") 233 | assert.Zero(t, uint16Val) 234 | 235 | uint32Val, err := FromString[uint32]("invalid") 236 | require.Error(t, err) 237 | assert.EqualError(t, err, "failed to convert string to type uint32: strconv.ParseUint: parsing \"invalid\": invalid syntax") 238 | assert.Zero(t, uint32Val) 239 | 240 | uint64Val, err := FromString[uint64]("invalid") 241 | require.Error(t, err) 242 | assert.EqualError(t, err, "failed to convert string to type uint64: strconv.ParseUint: parsing \"invalid\": invalid syntax") 243 | assert.Zero(t, uint64Val) 244 | 245 | float32Val, err := FromString[float32]("invalid") 246 | require.Error(t, err) 247 | assert.EqualError(t, err, "failed to convert string to type float32: strconv.ParseFloat: parsing \"invalid\": invalid syntax") 248 | assert.Zero(t, float32Val) 249 | 250 | float64Val, err := FromString[float64]("invalid") 251 | require.Error(t, err) 252 | assert.EqualError(t, err, "failed to convert string to type float64: strconv.ParseFloat: parsing \"invalid\": invalid syntax") 253 | assert.Zero(t, float64Val) 254 | 255 | complex64Val, err := FromString[complex64]("invalid") 256 | require.Error(t, err) 257 | assert.EqualError(t, err, "failed to convert string to type complex64: strconv.ParseComplex: parsing \"invalid\": invalid syntax") 258 | assert.Zero(t, complex64Val) 259 | 260 | complex128Val, err := FromString[complex128]("invalid") 261 | require.Error(t, err) 262 | assert.EqualError(t, err, "failed to convert string to type complex128: strconv.ParseComplex: parsing \"invalid\": invalid syntax") 263 | assert.Zero(t, complex128Val) 264 | 265 | boolVal, err := FromString[bool]("invalid") 266 | require.Error(t, err) 267 | assert.EqualError(t, err, "failed to convert string to type bool: strconv.ParseBool: parsing \"invalid\": invalid syntax") 268 | assert.False(t, boolVal) 269 | }) 270 | 271 | t.Run("Valid", func(t *testing.T) { 272 | stringVal, err := FromString[string]("abcd") 273 | require.NoError(t, err) 274 | assert.Equal(t, "abcd", stringVal) 275 | 276 | intVal, err := FromString[int]("1234") 277 | require.NoError(t, err) 278 | assert.Equal(t, 1234, intVal) 279 | 280 | int8Val, err := FromString[int8]("123") 281 | require.NoError(t, err) 282 | assert.Equal(t, int8(123), int8Val) 283 | 284 | int16Val, err := FromString[int16]("1234") 285 | require.NoError(t, err) 286 | assert.Equal(t, int16(1234), int16Val) 287 | 288 | int32Val, err := FromString[int32]("1234") 289 | require.NoError(t, err) 290 | assert.Equal(t, int32(1234), int32Val) 291 | 292 | int64Val, err := FromString[int64]("1234") 293 | require.NoError(t, err) 294 | assert.Equal(t, int64(1234), int64Val) 295 | 296 | uintVal, err := FromString[uint]("1234") 297 | require.NoError(t, err) 298 | assert.Equal(t, uint(1234), uintVal) 299 | 300 | uint8Val, err := FromString[uint8]("123") 301 | require.NoError(t, err) 302 | assert.Equal(t, uint8(123), uint8Val) 303 | 304 | uint16Val, err := FromString[uint16]("1234") 305 | require.NoError(t, err) 306 | assert.Equal(t, uint16(1234), uint16Val) 307 | 308 | uint32Val, err := FromString[uint32]("1234") 309 | require.NoError(t, err) 310 | assert.Equal(t, uint32(1234), uint32Val) 311 | 312 | uint64Val, err := FromString[uint64]("1234") 313 | require.NoError(t, err) 314 | assert.Equal(t, uint64(1234), uint64Val) 315 | 316 | float32Val, err := FromString[float32]("1234.56") 317 | require.NoError(t, err) 318 | assert.Equal(t, float32(1234.56), float32Val) 319 | 320 | float64Val, err := FromString[float64]("1234.56") 321 | require.NoError(t, err) 322 | assert.Equal(t, float64(1234.56), float64Val) 323 | 324 | complex64Val, err := FromString[complex64]("1234.56") 325 | require.NoError(t, err) 326 | assert.Equal(t, complex64(1234.56), complex64Val) 327 | 328 | complex128Val, err := FromString[complex128]("1234.56") 329 | require.NoError(t, err) 330 | assert.Equal(t, complex128(1234.56), complex128Val) 331 | 332 | boolVal, err := FromString[bool]("true") 333 | require.NoError(t, err) 334 | assert.True(t, boolVal) 335 | 336 | bytesVal, err := FromString[[]byte]("abcd") 337 | require.NoError(t, err) 338 | assert.Equal(t, []byte("abcd"), bytesVal) 339 | 340 | runesVal, err := FromString[[]rune]("abcd") 341 | require.NoError(t, err) 342 | assert.Equal(t, []rune("abcd"), runesVal) 343 | 344 | builderVal, err := FromString[*strings.Builder]("abcd") 345 | require.NoError(t, err) 346 | assert.Equal(t, "abcd", builderVal.String()) 347 | }) 348 | } 349 | 350 | func TestFromStringOrEmpty(t *testing.T) { 351 | t.Run("Unsupported", func(t *testing.T) { 352 | assert.Nil(t, FromStringOrEmpty[func()]("")) 353 | assert.Nil(t, FromStringOrEmpty[map[string]any]("")) 354 | assert.Zero(t, len(FromStringOrEmpty[map[string]any](""))) 355 | assert.Nil(t, FromStringOrEmpty[[]string]("")) 356 | assert.Len(t, FromStringOrEmpty[[]string](""), 0) 357 | assert.Empty(t, FromStringOrEmpty[struct{}]("")) 358 | }) 359 | 360 | t.Run("Empty", func(t *testing.T) { 361 | assert.Nil(t, FromStringOrEmpty[func()]("abcd")) 362 | assert.Nil(t, FromStringOrEmpty[map[string]any]("abcd")) 363 | assert.Zero(t, len(FromStringOrEmpty[map[string]any]("abcd"))) 364 | assert.Nil(t, FromStringOrEmpty[[]string]("abcd")) 365 | assert.Len(t, FromStringOrEmpty[[]string]("abcd"), 0) 366 | assert.Empty(t, FromStringOrEmpty[struct{}]("abcd")) 367 | assert.Equal(t, "", FromStringOrEmpty[string]("")) 368 | assert.Zero(t, FromStringOrEmpty[int]("")) 369 | assert.Zero(t, FromStringOrEmpty[int8]("")) 370 | assert.Zero(t, FromStringOrEmpty[int16]("")) 371 | assert.Zero(t, FromStringOrEmpty[int32]("")) 372 | assert.Zero(t, FromStringOrEmpty[int64]("")) 373 | assert.Zero(t, FromStringOrEmpty[uint]("")) 374 | assert.Zero(t, FromStringOrEmpty[uint8]("")) 375 | assert.Zero(t, FromStringOrEmpty[uint16]("")) 376 | assert.Zero(t, FromStringOrEmpty[uint32]("")) 377 | assert.Zero(t, FromStringOrEmpty[uint64]("")) 378 | assert.Zero(t, FromStringOrEmpty[float32]("")) 379 | assert.Zero(t, FromStringOrEmpty[float64]("")) 380 | assert.Zero(t, FromStringOrEmpty[complex64]("")) 381 | assert.Zero(t, FromStringOrEmpty[complex128]("")) 382 | assert.False(t, FromStringOrEmpty[bool]("")) 383 | assert.Empty(t, FromStringOrEmpty[[]byte]("")) 384 | assert.Empty(t, FromStringOrEmpty[[]rune]("")) 385 | assert.Equal(t, "", FromStringOrEmpty[*strings.Builder]("").String()) 386 | }) 387 | 388 | t.Run("Invalid", func(t *testing.T) { 389 | 390 | }) 391 | 392 | t.Run("Valid", func(t *testing.T) { 393 | assert.Equal(t, "abcd", FromStringOrEmpty[string]("abcd")) 394 | assert.Equal(t, 1234, FromStringOrEmpty[int]("1234")) 395 | assert.Equal(t, int8(123), FromStringOrEmpty[int8]("123")) 396 | assert.Equal(t, int16(1234), FromStringOrEmpty[int16]("1234")) 397 | assert.Equal(t, int32(1234), FromStringOrEmpty[int32]("1234")) 398 | assert.Equal(t, int64(1234), FromStringOrEmpty[int64]("1234")) 399 | assert.Equal(t, uint(1234), FromStringOrEmpty[uint]("1234")) 400 | assert.Equal(t, uint8(123), FromStringOrEmpty[uint8]("123")) 401 | assert.Equal(t, uint16(1234), FromStringOrEmpty[uint16]("1234")) 402 | assert.Equal(t, uint32(1234), FromStringOrEmpty[uint32]("1234")) 403 | assert.Equal(t, uint64(1234), FromStringOrEmpty[uint64]("1234")) 404 | assert.Equal(t, float32(1234.56), FromStringOrEmpty[float32]("1234.56")) 405 | assert.Equal(t, float64(1234.56), FromStringOrEmpty[float64]("1234.56")) 406 | assert.Equal(t, complex64(1234.56), FromStringOrEmpty[complex64]("1234.56")) 407 | assert.Equal(t, complex128(1234.56), FromStringOrEmpty[complex128]("1234.56")) 408 | assert.True(t, FromStringOrEmpty[bool]("true")) 409 | assert.Equal(t, []byte("abcd"), FromStringOrEmpty[[]byte]("abcd")) 410 | assert.Equal(t, []rune("abcd"), FromStringOrEmpty[[]rune]("abcd")) 411 | assert.Equal(t, "abcd", FromStringOrEmpty[*strings.Builder]("abcd").String()) 412 | }) 413 | } 414 | -------------------------------------------------------------------------------- /units.go: -------------------------------------------------------------------------------- 1 | package xo 2 | 3 | import "time" 4 | 5 | const ( 6 | UnitBytesOfTB = 1000 * UnitBytesOfGB 7 | UnitBytesOfGB = 1000 * UnitBytesOfMB 8 | UnitBytesOfMB = 1000 * UnitBytesOfKB 9 | UnitBytesOfKB = 1000 10 | 11 | UnitBytesOfTiB = 1024 * UnitBytesOfGiB 12 | UnitBytesOfGiB = 1024 * UnitBytesOfMiB 13 | UnitBytesOfMiB = 1024 * UnitBytesOfKiB 14 | UnitBytesOfKiB = 1024 15 | ) 16 | 17 | var ( 18 | UnitSecondsOfMonth = 30 * UnitSecondsOfDay 19 | UnitSecondsOfDay = 24 * UnitSecondsOfHour 20 | UnitSecondsOfHour = 60 * UnitSecondsOfMinute 21 | UnitSecondsOfMinute = 60 * time.Second 22 | ) 23 | --------------------------------------------------------------------------------