Compare commits

...
23 Commits
Author SHA1 Message Date
lukaszraczylo 1df2ae5d16 Update dependencies. 2023-10-12 16:29:50 +01:00
lukaszraczylo 39f489210b Update dependencies. 2023-09-06 09:26:06 +01:00
lukaszraczylo 21c9119bcb Add management of the retries via headers. 2023-04-10 03:12:39 +01:00
lukaszraczylo a7b385f0b7 Do not cache mutations. 2023-04-10 01:46:25 +01:00
lukaszraczylo 7d3abf5464 Replace allegro/bigcache with something lighter.
In high frequency environment the allegro bigcache caused significant
memory usage overhead. This commit replaces it with something much lighter.
2023-04-08 21:36:11 +01:00
lukaszraczylo 7af4e1f96a Memleak fix 2023-04-08 20:45:05 +01:00
lukaszraczylo 7623410bfe fixup! fixup! ... 2023-04-08 06:20:11 +01:00
lukaszraczylo 8e26d79df7 fixup! ... 2023-04-08 05:58:11 +01:00
lukaszraczylo aa25c43795 ... 2023-04-08 05:44:23 +01:00
lukaszraczylo dfb082ad51 fixup! fixup! fixup! Avoid unnecessary copying of the whole struct. 2023-04-08 05:19:57 +01:00
lukaszraczylo ce736958c6 fixup! fixup! Avoid unnecessary copying of the whole struct. 2023-04-08 05:18:30 +01:00
lukaszraczylo 4472041b21 fixup! Avoid unnecessary copying of the whole struct. 2023-04-08 05:17:09 +01:00
lukaszraczylo b81e612f51 Avoid unnecessary copying of the whole struct. 2023-04-08 05:14:14 +01:00
lukaszraczylo 3c508eaf15 fixup! fixup! Finally - fixed the cache version for single query if its enabled via header. 2023-04-08 03:34:32 +01:00
lukaszraczylo 4d0c04cb26 fixup! Finally - fixed the cache version for single query if its enabled via header. 2023-04-08 03:29:35 +01:00
lukaszraczylo 55182f2f3d Finally - fixed the cache version for single query if its enabled via header. 2023-04-08 03:25:33 +01:00
lukaszraczylo 841c136e71 fixup! fixup! Add randomisation test for cache concurrency. 2023-04-08 03:14:53 +01:00
lukaszraczylo 833517f35c fixup! Add randomisation test for cache concurrency. 2023-04-08 03:13:30 +01:00
lukaszraczylo 07e4f75a0a Add randomisation test for cache concurrency. 2023-04-08 03:05:55 +01:00
lukaszraczylo b8044c5d67 Fix concurrency issue. 2023-04-08 03:01:19 +01:00
lukaszraczylo cf3076d701 Update documentation. 2023-04-08 02:11:46 +01:00
lukaszraczylo 5a222aa316 fixup! Add missing retry mechanism. I have no idea how this was missed, but ho hum. 2023-04-08 02:09:18 +01:00
lukaszraczylo d843238d46 Add missing retry mechanism.
I have no idea how this was missed, but ho hum.
2023-04-08 01:59:28 +01:00
14 changed files with 529 additions and 243 deletions
+6
View File
@@ -9,6 +9,7 @@ Ps. It's Hasura friendly.
- [Features](#features) - [Features](#features)
- [Usage example](#usage-example) - [Usage example](#usage-example)
- [Environment variables](#environment-variables) - [Environment variables](#environment-variables)
- [Modifiers on the fly](#modifiers-on-the-fly)
- [Cache](#cache) - [Cache](#cache)
- [Example reader code](#example-reader-code) - [Example reader code](#example-reader-code)
- [Tips](#tips) - [Tips](#tips)
@@ -40,6 +41,11 @@ Therefore, I present you the simple client to which you can copy & paste your gr
* `GRAPHQL_RETRIES_NUMBER` - Number of retries: Default: `1` * `GRAPHQL_RETRIES_NUMBER` - Number of retries: Default: `1`
* `GRAPHQL_RETRIES_DELAY` - Delay in retries in milliseconds. Default: `250` * `GRAPHQL_RETRIES_DELAY` - Delay in retries in milliseconds. Default: `250`
### Modifiers on the fly
* `gql.SetEndpoint('your-endpoint-url')` - modifies endpoint, without the need to set the environment variable
* `gql.SetOutput('byte')` - modifies output format, without the need to set the environment variable
### Cache ### Cache
You have two options to enable the cache: You have two options to enable the cache:
+35 -15
View File
@@ -2,9 +2,10 @@ package gql
import ( import (
"crypto/tls" "crypto/tls"
"fmt"
"time" "time"
"github.com/lukaszraczylo/go-simple-graphql/utils/concurrency" "github.com/akyoto/cache"
"github.com/lukaszraczylo/go-simple-graphql/utils/logger" "github.com/lukaszraczylo/go-simple-graphql/utils/logger"
"golang.org/x/net/http2" "golang.org/x/net/http2"
@@ -14,18 +15,16 @@ import (
"os" "os"
"strings" "strings"
"github.com/allegro/bigcache"
"github.com/gookit/goutil/envutil" "github.com/gookit/goutil/envutil"
) )
func NewConnection() *BaseClient { func NewConnection() *BaseClient {
b := &BaseClient{ b := &BaseClient{
client: &http.Client{}, client: &http.Client{},
concurrencyManager: concurrency.NewPool(-1), MaxGoRoutines: -1,
MaxGoRoutines: -1, LoggingLevel: logger.Warn,
LoggingLevel: logger.Warn, LoggerColorful: true,
LoggerColorful: true,
} }
if b.LoggerWriter == nil { if b.LoggerWriter == nil {
@@ -42,13 +41,7 @@ func NewConnection() *BaseClient {
b.retries.max = envutil.GetInt("GRAPHQL_RETRIES_MAX", 1) b.retries.max = envutil.GetInt("GRAPHQL_RETRIES_MAX", 1)
b.retries.delay = envutil.GetInt("GRAPHQL_RETRIES_DELAY", 300) b.retries.delay = envutil.GetInt("GRAPHQL_RETRIES_DELAY", 300)
if b.cache.enabled { b.enableCache()
var err error
b.cache.client, err = bigcache.NewBigCache(bigcache.DefaultConfig(time.Duration(b.cache.ttl) * time.Second))
if err != nil {
b.Logger.Error(b, "Error while creating cache client;", "error", err.Error())
}
}
b_tmp_log_level := envutil.Getenv("LOG_LEVEL", "info") b_tmp_log_level := envutil.Getenv("LOG_LEVEL", "info")
@@ -71,9 +64,20 @@ func NewConnection() *BaseClient {
}) })
var httpClient *http.Client var httpClient *http.Client
httpTransport := &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 15 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
DisableKeepAlives: false,
}
if strings.HasPrefix(b.endpoint, "http://") { if strings.HasPrefix(b.endpoint, "http://") {
httpClient = &http.Client{ httpClient = &http.Client{
Transport: http.DefaultTransport, Transport: httpTransport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
} }
} else { } else {
tlsClientConfig := &tls.Config{} tlsClientConfig := &tls.Config{}
@@ -83,6 +87,8 @@ func NewConnection() *BaseClient {
http2Transport := &http2.Transport{ http2Transport := &http2.Transport{
AllowHTTP: true, AllowHTTP: true,
TLSClientConfig: tlsClientConfig, TLSClientConfig: tlsClientConfig,
ReadIdleTimeout: 15 * time.Second,
PingTimeout: 15 * time.Second,
} }
httpClient = &http.Client{ httpClient = &http.Client{
Transport: http2Transport, Transport: http2Transport,
@@ -102,3 +108,17 @@ func (b *BaseClient) SetEndpoint(endpoint string) {
func (b *BaseClient) SetOutput(output string) { func (b *BaseClient) SetOutput(output string) {
b.responseType = output b.responseType = output
} }
func (b *BaseClient) enableCache() {
var err error
b.cache.client = cache.New(time.Duration(b.cache.ttl) * time.Second * 2)
if err != nil {
fmt.Println(">> Error while creating cache client;", "error", err.Error())
panic(err)
}
}
func (b *BaseClient) disableCache() {
b.Logger.Debug(b, "Disabling cache")
b.cache.enabled = false
}
+29 -5
View File
@@ -10,16 +10,32 @@ import (
func TestNewConnection(t *testing.T) { func TestNewConnection(t *testing.T) {
tests := []struct { tests := []struct {
want *BaseClient want *BaseClient
name string cache bool
endpoint string
output string
name string
}{ }{
{ {
name: "Test NewConnection", name: "Test NewConnection https",
endpoint: "https://api.github.com/graphql",
output: "string",
cache: true,
want: &BaseClient{ want: &BaseClient{
endpoint: "https://api.github.com/graphql", endpoint: "https://api.github.com/graphql",
responseType: "string", responseType: "string",
}, },
}, },
{
name: "Test NewConnection http",
endpoint: "http://api.github.com/graphql",
output: "mapstring",
cache: false,
want: &BaseClient{
endpoint: "http://api.github.com/graphql",
responseType: "mapstring",
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -27,11 +43,19 @@ func TestNewConnection(t *testing.T) {
// and GRAPHQL_OUTPUT to string // and GRAPHQL_OUTPUT to string
current := os.Getenv("GRAPHQL_ENDPOINT") current := os.Getenv("GRAPHQL_ENDPOINT")
defer os.Setenv("GRAPHQL_ENDPOINT", current) defer os.Setenv("GRAPHQL_ENDPOINT", current)
os.Setenv("GRAPHQL_ENDPOINT", "https://api.github.com/graphql") os.Setenv("GRAPHQL_ENDPOINT", tt.endpoint)
got := NewConnection() got := NewConnection()
got.SetEndpoint(tt.endpoint)
got.SetOutput(tt.output)
if tt.cache {
got.enableCache()
} else {
got.disableCache()
}
assert.Equal(t, tt.want.endpoint, got.endpoint) assert.Equal(t, tt.want.endpoint, got.endpoint)
assert.Equal(t, tt.want.responseType, got.responseType) assert.Equal(t, tt.want.responseType, got.responseType)
assert.Equal(t, tt.cache, got.cache.enabled)
}) })
} }
} }
+3 -3
View File
@@ -2,10 +2,10 @@ package gql
func (c *BaseClient) cacheLookup(hash string) []byte { func (c *BaseClient) cacheLookup(hash string) []byte {
if c.cache.client != nil { if c.cache.client != nil {
g, err := c.cache.client.Get(hash) obj, found := c.cache.client.Get(hash)
if err == nil { if found {
c.Logger.Debug(c, "Cache hit;", "hash", hash) c.Logger.Debug(c, "Cache hit;", "hash", hash)
return g return obj.([]byte)
} }
// if error is not nil it means that hash is not present in cache // if error is not nil it means that hash is not present in cache
c.Logger.Debug(c, "Cache miss;", "hash", hash) c.Logger.Debug(c, "Cache miss;", "hash", hash)
+10 -10
View File
@@ -3,27 +3,27 @@ module github.com/lukaszraczylo/go-simple-graphql
go 1.20 go 1.20
require ( require (
github.com/allegro/bigcache v1.2.1 github.com/akyoto/cache v1.0.6
github.com/avast/retry-go/v4 v4.3.3 github.com/avast/retry-go/v4 v4.5.0
github.com/gookit/goutil v0.6.8 github.com/gookit/goutil v0.6.12
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/stretchr/testify v1.8.2 github.com/stretchr/testify v1.8.4
golang.org/x/net v0.9.0 golang.org/x/net v0.17.0
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gookit/color v1.5.3 // indirect github.com/gookit/color v1.5.4 // indirect
github.com/kr/pretty v0.3.0 // indirect github.com/kr/pretty v0.3.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 // indirect golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 // indirect
golang.org/x/sync v0.1.0 // indirect golang.org/x/sync v0.4.0 // indirect
golang.org/x/sys v0.7.0 // indirect golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.7.0 // indirect golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.9.0 // indirect golang.org/x/text v0.13.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+20 -26
View File
@@ -1,16 +1,16 @@
github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= github.com/akyoto/cache v1.0.6 h1:5XGVVYoi2i+DZLLPuVIXtsNIJ/qaAM16XT0LaBaXd2k=
github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/akyoto/cache v1.0.6/go.mod h1:WfxTRqKhfgAG71Xh6E3WLpjhBtZI37O53G4h5s+3iM4=
github.com/avast/retry-go/v4 v4.3.3 h1:G56Bp6mU0b5HE1SkaoVjscZjlQb0oy4mezwY/cGH19w= github.com/avast/retry-go/v4 v4.5.0 h1:QoRAZZ90cj5oni2Lsgl2GW8mNTnUCnmpx/iKpwVisHg=
github.com/avast/retry-go/v4 v4.3.3/go.mod h1:rg6XFaiuFYII0Xu3RDbZQkxCofFwruZKW8oEF1jpWiU= github.com/avast/retry-go/v4 v4.5.0/go.mod h1:7hLEXp0oku2Nir2xBAsg0PTphp9z71bN5Aq1fboC3+I=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gookit/color v1.5.3 h1:twfIhZs4QLCtimkP7MOxlF3A0U/5cDPseRT9M/+2SCE= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.3/go.mod h1:NUzwzeehUfl7GIb36pqId+UGmRfQcU/WiiyTTeNjHtE= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gookit/goutil v0.6.8 h1:B2XXSCGav5TXWtKRT9i/s/owOLXXB7sY6UsfqeSLroE= github.com/gookit/goutil v0.6.12 h1:73vPUcTtVGXbhSzBOFcnSB1aJl7Jq9np3RAE50yIDZc=
github.com/gookit/goutil v0.6.8/go.mod h1:u+Isykc6RQcZ4GQzulsaGm+Famd97U5Tzp3aQyo+jyA= github.com/gookit/goutil v0.6.12/go.mod h1:g6krlFib8xSe3G1h02IETowOtrUGpAmetT8IevDpvpM=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -30,32 +30,26 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4= golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+38 -41
View File
@@ -3,12 +3,10 @@ package gql
import ( import (
"context" "context"
"fmt" "fmt"
"reflect"
"strconv" "strconv"
"github.com/lukaszraczylo/go-simple-graphql/utils/helpers"
"github.com/gookit/goutil/strutil" "github.com/gookit/goutil/strutil"
"github.com/lukaszraczylo/go-simple-graphql/utils/helpers"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
) )
@@ -70,52 +68,45 @@ func (c *BaseClient) NewQuery(q ...any) *Query {
// It looks a bit weird because of the backward compatibility // It looks a bit weird because of the backward compatibility
func (c *BaseClient) Query(queryContent string, queryVariables interface{}, queryHeaders map[string]interface{}) (any, error) { func (c *BaseClient) Query(queryContent string, queryVariables interface{}, queryHeaders map[string]interface{}) (any, error) {
query := c.NewQuery(queryContent, queryVariables)
compiledQuery := c.NewQuery(queryContent, queryVariables)
parseQueryHeaders, enabledCache, headersModified, enabledRetries := compiledQuery.parseQueryHeaders(queryHeaders)
var should_cache, retries_enabled bool
var queryHash string var queryHash string
var cachedResponse []byte
var cacheBaseClient BaseClient
// Check for library specific headers if headersModified {
if len(queryHeaders) > 0 { should_cache = enabledCache
cacheBaseClient = reflect.ValueOf(*c).Interface().(BaseClient) retries_enabled = enabledRetries
queryHeaders = c.parseQueryHeaders(queryHeaders) } else {
should_cache = c.cache.enabled
retries_enabled = c.retries.enabled
} }
if c.cache.enabled { // if queryContent does not start with `query` then we don't want to cache it
queryHash = strutil.Md5(query.compiledQuery) // because it's probably a mutation or subscription.
c.Logger.Debug(c, "Hash calculated;", "hash:", queryHash) if !strutil.HasPrefix(queryContent, "query") {
should_cache = false
cachedResponse = c.cacheLookup(queryHash)
if cachedResponse != nil {
c.Logger.Debug(c, "Found cached response")
return c.decodeResponse(cachedResponse), nil
} else {
c.Logger.Debug(c, "No cached response found")
}
} }
response, err := c.executeQuery(query.compiledQuery, queryHeaders) if should_cache {
if err != nil { queryHash = strutil.Md5(fmt.Sprintf("%s-%+v", compiledQuery.compiledQuery, queryHeaders))
c.Logger.Error(c, "Error while executing query;", "error", err.Error())
return nil, err
} }
jsonData, err := json.Marshal(response) q := &queryExecutor{
if err != nil { client: c,
c.Logger.Error(c, "Error while converting to json;", "error", err.Error()) query: compiledQuery.compiledQuery,
return nil, err headers: parseQueryHeaders,
context: context.Background(),
should_cache: should_cache,
retries_enabled: retries_enabled,
hash: queryHash,
} }
if c.cache.enabled { q.execute()
c.cache.client.Set(queryHash, jsonData) defer q.done()
}
if len(queryHeaders) > 0 { return q.result.data, q.result.errors
*c = cacheBaseClient
}
return c.decodeResponse(jsonData), err
} }
func (c *BaseClient) decodeResponse(jsonData []byte) any { func (c *BaseClient) decodeResponse(jsonData []byte) any {
@@ -138,18 +129,24 @@ func (c *BaseClient) decodeResponse(jsonData []byte) any {
} }
} }
func (c *BaseClient) parseQueryHeaders(queryHeaders map[string]interface{}) (returnHeaders map[string]interface{}) { func (q *Query) parseQueryHeaders(queryHeaders map[string]interface{}) (returnHeaders map[string]interface{}, cache_enabled bool, headers_modified bool, retries_enabled bool) {
returnHeaders = make(map[string]interface{}) returnHeaders = make(map[string]interface{})
var err error
for k, v := range queryHeaders { for k, v := range queryHeaders {
if k == "gqlcache" { if k == "gqlcache" {
c.cache.enabled, _ = strconv.ParseBool(fmt.Sprintf("%v", v)) cache_enabled, err = strconv.ParseBool(fmt.Sprintf("%v", v))
if err != nil {
panic(fmt.Sprintf("Unable to parse gqlcache value %s", err.Error()))
}
headers_modified = true
continue continue
} }
if k == "gqlretries" { if k == "gqlretries" {
c.retries.enabled, _ = strconv.ParseBool(fmt.Sprintf("%v", v)) retries_enabled, _ = strconv.ParseBool(fmt.Sprintf("%v", v))
continue continue
} }
returnHeaders[k] = v returnHeaders[k] = v
} }
return returnHeaders return returnHeaders, cache_enabled, headers_modified, retries_enabled
} }
+232 -3
View File
@@ -1,6 +1,7 @@
package gql package gql
import ( import (
"math/rand"
"reflect" "reflect"
"testing" "testing"
@@ -40,6 +41,17 @@ func TestBaseClient_convertToJson(t *testing.T) {
}, },
want: []byte(`{"query":"query { hello }","variables":{"name":"John"}}`), want: []byte(`{"query":"query { hello }","variables":{"name":"John"}}`),
}, },
{
name: "Test NewQuery with modifier",
fields: fields{},
args: args{
v: map[string]interface{}{
"query": "query @cached(ttl: 120) { hello }",
"variables": map[string]interface{}{"name": "John"},
},
},
want: []byte(`{"query":"query @cached(ttl: 120) { hello }","variables":{"name":"John"}}`),
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -88,6 +100,19 @@ func TestBaseClient_NewQuery(t *testing.T) {
compiledQuery: []byte(`{"variables":{"name":"John"},"query":"query { hello }"}`), compiledQuery: []byte(`{"variables":{"name":"John"},"query":"query { hello }"}`),
}, },
}, },
{
name: "Test NewQuery with modifier",
fields: fields{},
args: args{
q: []any{
"query @cached(ttl: 120) { hello }",
map[string]interface{}{"name": "John"},
},
},
want: &Query{
compiledQuery: []byte(`{"variables":{"name":"John"},"query":"query @cached(ttl: 120) { hello }"}`),
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -133,6 +158,20 @@ func TestBaseClient_decodeResponse(t *testing.T) {
}, },
want: []byte(`{"data":{"hello":"world"}}`), want: []byte(`{"data":{"hello":"world"}}`),
}, },
{
name: "Test decodeResponse - mapstring",
fields: fields{
responseType: "mapstring",
},
args: args{
jsonData: []byte(`{"data":{"hello":"world"}}`),
},
want: map[string]interface{}{
"data": map[string]interface{}{
"hello": "world",
},
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -297,10 +336,10 @@ func TestBaseClient_parseQueryHeaders(t *testing.T) {
queryHeaders map[string]interface{} queryHeaders map[string]interface{}
} }
tests := []struct { tests := []struct {
name string
fields fields fields fields
args args args args
wantReturnHeaders map[string]interface{} wantReturnHeaders map[string]interface{}
name string
}{ }{
{ {
name: "Test parseQueryHeaders - no change", name: "Test parseQueryHeaders - no change",
@@ -332,10 +371,200 @@ func TestBaseClient_parseQueryHeaders(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := NewConnection() q := &Query{}
if gotReturnHeaders := c.parseQueryHeaders(tt.args.queryHeaders); !reflect.DeepEqual(gotReturnHeaders, tt.wantReturnHeaders) { if gotReturnHeaders, _, _, _ := q.parseQueryHeaders(tt.args.queryHeaders); !reflect.DeepEqual(gotReturnHeaders, tt.wantReturnHeaders) {
t.Errorf("BaseClient.parseQueryHeaders() = %v, want %v", gotReturnHeaders, tt.wantReturnHeaders) t.Errorf("BaseClient.parseQueryHeaders() = %v, want %v", gotReturnHeaders, tt.wantReturnHeaders)
} }
}) })
} }
} }
func TestBaseClient_QueryCache(t *testing.T) {
type fields struct {
graphql_endpoint string
cache_enabled bool
}
type args struct {
queryVariables interface{}
queryHeaders map[string]interface{}
queryContent string
}
tests := []struct {
want any
args args
name string
fields fields
wantErr bool
}{
{
name: "Test QueryCache - enabled",
fields: fields{
graphql_endpoint: "https://spacex-production.up.railway.app/",
cache_enabled: true,
},
args: args{
queryContent: `query Dragons {
dragons {
name
}
}`,
queryHeaders: map[string]interface{}{
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
"content-type": "application/json",
},
},
},
{
name: "Test QueryCache - disabled",
fields: fields{
graphql_endpoint: "https://spacex-production.up.railway.app/",
cache_enabled: false,
},
args: args{
queryContent: `query Dragons {
dragons {
name
}
}`,
queryHeaders: map[string]interface{}{
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
"content-type": "application/json",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewConnection()
c.cache.enabled = tt.fields.cache_enabled
if tt.fields.graphql_endpoint != "" {
c.endpoint = tt.fields.graphql_endpoint
}
_, err := c.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
if !tt.wantErr {
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
})
}
}
func TestBaseClient_QueryCacheRandomizedRace(t *testing.T) {
type fields struct {
graphql_endpoint string
cache_enabled bool
}
type args struct {
queryVariables interface{}
queryHeaders map[string]interface{}
queryContent string
}
tests := []struct {
want any
args args
name string
fields fields
wantErr bool
}{
{
name: "Test QueryCache - enabled",
fields: fields{
graphql_endpoint: "https://spacex-production.up.railway.app/",
},
args: args{
queryContent: `query Dragons {
dragons {
name
}
}`,
queryHeaders: map[string]interface{}{
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
"content-type": "application/json",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewConnection()
if tt.fields.graphql_endpoint != "" {
c.endpoint = tt.fields.graphql_endpoint
}
for i := 0; i < 10; i++ {
c.cache.enabled = rand.Intn(2) == 0
go func() {
_, err := c.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
if !tt.wantErr {
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
}()
}
})
}
}
func TestBaseClient_QueryCacheRandomizedRaceViaHeader(t *testing.T) {
type fields struct {
graphql_endpoint string
cache_enabled bool
}
type args struct {
queryVariables interface{}
queryHeaders map[string]interface{}
queryContent string
}
tests := []struct {
want any
args args
name string
fields fields
wantErr bool
}{
{
name: "Test QueryCache - enabled",
fields: fields{
graphql_endpoint: "https://spacex-production.up.railway.app/",
},
args: args{
queryContent: `query Dragons {
dragons {
name
}
}`,
queryHeaders: map[string]interface{}{
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
"content-type": "application/json",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewConnection()
if tt.fields.graphql_endpoint != "" {
c.endpoint = tt.fields.graphql_endpoint
}
for i := 0; i < 10; i++ {
go func() {
tt.args.queryHeaders["gqlcache"] = rand.Intn(2) == 0
_, err := c.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
if !tt.wantErr {
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
}()
}
})
}
}
+58 -2
View File
@@ -3,8 +3,9 @@ package gql
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"time"
"github.com/avast/retry-go/v4" "github.com/avast/retry-go/v4"
) )
@@ -21,6 +22,11 @@ func (c *BaseClient) executeQuery(query []byte, headers any) (result any, err er
httpRequest.Header.Add(key, fmt.Sprintf("%s", value)) httpRequest.Header.Add(key, fmt.Sprintf("%s", value))
} }
var retries_available = c.retries.max
if !c.retries.enabled {
retries_available = 1
}
var httpResponse *http.Response var httpResponse *http.Response
err = retry.Do( err = retry.Do(
@@ -30,13 +36,14 @@ func (c *BaseClient) executeQuery(query []byte, headers any) (result any, err er
c.Logger.Error(c, "Error while executing http request;", "error", err.Error()) c.Logger.Error(c, "Error while executing http request;", "error", err.Error())
return err return err
} }
defer io.Copy(io.Discard, httpResponse.Body) // equivalent to `cp body /dev/null`
defer httpResponse.Body.Close() defer httpResponse.Body.Close()
if httpResponse.StatusCode <= 200 && httpResponse.StatusCode >= 204 { if httpResponse.StatusCode <= 200 && httpResponse.StatusCode >= 204 {
return err return err
} }
body, err := ioutil.ReadAll(httpResponse.Body) body, err := io.ReadAll(httpResponse.Body)
if err != nil { if err != nil {
c.Logger.Error(c, "Error while reading http response;", "error", err.Error()) c.Logger.Error(c, "Error while reading http response;", "error", err.Error())
return err return err
@@ -50,6 +57,13 @@ func (c *BaseClient) executeQuery(query []byte, headers any) (result any, err er
return nil return nil
}, },
retry.OnRetry(func(n uint, err error) {
c.Logger.Error(c, "Retrying query", "error", err.Error())
}),
retry.Attempts(uint(retries_available)),
retry.DelayType(retry.BackOffDelay),
retry.Delay(time.Duration(c.retries.delay)*time.Second),
retry.LastErrorOnly(true),
) )
if len(queryResult.Errors) > 0 { if len(queryResult.Errors) > 0 {
@@ -57,3 +71,45 @@ func (c *BaseClient) executeQuery(query []byte, headers any) (result any, err er
} }
return queryResult.Data, nil return queryResult.Data, nil
} }
func (q *queryExecutor) execute() {
if q.should_cache {
cachedResponse := q.client.cacheLookup(q.hash)
if cachedResponse != nil {
q.client.Logger.Debug(q.client, "Found cached response")
q.result.data = q.client.decodeResponse(cachedResponse)
return // return cached response
} else {
q.client.Logger.Debug(q.client, "No cached response found")
}
}
if q.retries_enabled {
q.client.Logger.Debug(q.client, "Retries enabled")
q.client.retries.enabled = true
}
response, err := q.client.executeQuery(q.query, q.headers)
if err != nil {
q.client.Logger.Error(q.client, "Error while executing query;", "error", err.Error())
q.result.errors = err
return
}
jsonData, err := json.Marshal(response)
if err != nil {
q.client.Logger.Error(q.client, "Error while converting to json;", "error", err.Error())
q.result.errors = err
return
}
if q.should_cache {
q.client.cache.client.Set(q.hash, jsonData, time.Duration(q.client.cache.ttl)*time.Second)
}
q.result.data = q.client.decodeResponse(jsonData)
}
func (q *queryExecutor) done() {
q = nil
}
+29 -16
View File
@@ -4,14 +4,12 @@ import (
"context" "context"
"net/http" "net/http"
"github.com/lukaszraczylo/go-simple-graphql/utils/concurrency" "github.com/akyoto/cache"
"github.com/lukaszraczylo/go-simple-graphql/utils/logger" "github.com/lukaszraczylo/go-simple-graphql/utils/logger"
"github.com/allegro/bigcache"
) )
type cacheStore struct { type cacheStore struct {
client *bigcache.BigCache client *cache.Cache
ttl int ttl int
enabled bool enabled bool
} }
@@ -23,18 +21,17 @@ type retriesConfig struct {
} }
type BaseClient struct { type BaseClient struct {
Logger Logger Logger Logger
LoggerWriter Writer LoggerWriter Writer
client *http.Client client *http.Client
concurrencyManager *concurrency.Pool endpoint string
endpoint string responseType string
responseType string cache cacheStore
cache cacheStore MaxGoRoutines int
MaxGoRoutines int LoggingLevel logger.LogLevel
LoggingLevel logger.LogLevel LoggerColorful bool
LoggerColorful bool validate bool
validate bool retries retriesConfig
retries retriesConfig
} }
type Query struct { type Query struct {
@@ -44,6 +41,22 @@ type Query struct {
compiledQuery []byte `json:"compiledQuery"` compiledQuery []byte `json:"compiledQuery"`
} }
type queryExecutor struct {
result queryExecutorResult
context context.Context
client *BaseClient
headers map[string]interface{}
hash string
query []byte
should_cache bool
retries_enabled bool
}
type queryExecutorResult struct {
data interface{}
errors error
}
type request struct { type request struct {
Variables any `json:"variables"` Variables any `json:"variables"`
Query string `json:"query"` Query string `json:"query"`
-30
View File
@@ -1,30 +0,0 @@
package concurrency
type Pool struct {
queue chan struct{}
jobs int
}
func NewPool(size int) *Pool {
if size < 0 {
size = 0
}
return &Pool{
jobs: size,
queue: make(chan struct{}, size),
}
}
func (p *Pool) Enqueue(job func(params ...any), params ...any) {
if p.jobs == 0 {
go job(params...)
return
}
p.queue <- struct{}{}
go func() {
defer func() {
<-p.queue
}()
job(params...)
}()
}
-74
View File
@@ -1,74 +0,0 @@
package concurrency
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewPool(t *testing.T) {
type args struct {
size int
}
tests := []struct {
want *Pool
name string
args args
}{
{
name: "TestNewPool",
args: args{
size: 0,
},
want: &Pool{
jobs: 0,
queue: make(chan struct{}, 0),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewPool(tt.args.size)
assert.Equal(t, got.jobs, tt.want.jobs)
assert.Equal(t, cap(got.queue), cap(tt.want.queue))
})
}
}
func TestPool_Enqueue(t *testing.T) {
type fields struct {
queue chan struct{}
jobs int
}
type args struct {
job func(params ...any)
params []any
}
tests := []struct {
name string
fields fields
args args
}{
{
name: "TestPool_Enqueue",
fields: fields{
queue: make(chan struct{}, 0),
jobs: 0,
},
args: args{
job: func(params ...any) {
return
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Pool{
queue: tt.fields.queue,
jobs: tt.fields.jobs,
}
p.Enqueue(tt.args.job, tt.args.params...)
})
}
}
+10 -16
View File
@@ -1,16 +1,10 @@
package logger package logger
import ( import (
"encoding/json"
"reflect" "reflect"
"github.com/lukaszraczylo/go-simple-graphql/utils/helpers"
"github.com/lukaszraczylo/go-simple-graphql/utils/ordered_map"
jsoniter "github.com/json-iterator/go"
) )
var json = jsoniter.ConfigCompatibleWithStandardLibrary
const ( const (
reset = "\033[0m" reset = "\033[0m"
redBold = "\033[1;31m" redBold = "\033[1;31m"
@@ -34,25 +28,25 @@ type Config struct {
LogLevel LogLevel LogLevel LogLevel
} }
func Serialize(v any) (string, error) { func Serialize(v interface{}) ([]byte, error) {
marshal, err := json.Marshal(internalSerialize(v)) b, err := json.Marshal(internalSerialize(v))
if err != nil { if err != nil {
return "", err return nil, err
} }
return helpers.BytesToString(marshal), nil return b, nil
} }
func internalSerialize(v any) any { func internalSerialize(v interface{}) interface{} {
if v != nil { if v != nil {
switch reflect.TypeOf(v).Kind() { switch reflect.TypeOf(v).Kind() {
case reflect.Struct: case reflect.Struct:
r := ordered_map.New() r := make(map[string]interface{})
name := reflect.TypeOf(v).Name() name := reflect.TypeOf(v).Name()
if name != "" { if name != "" {
r.Set("_", reflect.TypeOf(v).String()) r["_"] = reflect.TypeOf(v).String()
} }
for i := 0; i < reflect.ValueOf(v).NumField(); i++ { for i := 0; i < reflect.ValueOf(v).NumField(); i++ {
r.Set(reflect.TypeOf(v).Field(i).Name, internalSerialize(reflect.ValueOf(v).Field(i).Interface())) r[reflect.TypeOf(v).Field(i).Name] = internalSerialize(reflect.ValueOf(v).Field(i).Interface())
} }
return r return r
case reflect.Ptr: case reflect.Ptr:
@@ -60,7 +54,7 @@ func internalSerialize(v any) any {
return internalSerialize(reflect.ValueOf(v).Elem().Interface()) return internalSerialize(reflect.ValueOf(v).Elem().Interface())
} }
case reflect.Slice: case reflect.Slice:
var r []any var r []interface{}
tmpSlice := reflect.ValueOf(v) tmpSlice := reflect.ValueOf(v)
for i := 0; i < tmpSlice.Len(); i++ { for i := 0; i < tmpSlice.Len(); i++ {
r = append(r, internalSerialize(tmpSlice.Index(i).Interface())) r = append(r, internalSerialize(tmpSlice.Index(i).Interface()))
+59 -2
View File
@@ -55,10 +55,67 @@ type customJsonEncoder struct {
} }
func (e *customJsonEncoder) Encode(v interface{}) error { func (e *customJsonEncoder) Encode(v interface{}) error {
data, err := json.Marshal(v) if v == nil {
e.buf.WriteString("null")
return nil
}
switch val := v.(type) {
case *OrderedMap:
data, err := json.Marshal(val.values)
if err != nil {
return err
}
e.buf.Write(data)
default:
data, err := json.Marshal(val)
if err != nil {
return err
}
e.buf.Write(data)
}
return nil
}
func (o OrderedMap) Keys() []string {
return o.keys
}
func (o OrderedMap) Values() []interface{} {
var values []interface{}
for _, k := range o.keys {
values = append(values, o.values[k])
}
return values
}
func (o *OrderedMap) Delete(key string) {
delete(o.values, key)
for i, k := range o.keys {
if k == key {
o.keys = append(o.keys[:i], o.keys[i+1:]...)
break
}
}
}
func (o OrderedMap) Exists(key string) bool {
_, exists := o.values[key]
return exists
}
func (o *OrderedMap) UnmarshalJSON(data []byte) error {
var values map[string]interface{}
err := json.Unmarshal(data, &values)
if err != nil { if err != nil {
return err return err
} }
e.buf.Write(data)
o.keys = []string{}
o.values = map[string]interface{}{}
for k, v := range values {
o.Set(k, v)
}
return nil return nil
} }