diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 48fd5dc..ae3f3ee 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -35,4 +35,4 @@ jobs: COLOR=orange fi curl "https://img.shields.io/badge/coverage-$total%25-$COLOR" > badge.svg - aws s3api put-object --cache-control no-cache --content-type image/svg+xml --bucket code-coverage --key go-pkgs.svg --body badge.svg --endpoint-url https://a13880696afbb75cf78cdb89324aafbc.r2.cloudflarestorage.com + aws s3api put-object --cache-control no-cache --content-type image/svg+xml --bucket code-coverage --key go-pkgs.svg --body badge.svg --checksum-algorithm CRC32 --endpoint-url https://a13880696afbb75cf78cdb89324aafbc.r2.cloudflarestorage.com diff --git a/.golangci.yml b/.golangci.yml index 842b756..adf3063 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -33,9 +33,6 @@ output: # print linter name in the end of issue text, default is true print-linter-name: true - # make issues output unique by line, default is true - uniq-by-line: true - # add a prefix to the output file references; default is no prefix path-prefix: "" @@ -70,7 +67,6 @@ linters: - noctx - nolintlint - prealloc - - exportloopref - staticcheck - stylecheck - typecheck @@ -80,7 +76,6 @@ linters: - nlreturn - whitespace - gocritic - - exportloopref - gocognit - gofumpt - godot diff --git a/go.mod b/go.mod index c8d09c8..a7da94c 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/neurocode-io/go-pkgs -go 1.21 +go 1.23.0 require ( - github.com/stretchr/testify v1.9.0 - golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa + github.com/stretchr/testify v1.10.0 + golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa ) require ( diff --git a/go.sum b/go.sum index 816483a..791cbf5 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,12 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w= -golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/map/async_map.go b/map/async_map.go index 149c8a2..4883e6e 100644 --- a/map/async_map.go +++ b/map/async_map.go @@ -49,7 +49,7 @@ func (m *AsyncMap[T, V]) Load(key T) (value V, ok bool) { return value, ok } - return v.(V), ok + return v.(V), ok //nolint:errcheck } /* @@ -64,7 +64,7 @@ Example func (m *AsyncMap[T, V]) LoadOrStore(key T, value V) (V, bool) { v, loaded := m.item.LoadOrStore(key, value) - return v.(V), loaded + return v.(V), loaded //nolint:errcheck } /* @@ -83,7 +83,7 @@ func (m *AsyncMap[T, V]) LoadAndDelete(key T) (value V, ok bool) { return value, ok } - return v.(V), ok + return v.(V), ok //nolint:errcheck } /* @@ -115,6 +115,6 @@ Example */ func (m *AsyncMap[T, V]) Range(fn func(key T, value V) bool) { m.item.Range(func(key, value any) bool { - return fn(key.(T), value.(V)) + return fn(key.(T), value.(V)) //nolint:errcheck }) } diff --git a/structs/structs.go b/structs/structs.go new file mode 100644 index 0000000..54d005d --- /dev/null +++ b/structs/structs.go @@ -0,0 +1,92 @@ +// Package structs provides a set of functions to manipulate structs. +// +// Example +// +// import "github.com/neurocode-io/go-pkgs/structs" +// +// input := struct { +// Name string +// Age int +// }{ +// Name: "John", +// Age: 30, +// } +// +// result := structs.ToMap(input) +// // result == map[string]any{"Name": "John", "Age": 30} +package structs + +import ( + "fmt" + "reflect" +) + +/* +ToMap converts a struct to a map[string]any. + +Example + + input := struct { + Name string + Age int + }{ + Name: "John", + Age: 30, + } + result := structs.ToMap(input) + // result == map[string]any{"Name": "John", "Age": 30} +*/ +//nolint:gocognit,cyclop +func ToMap(input any) any { + val := reflect.ValueOf(input) + + switch val.Kind() { //nolint:exhaustive + case reflect.Pointer: + // Dereference pointers + if val.IsNil() { + return nil + } + + return ToMap(val.Elem().Interface()) + + case reflect.Struct: + // Convert struct to map + out := make(map[string]any) + typ := val.Type() + for i := 0; i < val.NumField(); i++ { + field := typ.Field(i) + // Skip unexported fields + if field.PkgPath != "" { + continue + } + out[field.Name] = ToMap(val.Field(i).Interface()) + } + + return out + + case reflect.Slice, reflect.Array: + // Convert slices/arrays to a slice of interfaces + length := val.Len() + out := make([]any, 0, length) + for i := 0; i < length; i++ { + out = append(out, ToMap(val.Index(i).Interface())) + } + + return out + + case reflect.Map: + // Convert maps to map[string]any + out := make(map[string]any) + for _, key := range val.MapKeys() { + // We assume string keys; otherwise convert them as needed + strKey := fmt.Sprintf("%v", key.Interface()) + out[strKey] = ToMap(val.MapIndex(key).Interface()) + } + + return out + + default: + // basic types, return as-is + return input + } +} diff --git a/structs/structs_test.go b/structs/structs_test.go new file mode 100644 index 0000000..6f4a7bb --- /dev/null +++ b/structs/structs_test.go @@ -0,0 +1,142 @@ +package structs + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToMap(t *testing.T) { + t.Run("basic types", func(t *testing.T) { + t.Run("returns integers as-is", func(t *testing.T) { + result := ToMap(42) + assert.Equal(t, 42, result) + }) + + t.Run("returns strings as-is", func(t *testing.T) { + result := ToMap("hello") + assert.Equal(t, "hello", result) + }) + }) + + t.Run("pointers", func(t *testing.T) { + t.Run("handles nil pointer", func(t *testing.T) { + var ptr *string + result := ToMap(ptr) + assert.Nil(t, result) + }) + + t.Run("dereferences pointer", func(t *testing.T) { + val := "test" + result := ToMap(&val) + assert.Equal(t, "test", result) + }) + }) + + t.Run("structs", func(t *testing.T) { + type Person struct { + Name string + Age int + } + + t.Run("converts struct to map", func(t *testing.T) { + person := Person{Name: "Alice", Age: 30} + result := ToMap(person) + + expected := map[string]any{ + "Name": "Alice", + "Age": 30, + } + assert.Equal(t, expected, result) + }) + + t.Run("handles nested structs", func(t *testing.T) { + type Address struct { + City string + Country string + } + type Employee struct { + Person + Address Address + } + + emp := Employee{ + Person: Person{Name: "Bob", Age: 25}, + Address: Address{City: "Reykjavik", Country: "Iceland"}, + } + + expected := map[string]any{ + "Person": map[string]any{ + "Name": "Bob", + "Age": 25, + }, + "Address": map[string]any{ + "City": "Reykjavik", + "Country": "Iceland", + }, + } + assert.Equal(t, expected, ToMap(emp)) + }) + + t.Run("handles slices", func(t *testing.T) { + type Person struct { + Name string + Age int + } + + type Company struct { + Employees []Person + } + + company := Company{ + Employees: []Person{{Name: "Alice", Age: 30}, {Name: "Bob", Age: 25}}, + } + + expected := map[string]any{ + "Employees": []any{ + map[string]any{"Name": "Alice", "Age": 30}, + map[string]any{"Name": "Bob", "Age": 25}, + }, + } + + assert.Equal(t, expected, ToMap(company)) + }) + }) + + t.Run("slices", func(t *testing.T) { + t.Run("converts slice to array", func(t *testing.T) { + input := []int{1, 2, 3} + expected := []any{1, 2, 3} + assert.Equal(t, expected, ToMap(input)) + }) + + t.Run("handles nested slices", func(t *testing.T) { + input := [][]int{{1, 2}, {3, 4}} + expected := []any{ + []any{1, 2}, + []any{3, 4}, + } + assert.Equal(t, expected, ToMap(input)) + }) + }) + + t.Run("maps", func(t *testing.T) { + t.Run("converts map to string keys", func(t *testing.T) { + input := map[string]int{"one": 1, "two": 2} + expected := map[string]any{ + "one": 1, + "two": 2, + } + assert.Equal(t, expected, ToMap(input)) + }) + + t.Run("handles non-string keys", func(t *testing.T) { + input := map[int]string{1: "one", 2: "two"} + expected := map[string]any{ + "1": "one", + "2": "two", + } + assert.Equal(t, expected, ToMap(input)) + }) + }) +}