mirror of
https://github.com/lukaszraczylo/go-simple-graphql.git
synced 2026-10-12 00:45:31 +00:00
New minor release after complete rewrite.
Brings neat and testable codebase with minimum allocations in mind.
This commit is contained in:
@@ -32,18 +32,19 @@ Therefore, I present you the simple client to which you can copy & paste your gr
|
|||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
* `GRAPHQL_ENDPOINT` - Your GraphQL endpoint. Default: `http://127.0.0.1:9090/v1/graphql`
|
* `GRAPHQL_ENDPOINT` - Your GraphQL endpoint. Default: `http://127.0.0.1:9090/v1/graphql`
|
||||||
* `GRAPHQL_CACHE` - Should the query cache be enabled? Default: `false`
|
* `GRAPHQL_CACHE_ENABLED` - Should the query cache be enabled? Default: `false`
|
||||||
* `GRAPHQL_CACHE_TTL` - Cache TTL in seconds for SELECT type of queries. Default: `5`
|
* `GRAPHQL_CACHE_TTL` - Cache TTL in seconds for SELECT type of queries. Default: `5`
|
||||||
* `LOG_LEVEL` - Logging level. Default: `info`
|
* `GRAPHQL_OUTPUT` - Output format. Default: `byte`, available: `byte`, `string`
|
||||||
* `RETRIES_ENABLE` - Should retries be enabled? Default: `false`
|
* `LOG_LEVEL` - Logging level. Default: `info` available: `debug`, `info`, `warn`, `error`
|
||||||
|
<!-- * `RETRIES_ENABLE` - Should retries be enabled? Default: `false`
|
||||||
* `RETRIES_NUMBER` - Number of retries: Default: `1`
|
* `RETRIES_NUMBER` - Number of retries: Default: `1`
|
||||||
* `RETRIES_DELAY` - Delay in retries in milliseconds. Default: `250`
|
* `RETRIES_DELAY` - Delay in retries in milliseconds. Default: `250` -->
|
||||||
|
|
||||||
### Cache
|
### Cache
|
||||||
|
|
||||||
You have two options to enable the cache:
|
You have two options to enable the cache:
|
||||||
|
|
||||||
* Use `GRAPHQL_CACHE` environment variable which will enable the cache globally. It may be desired if you want to use the cache for all queries.
|
* Use `GRAPHQL_CACHE_ENABLED` environment variable which will enable the cache globally. It may be desired if you want to use the cache for all queries.
|
||||||
* Add `gqlcache: true` header for your query which will enable the cache for this query only with `GRAPHQL_CACHE_TTL` TTL.
|
* Add `gqlcache: true` header for your query which will enable the cache for this query only with `GRAPHQL_CACHE_TTL` TTL.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package gql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lukaszraczylo/go-simple-graphql/utils/concurrency"
|
||||||
|
"github.com/lukaszraczylo/go-simple-graphql/utils/logger"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/allegro/bigcache"
|
||||||
|
"github.com/gookit/goutil/envutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewConnection() *BaseClient {
|
||||||
|
|
||||||
|
b := &BaseClient{
|
||||||
|
client: &http.Client{},
|
||||||
|
concurrencyManager: concurrency.NewPool(-1),
|
||||||
|
MaxGoRoutines: -1,
|
||||||
|
LoggingLevel: logger.Warn,
|
||||||
|
LoggerColorful: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.LoggerWriter == nil {
|
||||||
|
b.LoggerWriter = log.New(os.Stdout, "", log.LstdFlags)
|
||||||
|
} else {
|
||||||
|
b.LoggerWriter = b.LoggerWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
b.endpoint = envutil.Getenv("GRAPHQL_ENDPOINT", "https://api.github.com/graphql")
|
||||||
|
b.responseType = envutil.Getenv("GRAPHQL_OUTPUT", "string")
|
||||||
|
b.validate = envutil.GetBool("GRAPHQL_VALIDATE", false)
|
||||||
|
b.cache.enabled = envutil.GetBool("GRAPHQL_CACHE_ENABLED", true)
|
||||||
|
b.cache.ttl = envutil.GetInt("GRAPHQL_CACHE_TTL", 60)
|
||||||
|
|
||||||
|
if b.cache.enabled {
|
||||||
|
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")
|
||||||
|
|
||||||
|
switch b_tmp_log_level {
|
||||||
|
case "silent":
|
||||||
|
b.LoggingLevel = logger.Silent
|
||||||
|
case "error":
|
||||||
|
b.LoggingLevel = logger.Error
|
||||||
|
case "warn":
|
||||||
|
b.LoggingLevel = logger.Warn
|
||||||
|
case "info":
|
||||||
|
b.LoggingLevel = logger.Info
|
||||||
|
case "debug":
|
||||||
|
b.LoggingLevel = logger.Debug
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Logger = NewLogger(b.LoggerWriter, logger.Config{
|
||||||
|
Colorful: b.LoggerColorful,
|
||||||
|
LogLevel: b.LoggingLevel,
|
||||||
|
})
|
||||||
|
|
||||||
|
var httpClient *http.Client
|
||||||
|
if strings.HasPrefix(b.endpoint, "http://") {
|
||||||
|
httpClient = &http.Client{
|
||||||
|
Transport: http.DefaultTransport,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tlsClientConfig := &tls.Config{}
|
||||||
|
if strings.HasPrefix(b.endpoint, "https://") {
|
||||||
|
tlsClientConfig.InsecureSkipVerify = true
|
||||||
|
}
|
||||||
|
http2Transport := &http2.Transport{
|
||||||
|
AllowHTTP: true,
|
||||||
|
TLSClientConfig: tlsClientConfig,
|
||||||
|
}
|
||||||
|
httpClient = &http.Client{
|
||||||
|
Transport: http2Transport,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.client = httpClient
|
||||||
|
|
||||||
|
b.Logger.Debug(b, "graphQL client initialized;", "endpoint", b.endpoint, "responseType", b.responseType, "validate", b.validate, "cache", b.cache.enabled, "cacheTTL", b.cache.ttl, "maxGoRoutines", b.MaxGoRoutines, "loggingLevel", b_tmp_log_level, "loggerColorful", b.LoggerColorful)
|
||||||
|
b.Logger.Info(b, "graphQL client initialized;")
|
||||||
|
return b
|
||||||
|
}
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
package gql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/lukaszraczylo/go-simple-graphql/utils/logger"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewConnection(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
want *BaseClient
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Test NewConnection",
|
||||||
|
want: &BaseClient{
|
||||||
|
endpoint: "https://api.github.com/graphql",
|
||||||
|
responseType: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// temporarily set GRAPHQL_ENDPOINT env variable to https://api.github.com/graphql
|
||||||
|
// and GRAPHQL_OUTPUT to string
|
||||||
|
current := os.Getenv("GRAPHQL_ENDPOINT")
|
||||||
|
defer os.Setenv("GRAPHQL_ENDPOINT", current)
|
||||||
|
os.Setenv("GRAPHQL_ENDPOINT", "https://api.github.com/graphql")
|
||||||
|
|
||||||
|
got := NewConnection()
|
||||||
|
assert.Equal(t, tt.want.endpoint, got.endpoint)
|
||||||
|
assert.Equal(t, tt.want.responseType, got.responseType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewConnectionLogLevels(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
want *BaseClient
|
||||||
|
name string
|
||||||
|
env string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Test NewConnection with log level set to debug",
|
||||||
|
env: "debug",
|
||||||
|
want: &BaseClient{
|
||||||
|
endpoint: "https://api.github.com/graphql",
|
||||||
|
responseType: "string",
|
||||||
|
LoggingLevel: logger.Debug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Test NewConnection with log level set to info",
|
||||||
|
env: "info",
|
||||||
|
want: &BaseClient{
|
||||||
|
endpoint: "https://api.github.com/graphql",
|
||||||
|
responseType: "string",
|
||||||
|
LoggingLevel: logger.Info,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Test NewConnection with log level set to warn",
|
||||||
|
env: "warn",
|
||||||
|
want: &BaseClient{
|
||||||
|
endpoint: "https://api.github.com/graphql",
|
||||||
|
responseType: "string",
|
||||||
|
LoggingLevel: logger.Warn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Test NewConnection with log level set to error",
|
||||||
|
env: "error",
|
||||||
|
want: &BaseClient{
|
||||||
|
endpoint: "https://api.github.com/graphql",
|
||||||
|
responseType: "string",
|
||||||
|
LoggingLevel: logger.Error,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// temporarily set GRAPHQL_ENDPOINT env variable to https://api.github.com/graphql
|
||||||
|
// and GRAPHQL_OUTPUT to string
|
||||||
|
current := os.Getenv("GRAPHQL_ENDPOINT")
|
||||||
|
defer os.Setenv("GRAPHQL_ENDPOINT", current)
|
||||||
|
os.Setenv("GRAPHQL_ENDPOINT", "https://api.github.com/graphql")
|
||||||
|
current_log_level := os.Getenv("LOG_LEVEL")
|
||||||
|
defer os.Setenv("LOG_LEVEL", current_log_level)
|
||||||
|
os.Setenv("LOG_LEVEL", tt.env)
|
||||||
|
|
||||||
|
got := NewConnection()
|
||||||
|
assert.Equal(t, tt.want.endpoint, got.endpoint)
|
||||||
|
assert.Equal(t, tt.want.responseType, got.responseType)
|
||||||
|
assert.Equal(t, tt.want.LoggingLevel, got.LoggingLevel)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package gql
|
||||||
|
|
||||||
|
func (c *BaseClient) cacheLookup(hash string) []byte {
|
||||||
|
if c.cache.client != nil {
|
||||||
|
g, err := c.cache.client.Get(hash)
|
||||||
|
if err == nil {
|
||||||
|
c.Logger.Debug(c, "Cache hit;", "hash", hash)
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
// if error is not nil it means that hash is not present in cache
|
||||||
|
c.Logger.Debug(c, "Cache miss;", "hash", hash)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -3,30 +3,26 @@ module github.com/lukaszraczylo/go-simple-graphql
|
|||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/allegro/bigcache/v3 v3.1.0
|
github.com/allegro/bigcache v1.2.1
|
||||||
github.com/avast/retry-go v3.0.0+incompatible
|
github.com/gookit/goutil v0.6.8
|
||||||
github.com/buger/jsonparser v1.1.1
|
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/lukaszraczylo/pandati v0.0.20
|
github.com/stretchr/testify v1.8.2
|
||||||
github.com/rs/zerolog v1.29.0
|
|
||||||
github.com/sethvargo/go-retry v0.2.4
|
|
||||||
github.com/stretchr/testify v1.7.1
|
|
||||||
golang.org/x/net v0.8.0
|
golang.org/x/net v0.8.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/imdario/mergo v0.3.15 // indirect
|
github.com/gookit/color v1.5.3 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/kr/pretty v0.3.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.18 // 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/tidwall/gjson v1.14.4 // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
github.com/wI2L/jsondiff v0.3.0 // indirect
|
|
||||||
golang.org/x/sys v0.6.0 // indirect
|
golang.org/x/sys v0.6.0 // indirect
|
||||||
|
golang.org/x/term v0.6.0 // indirect
|
||||||
golang.org/x/text v0.8.0 // indirect
|
golang.org/x/text v0.8.0 // 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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,67 +1,58 @@
|
|||||||
github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk=
|
github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc=
|
||||||
github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
|
github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
||||||
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
|
||||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
|
||||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
|
||||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
|
||||||
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
|
||||||
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/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
|
github.com/gookit/color v1.5.3 h1:twfIhZs4QLCtimkP7MOxlF3A0U/5cDPseRT9M/+2SCE=
|
||||||
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
github.com/gookit/color v1.5.3/go.mod h1:NUzwzeehUfl7GIb36pqId+UGmRfQcU/WiiyTTeNjHtE=
|
||||||
|
github.com/gookit/goutil v0.6.8 h1:B2XXSCGav5TXWtKRT9i/s/owOLXXB7sY6UsfqeSLroE=
|
||||||
|
github.com/gookit/goutil v0.6.8/go.mod h1:u+Isykc6RQcZ4GQzulsaGm+Famd97U5Tzp3aQyo+jyA=
|
||||||
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/lukaszraczylo/pandati v0.0.18 h1:6khtDY6QXkiyh4a3dzHtIzx/9UqUGfHjzKBCA0SIurI=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/lukaszraczylo/pandati v0.0.18/go.mod h1:Aqlc0KH+ssvrscYYbEKMv7n5cFwe+0MjsJbikNz1h60=
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
github.com/lukaszraczylo/pandati v0.0.20 h1:LqF7CS3z9Zi04KwEY8wd/YUpfWKSwi0nc7pagKQLXHc=
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/lukaszraczylo/pandati v0.0.20/go.mod h1:ZegK9ll9UebKV1uNMF73qdLb43G/O4CSOdPglu6NniQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
|
||||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
|
||||||
github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec=
|
|
||||||
github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw=
|
|
||||||
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 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4=
|
||||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||||
github.com/wI2L/jsondiff v0.3.0 h1:iTzQ9u/d86GE9RsBzVHX88f2EA1vQUboHwLhSQFc1s4=
|
|
||||||
github.com/wI2L/jsondiff v0.3.0/go.mod h1:y1IMzNNjlSsk3IUoJdRJO7VRBtzMvRgyo4Vu0LdHpTc=
|
|
||||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||||
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
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/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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
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.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=
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
// Package go-simple-graphql is a wrapper for GraphQL queries execution.
|
|
||||||
// It's sole purpose is to allow developer to execute queries without specific fields / types mapping
|
|
||||||
// which can be really confusing and even impossible whilst dealing with Hasura custom generated types.
|
|
||||||
//
|
|
||||||
// Library supports advanced error reporting on unsuccessful queries.
|
|
||||||
// Library also uses HTTP2 for communication with GraphQL API if it's supported by server
|
|
||||||
|
|
||||||
// Environment variables:
|
|
||||||
// GRAPHQL_ENDPOINT - GraphQL endpoint to use. Default: http://127.0.0.1:9090/v1/graphql
|
|
||||||
// LOG_LEVEL - Log level to use. Default: INFO
|
|
||||||
// Available log levels: DEBUG, INFO, WARN, ERROR, FATAL
|
|
||||||
|
|
||||||
package gql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
bigcache "github.com/allegro/bigcache/v3"
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
"github.com/lukaszraczylo/go-simple-graphql/pkg/logging"
|
|
||||||
retry "github.com/sethvargo/go-retry"
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
json = jsoniter.ConfigCompatibleWithStandardLibrary
|
|
||||||
)
|
|
||||||
|
|
||||||
type GraphQL struct {
|
|
||||||
BackoffSetup retry.Backoff
|
|
||||||
HttpClient *http.Client
|
|
||||||
Log *logging.LogConfig
|
|
||||||
CacheStore *bigcache.BigCache
|
|
||||||
Endpoint string
|
|
||||||
RetriesNumber int
|
|
||||||
RetriesDelay time.Duration
|
|
||||||
Cache bool
|
|
||||||
RetriesEnable bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func pickGraphqlEndpoint() (graphqlEndpoint string) {
|
|
||||||
value, present := os.LookupEnv("GRAPHQL_ENDPOINT")
|
|
||||||
if present && value != "" {
|
|
||||||
graphqlEndpoint = value
|
|
||||||
} else {
|
|
||||||
graphqlEndpoint = "http://127.0.0.1:9090/v1/graphql"
|
|
||||||
fmt.Println("Setting default endpoint", graphqlEndpoint)
|
|
||||||
}
|
|
||||||
return graphqlEndpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
func setCacheTTL() int {
|
|
||||||
value, present := os.LookupEnv("GRAPHQL_CACHE_TTL")
|
|
||||||
if present && value != "" {
|
|
||||||
i, err := strconv.Atoi(value)
|
|
||||||
if err != nil {
|
|
||||||
panic("Invalid value for query cache ttl")
|
|
||||||
}
|
|
||||||
return int(i)
|
|
||||||
} else {
|
|
||||||
return 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setCacheEnabled() bool {
|
|
||||||
value, present := os.LookupEnv("GRAPHQL_CACHE")
|
|
||||||
if present && value != "" {
|
|
||||||
b, err := strconv.ParseBool(value)
|
|
||||||
if err != nil {
|
|
||||||
panic("Invalid value for query cache")
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConnection() *GraphQL {
|
|
||||||
|
|
||||||
endpoint := pickGraphqlEndpoint()
|
|
||||||
var httpClient *http.Client
|
|
||||||
if strings.HasPrefix(endpoint, "http://") {
|
|
||||||
httpClient = &http.Client{
|
|
||||||
Transport: http.DefaultTransport,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tlsClientConfig := &tls.Config{}
|
|
||||||
if strings.HasPrefix(endpoint, "https://") {
|
|
||||||
tlsClientConfig.InsecureSkipVerify = true
|
|
||||||
}
|
|
||||||
http2Transport := &http2.Transport{
|
|
||||||
AllowHTTP: true,
|
|
||||||
TLSClientConfig: tlsClientConfig,
|
|
||||||
}
|
|
||||||
httpClient = &http.Client{
|
|
||||||
Transport: http2Transport,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
g := GraphQL{
|
|
||||||
Endpoint: endpoint,
|
|
||||||
HttpClient: httpClient,
|
|
||||||
Log: logging.NewLogger(),
|
|
||||||
Cache: setCacheEnabled(),
|
|
||||||
CacheStore: setupCache(),
|
|
||||||
RetriesEnable: false,
|
|
||||||
RetriesNumber: 1,
|
|
||||||
RetriesDelay: 250,
|
|
||||||
BackoffSetup: nil,
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
retriesEnable, retriesEnableExists := os.LookupEnv("RETRIES_ENABLE")
|
|
||||||
if retriesEnableExists {
|
|
||||||
g.RetriesEnable, err = strconv.ParseBool(retriesEnable)
|
|
||||||
if err != nil {
|
|
||||||
panic("Invalid value for RETRIES_ENABLE")
|
|
||||||
}
|
|
||||||
retriesNumber, retriesNumberExists := os.LookupEnv("RETRIES_NUMBER")
|
|
||||||
if retriesNumberExists {
|
|
||||||
g.RetriesNumber, err = strconv.Atoi(retriesNumber)
|
|
||||||
if err != nil {
|
|
||||||
panic("Invalid value for RETRIES_NUMBER")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
retriesDelay, retriesDelayExists := os.LookupEnv("RETRIES_DELAY")
|
|
||||||
if retriesDelayExists {
|
|
||||||
retDel, err := strconv.Atoi(retriesDelay)
|
|
||||||
if err != nil {
|
|
||||||
panic("Invalid value for RETRIES_DELAY")
|
|
||||||
}
|
|
||||||
g.RetriesDelay = time.Duration(retDel) * time.Millisecond
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g.BackoffSetup = retry.NewExponential(g.RetriesDelay)
|
|
||||||
g.BackoffSetup = retry.WithMaxRetries(uint64(g.RetriesNumber), g.BackoffSetup)
|
|
||||||
|
|
||||||
return &g
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupCache() *bigcache.BigCache {
|
|
||||||
cache, err := bigcache.NewBigCache(bigcache.DefaultConfig(time.Duration(setCacheTTL()) * time.Second))
|
|
||||||
if err != nil {
|
|
||||||
panic("Error creating cache: " + err.Error())
|
|
||||||
}
|
|
||||||
return cache
|
|
||||||
}
|
|
||||||
-66
@@ -1,66 +0,0 @@
|
|||||||
package gql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
func mockGraphQLServerResponses(responseQuery string) (*httptest.Server, string) {
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
rw.Write([]byte(responseQuery))
|
|
||||||
}))
|
|
||||||
serverURL := server.URL + "/v1/graphql"
|
|
||||||
return server, serverURL
|
|
||||||
}
|
|
||||||
|
|
||||||
type TestSuite struct {
|
|
||||||
suite.Suite
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *TestSuite) SetupTest() {
|
|
||||||
os.Unsetenv("GRAPHQL_ENDPOINT")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSuiteRun(t *testing.T) {
|
|
||||||
suite.Run(t, new(TestSuite))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *TestSuite) TestNewConnection() {
|
|
||||||
type args struct {
|
|
||||||
endpoint string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
want *GraphQL
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "New connection: Env variable endpoint",
|
|
||||||
args: args{
|
|
||||||
endpoint: "http://localhost:8080/graphql",
|
|
||||||
},
|
|
||||||
want: &GraphQL{
|
|
||||||
Endpoint: "http://localhost:8080/graphql",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "New connection: No env variable endpoint",
|
|
||||||
args: args{},
|
|
||||||
want: &GraphQL{
|
|
||||||
Endpoint: "http://127.0.0.1:9090/v1/graphql",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
suite.T().Run(tt.name, func(t *testing.T) {
|
|
||||||
os.Setenv("GRAPHQL_ENDPOINT", tt.args.endpoint)
|
|
||||||
got := NewConnection()
|
|
||||||
assert.Equal(t, tt.want.Endpoint, got.Endpoint)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package gql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lukaszraczylo/go-simple-graphql/utils/logger"
|
||||||
|
|
||||||
|
"github.com/gookit/goutil/timex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewLogger(writer Writer, config logger.Config) Logger {
|
||||||
|
colorfulPrint := "%s[%s]%s %%s %%s"
|
||||||
|
closeColor, debugColor, infoColor, warnColor, errorColor := "\033[0m", "\033[1;34m", "\033[1;32m", "\033[1;33m", "\033[1;31m"
|
||||||
|
if !config.Colorful {
|
||||||
|
closeColor, debugColor, infoColor, warnColor, errorColor = "", "", "", "", ""
|
||||||
|
}
|
||||||
|
debugStr := fmt.Sprintf(colorfulPrint, debugColor, "DEBUG", closeColor)
|
||||||
|
infoStr := fmt.Sprintf(colorfulPrint, infoColor, "INFO", closeColor)
|
||||||
|
warnStr := fmt.Sprintf(colorfulPrint, warnColor, "WARN", closeColor)
|
||||||
|
errStr := fmt.Sprintf(colorfulPrint, errorColor, "ERROR", closeColor)
|
||||||
|
|
||||||
|
return &logging{
|
||||||
|
Writer: writer,
|
||||||
|
Config: config,
|
||||||
|
infoStr: infoStr,
|
||||||
|
warnStr: warnStr,
|
||||||
|
errStr: errStr,
|
||||||
|
debugStr: debugStr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDate() string {
|
||||||
|
tx := time.Now()
|
||||||
|
return timex.DateFormat(tx, "Y-m-d H:I:S")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l logging) Debug(client *BaseClient, msg string, data ...any) {
|
||||||
|
if l.LogLevel >= logger.Debug {
|
||||||
|
t := setDate()
|
||||||
|
msg, data = censorSensitiveInfo(client, msg, data...)
|
||||||
|
if client != nil {
|
||||||
|
l.Printf(l.debugStr, fmt.Sprintf("[%s] %s", t, msg), sprint(data...))
|
||||||
|
} else {
|
||||||
|
l.Printf(l.debugStr, msg, sprint(data...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l logging) Info(client *BaseClient, msg string, data ...any) {
|
||||||
|
if l.LogLevel >= logger.Info {
|
||||||
|
t := setDate()
|
||||||
|
msg, data = censorSensitiveInfo(client, msg, data...)
|
||||||
|
if client != nil {
|
||||||
|
l.Printf(l.infoStr, fmt.Sprintf("[%s] %s", t, msg), sprint(data...))
|
||||||
|
} else {
|
||||||
|
l.Printf(l.infoStr, msg, sprint(data...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l logging) Warn(client *BaseClient, msg string, data ...any) {
|
||||||
|
if l.LogLevel >= logger.Warn {
|
||||||
|
t := setDate()
|
||||||
|
msg, data = censorSensitiveInfo(client, msg, data...)
|
||||||
|
if client != nil {
|
||||||
|
l.Printf(l.warnStr, fmt.Sprintf("[%s] %s", t, msg), sprint(data...))
|
||||||
|
} else {
|
||||||
|
l.Printf(l.warnStr, msg, sprint(data...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l logging) Error(client *BaseClient, msg string, data ...any) {
|
||||||
|
if l.LogLevel >= logger.Error {
|
||||||
|
t := setDate()
|
||||||
|
msg, data = censorSensitiveInfo(client, msg, data...)
|
||||||
|
if client != nil {
|
||||||
|
l.Printf(l.errStr, fmt.Sprintf("[%s] %s", t, msg), sprint(data...))
|
||||||
|
} else {
|
||||||
|
l.Printf(l.errStr, msg, sprint(data...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Debug(*BaseClient, string, ...any)
|
||||||
|
Info(*BaseClient, string, ...any)
|
||||||
|
Warn(*BaseClient, string, ...any)
|
||||||
|
Error(*BaseClient, string, ...any)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Writer interface {
|
||||||
|
Printf(string, ...any)
|
||||||
|
}
|
||||||
|
|
||||||
|
type logging struct {
|
||||||
|
Writer
|
||||||
|
debugStr string
|
||||||
|
infoStr string
|
||||||
|
warnStr string
|
||||||
|
errStr string
|
||||||
|
logger.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func censorSensitiveInfo(client *BaseClient, a string, b ...any) (string, []any) {
|
||||||
|
// if client == nil || client.Token == "" {
|
||||||
|
// return a, b
|
||||||
|
// }
|
||||||
|
// tokenFixed := strings.Split(client.Token, ":")[1]
|
||||||
|
// for t := range b {
|
||||||
|
// if b[t] != nil {
|
||||||
|
// rV := reflect.TypeOf(b[t]).Kind()
|
||||||
|
// if rV == reflect.String {
|
||||||
|
// b[t] = strings.ReplaceAll(b[t].(string), tokenFixed, strings.Repeat("X", len(tokenFixed)))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// a = strings.ReplaceAll(a, tokenFixed, strings.Repeat("X", len(tokenFixed)))
|
||||||
|
// return a, b
|
||||||
|
return a, b
|
||||||
|
}
|
||||||
|
|
||||||
|
func sprint(a ...any) string {
|
||||||
|
for t := range a {
|
||||||
|
if a[t] != nil {
|
||||||
|
rV := reflect.TypeOf(a[t]).Kind()
|
||||||
|
if rV == reflect.Ptr {
|
||||||
|
a[t] = reflect.ValueOf(a[t]).Elem().Interface()
|
||||||
|
}
|
||||||
|
rV = reflect.TypeOf(a[t]).Kind()
|
||||||
|
if rV == reflect.Struct || rV == reflect.Map || rV == reflect.Slice {
|
||||||
|
a[t], _ = logger.Serialize(a[t])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r := fmt.Sprintln(a...)
|
||||||
|
r = r[:len(r)-1]
|
||||||
|
return r
|
||||||
|
}
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package logging
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
stdlog "log"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/lukaszraczylo/pandati"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LogConfig struct {
|
|
||||||
DefaultOutput *os.File
|
|
||||||
Logger zerolog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLogger() *LogConfig {
|
|
||||||
log := new(LogConfig)
|
|
||||||
switch dll := os.Getenv("LOG_LEVEL"); dll {
|
|
||||||
case "debug":
|
|
||||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
|
||||||
case "warn":
|
|
||||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
|
||||||
case "error":
|
|
||||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
|
||||||
default:
|
|
||||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
|
||||||
}
|
|
||||||
zerolog.TimeFieldFormat = time.RFC3339
|
|
||||||
zerolog.MessageFieldName = "short_message"
|
|
||||||
zerolog.TimestampFieldName = "timestamp"
|
|
||||||
zerolog.LevelFatalValue = "critical"
|
|
||||||
log.Logger = log.Logger.With().Timestamp().Logger()
|
|
||||||
stdlog.SetFlags(0)
|
|
||||||
stdlog.SetOutput(log.Logger)
|
|
||||||
return log
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogConfig) Critical(message string, v ...map[string]interface{}) {
|
|
||||||
w := l.Logger.Output(os.Stderr)
|
|
||||||
log(w.Fatal(), message, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogConfig) Error(message string, v ...map[string]interface{}) {
|
|
||||||
w := l.Logger.Output(os.Stderr)
|
|
||||||
log(w.Error(), message, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogConfig) Warning(message string, v ...map[string]interface{}) {
|
|
||||||
w := l.Logger.Output(os.Stdout)
|
|
||||||
log(w.Warn(), message, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogConfig) Info(message string, v ...map[string]interface{}) {
|
|
||||||
w := l.Logger.Output(os.Stdout)
|
|
||||||
log(w.Info(), message, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LogConfig) Debug(message string, v ...map[string]interface{}) {
|
|
||||||
w := l.Logger.Output(os.Stdout)
|
|
||||||
log(w.Debug(), message, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func log(contextLog *zerolog.Event, message string, v ...map[string]interface{}) {
|
|
||||||
if len(v) > 0 {
|
|
||||||
for _, m := range v {
|
|
||||||
for k, v := range m {
|
|
||||||
if !pandati.IsZero(v) && !pandati.IsZero(k) {
|
|
||||||
contextLog.Str(k, fmt.Sprintf("%v", v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// contextLog.Str("_stack_trace", pandati.Trace())
|
|
||||||
contextLog.Msg(message)
|
|
||||||
}
|
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
package logging
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/buger/jsonparser"
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LoggingTestSuite struct {
|
|
||||||
suite.Suite
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
testsLogger *LogConfig
|
|
||||||
)
|
|
||||||
|
|
||||||
type stdoutCapture struct {
|
|
||||||
oldStdout *os.File
|
|
||||||
readPipe *os.File
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sc *stdoutCapture) StartCapture() {
|
|
||||||
sc.oldStdout = os.Stdout
|
|
||||||
sc.readPipe, os.Stdout, _ = os.Pipe()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sc *stdoutCapture) StopCapture() (string, error) {
|
|
||||||
if sc.oldStdout == nil || sc.readPipe == nil {
|
|
||||||
return "", errors.New("StartCapture not called before StopCapture on Stdout")
|
|
||||||
}
|
|
||||||
os.Stdout.Close()
|
|
||||||
os.Stdout = sc.oldStdout
|
|
||||||
bytes, err := ioutil.ReadAll(sc.readPipe)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(bytes), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type stderrCapture struct {
|
|
||||||
oldStderr *os.File
|
|
||||||
readPipe *os.File
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sc *stderrCapture) StartCapture() {
|
|
||||||
sc.oldStderr = os.Stderr
|
|
||||||
sc.readPipe, os.Stderr, _ = os.Pipe()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sc *stderrCapture) StopCapture() (string, error) {
|
|
||||||
if sc.oldStderr == nil || sc.readPipe == nil {
|
|
||||||
return "", errors.New("StartCapture not called before StopCapture on Stderr")
|
|
||||||
}
|
|
||||||
os.Stderr.Close()
|
|
||||||
os.Stderr = sc.oldStderr
|
|
||||||
bytes, err := ioutil.ReadAll(sc.readPipe)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(bytes), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *LoggingTestSuite) SetupTest() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggingTestSuite(t *testing.T) {
|
|
||||||
suite.Run(t, new(LoggingTestSuite))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *LoggingTestSuite) TestLogConfig_AllHandlers() {
|
|
||||||
type args struct {
|
|
||||||
message string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
wantLevel string
|
|
||||||
wantMessage string
|
|
||||||
envMinLogLevel string
|
|
||||||
loggerType string
|
|
||||||
stdOutExpect bool
|
|
||||||
stdErrExpect bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Test log: Error",
|
|
||||||
loggerType: "Error",
|
|
||||||
args: args{
|
|
||||||
message: "This is a error message",
|
|
||||||
},
|
|
||||||
wantLevel: "error",
|
|
||||||
wantMessage: "This is a error message",
|
|
||||||
stdErrExpect: true,
|
|
||||||
stdOutExpect: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test log: Warning",
|
|
||||||
loggerType: "Warning",
|
|
||||||
args: args{
|
|
||||||
message: "This is a warning message",
|
|
||||||
},
|
|
||||||
wantLevel: "warn",
|
|
||||||
wantMessage: "This is a warning message",
|
|
||||||
stdErrExpect: false,
|
|
||||||
stdOutExpect: true,
|
|
||||||
envMinLogLevel: "info",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test log: Warning | Min level: Debug",
|
|
||||||
loggerType: "Warning",
|
|
||||||
args: args{
|
|
||||||
message: "This is a warning message",
|
|
||||||
},
|
|
||||||
wantLevel: "warn",
|
|
||||||
wantMessage: "This is a warning message",
|
|
||||||
stdErrExpect: false,
|
|
||||||
stdOutExpect: true,
|
|
||||||
envMinLogLevel: "debug",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test log: Info",
|
|
||||||
loggerType: "Info",
|
|
||||||
args: args{
|
|
||||||
message: "This is a info message",
|
|
||||||
},
|
|
||||||
wantLevel: "info",
|
|
||||||
wantMessage: "This is a info message",
|
|
||||||
stdErrExpect: false,
|
|
||||||
stdOutExpect: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test log: Info | Min level: Warn",
|
|
||||||
loggerType: "Info",
|
|
||||||
args: args{
|
|
||||||
message: "This is a info message",
|
|
||||||
},
|
|
||||||
wantLevel: "",
|
|
||||||
wantMessage: "",
|
|
||||||
stdErrExpect: false,
|
|
||||||
stdOutExpect: false,
|
|
||||||
envMinLogLevel: "warn",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test log: Warning | Min level: Warn",
|
|
||||||
loggerType: "Warning",
|
|
||||||
args: args{
|
|
||||||
message: "This is a warning message",
|
|
||||||
},
|
|
||||||
wantLevel: "warn",
|
|
||||||
wantMessage: "This is a warning message",
|
|
||||||
stdErrExpect: false,
|
|
||||||
stdOutExpect: true,
|
|
||||||
envMinLogLevel: "warn",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test log: Warning | Min level: Error",
|
|
||||||
loggerType: "Warning",
|
|
||||||
args: args{
|
|
||||||
message: "This is an error message",
|
|
||||||
},
|
|
||||||
wantLevel: "",
|
|
||||||
wantMessage: "",
|
|
||||||
stdErrExpect: false,
|
|
||||||
stdOutExpect: false,
|
|
||||||
envMinLogLevel: "error",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test log: Debug | Min level: Debug",
|
|
||||||
loggerType: "Debug",
|
|
||||||
args: args{
|
|
||||||
message: "This is a debug message",
|
|
||||||
},
|
|
||||||
wantLevel: "debug",
|
|
||||||
wantMessage: "This is a debug message",
|
|
||||||
stdErrExpect: false,
|
|
||||||
stdOutExpect: true,
|
|
||||||
envMinLogLevel: "debug",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
suite.T().Run(tt.name, func(t *testing.T) {
|
|
||||||
if tt.envMinLogLevel != "" {
|
|
||||||
os.Setenv("LOG_LEVEL", tt.envMinLogLevel)
|
|
||||||
defer os.Unsetenv("LOG_LEVEL")
|
|
||||||
}
|
|
||||||
testsLogger = NewLogger()
|
|
||||||
|
|
||||||
captureStdout := stdoutCapture{}
|
|
||||||
captureStdout.StartCapture()
|
|
||||||
captureStderr := stderrCapture{}
|
|
||||||
captureStderr.StartCapture()
|
|
||||||
|
|
||||||
reflect.ValueOf(testsLogger).MethodByName(tt.loggerType).Call([]reflect.Value{reflect.ValueOf(tt.args.message)})
|
|
||||||
|
|
||||||
stdoutOut, err := captureStdout.StopCapture()
|
|
||||||
if err != nil {
|
|
||||||
suite.T().Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
stderrOut, err := captureStderr.StopCapture()
|
|
||||||
if err != nil {
|
|
||||||
suite.T().Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.stdErrExpect && !tt.stdOutExpect {
|
|
||||||
gotLvl, gotMsg, err := getResponseValues(stderrOut, "short_message")
|
|
||||||
suite.NoError(err, "Failed in [STDERR]: "+tt.name)
|
|
||||||
suite.Equal(tt.wantLevel, gotLvl, "Failed in [STDERR]: "+tt.name)
|
|
||||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDERR]: "+tt.name)
|
|
||||||
suite.Equal("", stdoutOut, "Failed in [STDERR]: "+tt.name)
|
|
||||||
}
|
|
||||||
if tt.stdOutExpect && !tt.stdErrExpect {
|
|
||||||
gotLvl, gotMsg, err := getResponseValues(stdoutOut, "short_message")
|
|
||||||
suite.NoError(err, "Failed in [STDOUT]: "+tt.name)
|
|
||||||
suite.Equal(tt.wantLevel, gotLvl, "Failed in [STDOUT]: "+tt.name)
|
|
||||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDOUT]: "+tt.name)
|
|
||||||
suite.Equal("", stderrOut, "Failed in [STDOUT]: "+tt.name)
|
|
||||||
}
|
|
||||||
if !tt.stdErrExpect && !tt.stdOutExpect {
|
|
||||||
suite.Equal("", stderrOut, "Failed in [NEITHER]: "+tt.name)
|
|
||||||
suite.Equal("", stdoutOut, "Failed in [NEITHER]: "+tt.name)
|
|
||||||
}
|
|
||||||
os.Unsetenv("LOG_LEVEL")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *LoggingTestSuite) TestFullMessage() {
|
|
||||||
type args struct {
|
|
||||||
extraFields map[string]interface{}
|
|
||||||
message string
|
|
||||||
}
|
|
||||||
extraFields := make(map[string]interface{})
|
|
||||||
extraFields["_full_message"] = "full message"
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
args args
|
|
||||||
name string
|
|
||||||
wantLevel string
|
|
||||||
wantMessage string
|
|
||||||
envMinLogLevel string
|
|
||||||
loggerType string
|
|
||||||
stdOutExpect bool
|
|
||||||
stdErrExpect bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Test log: Error",
|
|
||||||
loggerType: "Error",
|
|
||||||
args: args{
|
|
||||||
message: "This is a error message",
|
|
||||||
extraFields: extraFields,
|
|
||||||
},
|
|
||||||
wantLevel: "error",
|
|
||||||
wantMessage: extraFields["_full_message"].(string),
|
|
||||||
stdErrExpect: true,
|
|
||||||
stdOutExpect: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test log: Info",
|
|
||||||
loggerType: "Info",
|
|
||||||
args: args{
|
|
||||||
message: "This is a info message",
|
|
||||||
extraFields: extraFields,
|
|
||||||
},
|
|
||||||
wantMessage: extraFields["_full_message"].(string),
|
|
||||||
stdErrExpect: false,
|
|
||||||
stdOutExpect: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
suite.T().Run(tt.name, func(t *testing.T) {
|
|
||||||
if tt.envMinLogLevel != "" {
|
|
||||||
os.Setenv("LOG_LEVEL", tt.envMinLogLevel)
|
|
||||||
defer os.Unsetenv("LOG_LEVEL")
|
|
||||||
}
|
|
||||||
testsLogger = NewLogger()
|
|
||||||
|
|
||||||
captureStdout := stdoutCapture{}
|
|
||||||
captureStdout.StartCapture()
|
|
||||||
captureStderr := stderrCapture{}
|
|
||||||
captureStderr.StartCapture()
|
|
||||||
|
|
||||||
reflect.ValueOf(testsLogger).MethodByName(tt.loggerType).Call([]reflect.Value{
|
|
||||||
reflect.ValueOf(tt.args.message),
|
|
||||||
reflect.ValueOf(tt.args.extraFields),
|
|
||||||
})
|
|
||||||
|
|
||||||
stdoutOut, err := captureStdout.StopCapture()
|
|
||||||
if err != nil {
|
|
||||||
suite.T().Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
stderrOut, err := captureStderr.StopCapture()
|
|
||||||
if err != nil {
|
|
||||||
suite.T().Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.stdErrExpect && !tt.stdOutExpect {
|
|
||||||
_, gotMsg, err := getResponseValues(stderrOut, "_full_message")
|
|
||||||
suite.NoError(err, "Failed in [STDERR]: "+tt.name)
|
|
||||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDERR]: "+tt.name)
|
|
||||||
}
|
|
||||||
if tt.stdOutExpect && !tt.stdErrExpect {
|
|
||||||
_, gotMsg, err := getResponseValues(stdoutOut, "_full_message")
|
|
||||||
suite.NoError(err, "Failed in [STDOUT]: "+tt.name)
|
|
||||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDOUT]: "+tt.name)
|
|
||||||
}
|
|
||||||
os.Unsetenv("LOG_LEVEL")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_getResponseValues(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
sourceJson string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
wantGotLvl string
|
|
||||||
wantGotMsg string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Test with json",
|
|
||||||
args: args{
|
|
||||||
sourceJson: `{"level": "debug", "short_message": "hello world"`,
|
|
||||||
},
|
|
||||||
wantGotLvl: "debug",
|
|
||||||
wantGotMsg: "hello world",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test with json, wrong message field",
|
|
||||||
args: args{
|
|
||||||
sourceJson: `{"level": "debug", "message": "hello world"`,
|
|
||||||
},
|
|
||||||
wantGotLvl: "debug",
|
|
||||||
wantGotMsg: "",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
gotGotLvl, gotGotMsg, err := getResponseValues(tt.args.sourceJson, "short_message")
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("getResponseValues() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if gotGotLvl != tt.wantGotLvl {
|
|
||||||
t.Errorf("getResponseValues() gotGotLvl = %v, want %v", gotGotLvl, tt.wantGotLvl)
|
|
||||||
}
|
|
||||||
if gotGotMsg != tt.wantGotMsg {
|
|
||||||
t.Errorf("getResponseValues() gotGotMsg = %v, want %v", gotGotMsg, tt.wantGotMsg)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getResponseValues(sourceJson string, key string) (gotLvl, gotMsg string, err error) {
|
|
||||||
gotLvl, err = jsonparser.GetString([]byte(sourceJson), "level")
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
gotMsg, err = jsonparser.GetString([]byte(sourceJson), key)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,141 +1,117 @@
|
|||||||
package gql
|
package gql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"context"
|
||||||
"crypto/md5"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
retry "github.com/avast/retry-go"
|
"github.com/lukaszraczylo/go-simple-graphql/utils/helpers"
|
||||||
"github.com/lukaszraczylo/pandati"
|
|
||||||
|
"github.com/gookit/goutil/strutil"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
type requestBase struct {
|
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
Variables interface{} `json:"variables"`
|
|
||||||
Query string `json:"query"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type queryResults struct {
|
func (c *BaseClient) convertToJson(v any) []byte {
|
||||||
Data interface{} `json:"data"`
|
json, err := json.Marshal(v)
|
||||||
Errors []struct {
|
|
||||||
Message interface{} `json:"message"`
|
|
||||||
} `json:"errors"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GraphQL) queryBuilder(queryContent string, queryVariables interface{}) ([]byte, error) {
|
|
||||||
var qb = &requestBase{
|
|
||||||
Query: queryContent,
|
|
||||||
Variables: queryVariables,
|
|
||||||
}
|
|
||||||
|
|
||||||
j2, err := json.Marshal(qb)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.Log.Critical("Unable to marshal the query", map[string]interface{}{"_error": err.Error(), "_query": queryContent, "_variables": queryVariables})
|
c.Logger.Error(c, "Can't convert to json;", "error", err.Error())
|
||||||
return []byte{}, err
|
return nil
|
||||||
}
|
}
|
||||||
return j2, err
|
return json
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GraphQL) Query(queryContent string, queryVariables interface{}, queryHeaders map[string]interface{}) (responseContent string, err error) {
|
func (c *BaseClient) NewQuery(q ...any) *Query {
|
||||||
g.Log.Debug("Query details", map[string]interface{}{"_query": queryContent, "_variables": queryVariables})
|
query := &Query{}
|
||||||
query, err := g.queryBuilder(queryContent, queryVariables)
|
|
||||||
if err != nil {
|
|
||||||
g.Log.Error("Unable to build the query", map[string]interface{}{"_error": err.Error()})
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var body []byte
|
|
||||||
var queryResult queryResults
|
|
||||||
var queryHash string
|
|
||||||
|
|
||||||
cacheEnabled := queryHeaders["gqlcache"] == true
|
if len(q) > 0 {
|
||||||
// If header 'gqlcache' is set to true, we will enforce the cache for the query
|
for _, v := range q {
|
||||||
if cacheEnabled || g.Cache {
|
switch val := v.(type) {
|
||||||
g.Log.Debug("Forced cache for query", map[string]interface{}{"_query": queryContent, "_headers": queryHeaders})
|
case string:
|
||||||
g.Cache = true
|
query.query = val
|
||||||
queryHash = fmt.Sprintf("%x", md5.Sum(query))
|
case map[string]interface{}:
|
||||||
|
query.variables = val
|
||||||
g.Log.Debug("Checking the cache for the query", map[string]interface{}{"_query": queryHash})
|
case context.Context:
|
||||||
if entry, entryInfo, err := g.CacheStore.GetWithInfo(queryHash); err == nil {
|
query.context = val
|
||||||
g.Log.Debug("Found the query in the cache", map[string]interface{}{"_query": queryHash})
|
|
||||||
if pandati.IsZero(entryInfo.EntryStatus) {
|
|
||||||
return pandati.BytesToString(entry), nil
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
g.Log.Debug("Unable to find the query in the cache", map[string]interface{}{"_query": queryHash, "_error": err.Error()})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query.compiledQuery = c.convertToJson(request{
|
||||||
|
Query: query.query,
|
||||||
|
Variables: query.variables,
|
||||||
|
})
|
||||||
|
|
||||||
httpRequest, err := http.NewRequest("POST", g.Endpoint, bytes.NewBuffer(query))
|
c.Logger.Debug(c, "Clearing previously prepared variables and query")
|
||||||
// httpRequest.Header.Set("Content-Type", "application/json")
|
query.variables = nil
|
||||||
if err != nil {
|
|
||||||
g.Log.Error("Unable to create the request", map[string]interface{}{"_error": err.Error()})
|
if query.context == nil {
|
||||||
return "", err
|
query.context = context.Background()
|
||||||
}
|
}
|
||||||
|
|
||||||
for header, value := range queryHeaders {
|
if c.validate {
|
||||||
httpRequest.Header.Add(header, fmt.Sprintf("%v", value))
|
c.Logger.Warn(c, "Validating query is not active")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query.query = ""
|
||||||
|
c.Logger.Debug(c, "Query prepared;", "query", helpers.BytesToString(query.compiledQuery))
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
var httpResponse *http.Response
|
// Query is a function that sends a query to the server
|
||||||
|
// It takes 3 arguments:
|
||||||
|
// 1. queryContent: the query string
|
||||||
|
// 2. queryVariables: the variables for the query
|
||||||
|
// 3. queryHeaders: the headers for the query
|
||||||
|
// It looks a bit weird because of the backward compatibility
|
||||||
|
|
||||||
err = retry.Do(
|
func (c *BaseClient) Query(queryContent string, queryVariables interface{}, queryHeaders map[string]interface{}) (any, error) {
|
||||||
func() error {
|
query := c.NewQuery(queryContent, queryVariables)
|
||||||
g.Log.Debug("Sending the query")
|
|
||||||
httpResponse, err = g.HttpClient.Do(httpRequest)
|
|
||||||
|
|
||||||
if err != nil {
|
var queryHash string
|
||||||
g.Log.Debug("Unable to send the query RETRY", map[string]interface{}{"_error": err.Error()})
|
var cachedResponse []byte
|
||||||
return err
|
|
||||||
|
if c.cache.enabled {
|
||||||
|
queryHash = strutil.Md5(query.compiledQuery)
|
||||||
|
c.Logger.Debug(c, "Hash calculated;", "hash:", queryHash)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
defer httpResponse.Body.Close()
|
|
||||||
|
|
||||||
if httpResponse.StatusCode <= 200 && httpResponse.StatusCode >= 204 {
|
response, err := c.executeQuery(query.compiledQuery, queryHeaders)
|
||||||
return err
|
if err != nil {
|
||||||
|
c.Logger.Error(c, "Error while executing query;", "error", err.Error())
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err = ioutil.ReadAll(httpResponse.Body)
|
jsonData, err := json.Marshal(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.Log.Debug("Unable to read the response", map[string]interface{}{"_error": err.Error()})
|
c.Logger.Error(c, "Error while converting to json;", "error", err.Error())
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(body, &queryResult)
|
if c.cache.enabled {
|
||||||
if err != nil {
|
c.cache.client.Set(queryHash, jsonData)
|
||||||
g.Log.Debug("Unable to unmarshal the query", map[string]interface{}{"_error": err.Error()})
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return c.decodeResponse(jsonData), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BaseClient) decodeResponse(jsonData []byte) any {
|
||||||
|
switch c.responseType {
|
||||||
|
case "string":
|
||||||
|
return helpers.BytesToString(jsonData)
|
||||||
|
case "byte":
|
||||||
|
return jsonData
|
||||||
|
default:
|
||||||
|
c.Logger.Error(c, "Unknown response type", "response", c.responseType)
|
||||||
return nil
|
return nil
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if !pandati.IsZero(queryResult.Errors) {
|
|
||||||
g.Log.Error("Query returned error", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_error": fmt.Sprintf("%v", queryResult.Errors), "_response_code": httpResponse.StatusCode})
|
|
||||||
return "", fmt.Errorf("%v", queryResult.Errors)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if pandati.IsZero(queryResult.Data) {
|
|
||||||
g.Log.Error("Query returned no data", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_response_code": httpResponse.StatusCode})
|
|
||||||
return "", errors.New("Query returned no data")
|
|
||||||
}
|
|
||||||
|
|
||||||
responseContent, err = json.MarshalToString(queryResult.Data)
|
|
||||||
if err != nil {
|
|
||||||
g.Log.Error("Invalid data result", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_result": responseContent, "_response_code": httpResponse.StatusCode})
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if g.Cache {
|
|
||||||
g.Log.Debug("Caching the query", map[string]interface{}{"_query": queryHash})
|
|
||||||
if queryContent[0:5] == "query" {
|
|
||||||
err = g.CacheStore.Set(queryHash, pandati.StringToBytes(responseContent))
|
|
||||||
if err != nil {
|
|
||||||
g.Log.Error("Unable to cache the query", map[string]interface{}{"_query": queryHash, "_error": err.Error()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
package gql
|
|
||||||
|
|
||||||
// import (
|
|
||||||
// "testing"
|
|
||||||
// )
|
|
||||||
|
|
||||||
// func Benchmark_GraphQL_Query(t *testing.B) {
|
|
||||||
// if testing.Short() {
|
|
||||||
// t.Skip("Skipping test in short / CI mode")
|
|
||||||
// }
|
|
||||||
// type args struct {
|
|
||||||
// queryContent string
|
|
||||||
// queryVariables interface{}
|
|
||||||
// }
|
|
||||||
// tests := []struct {
|
|
||||||
// name string
|
|
||||||
// endpoint string
|
|
||||||
// isLocal bool
|
|
||||||
// args args
|
|
||||||
// wantResult string
|
|
||||||
// mockedReply string
|
|
||||||
// wantErr bool
|
|
||||||
// }{
|
|
||||||
// {
|
|
||||||
// name: "Valid query",
|
|
||||||
// endpoint: "http://hasura.local/v1/graphql",
|
|
||||||
// isLocal: false,
|
|
||||||
// args: args{
|
|
||||||
// queryContent: `query listUserBots {
|
|
||||||
// tbl_bots(limit: 1) {
|
|
||||||
// bot_name
|
|
||||||
// }
|
|
||||||
// }`,
|
|
||||||
// queryVariables: nil,
|
|
||||||
// },
|
|
||||||
// wantResult: `{"tbl_bots":[{"bot_name":"littleMentionBot"}]}`,
|
|
||||||
// mockedReply: `{"data":{"tbl_bots":[{"bot_name":"littleMentionBot"}]}}`,
|
|
||||||
// wantErr: false,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Invalid query",
|
|
||||||
// endpoint: "http://hasura.local/v1/graphql",
|
|
||||||
// isLocal: false,
|
|
||||||
// args: args{
|
|
||||||
// queryContent: `query listUserBots {
|
|
||||||
// tbl_botz(limit: 1) {
|
|
||||||
// bot_name
|
|
||||||
// }
|
|
||||||
// }`,
|
|
||||||
// queryVariables: nil,
|
|
||||||
// },
|
|
||||||
// mockedReply: `{"errors":[{"message":"Unknown field \"tbl_botz\" on type \"Query\""}]}`,
|
|
||||||
// wantResult: ``,
|
|
||||||
// wantErr: true,
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for _, tt := range tests {
|
|
||||||
// t.Run(tt.name, func(t *testing.B) {
|
|
||||||
// g := NewConnection()
|
|
||||||
// server, serverURL := mockGraphQLServerResponses(tt.mockedReply)
|
|
||||||
// g.Endpoint = serverURL
|
|
||||||
// for n := 0; n < t.N; n++ {
|
|
||||||
// g.Query(tt.args.queryContent, tt.args.queryVariables, nil)
|
|
||||||
// }
|
|
||||||
// server.Close()
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
+223
-217
@@ -1,282 +1,288 @@
|
|||||||
package gql
|
package gql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"reflect"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (suite *TestSuite) Test_GraphQL_queryBuilder() {
|
func TestBaseClient_convertToJson(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
queryVariables interface{}
|
v any
|
||||||
queryContent string
|
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
fields fields
|
||||||
args args
|
args args
|
||||||
want []byte
|
want []byte
|
||||||
wantErr bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid query",
|
name: "Test convertToJson",
|
||||||
|
fields: fields{},
|
||||||
args: args{
|
args: args{
|
||||||
queryContent: `query listUserBots { tbl_bots { bot_name } }`,
|
v: map[string]interface{}{
|
||||||
queryVariables: nil,
|
"query": "query { hello }",
|
||||||
},
|
},
|
||||||
want: []byte(`{"variables":null,"query":"query listUserBots { tbl_bots { bot_name } }"}`),
|
},
|
||||||
wantErr: false,
|
want: []byte(`{"query":"query { hello }"}`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Valid query with variables",
|
name: "Test convertToJson with variables",
|
||||||
|
fields: fields{},
|
||||||
args: args{
|
args: args{
|
||||||
queryContent: `query listUserBots { tbl_bots { bot_name } }`,
|
v: map[string]interface{}{
|
||||||
queryVariables: map[string]interface{}{"user_id": 1},
|
"query": "query { hello }",
|
||||||
|
"variables": map[string]interface{}{"name": "John"},
|
||||||
},
|
},
|
||||||
want: []byte(`{"variables":{"user_id":1},"query":"query listUserBots { tbl_bots { bot_name } }"}`),
|
},
|
||||||
wantErr: false,
|
want: []byte(`{"query":"query { hello }","variables":{"name":"John"}}`),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
suite.T().Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
q := NewConnection()
|
c := &BaseClient{}
|
||||||
got, gotErr := q.queryBuilder(tt.args.queryContent, tt.args.queryVariables)
|
if got := c.convertToJson(tt.args.v); !reflect.DeepEqual(got, tt.want) {
|
||||||
assert.Equal(t, tt.want, got)
|
t.Errorf("BaseClient.convertToJson() = %v, want %v", got, tt.want)
|
||||||
if tt.wantErr {
|
|
||||||
assert.Error(t, gotErr)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *TestSuite) Test_GraphQL_Query() {
|
func TestBaseClient_NewQuery(t *testing.T) {
|
||||||
if testing.Short() {
|
type fields struct {
|
||||||
suite.T().Skip("Skipping test in short / CI mode")
|
|
||||||
}
|
}
|
||||||
g := NewConnection()
|
type args struct {
|
||||||
|
q []any
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
fields fields
|
||||||
|
want *Query
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Test NewQuery",
|
||||||
|
fields: fields{},
|
||||||
|
args: args{
|
||||||
|
q: []any{
|
||||||
|
"query { hello }",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &Query{
|
||||||
|
compiledQuery: []byte(`{"variables":null,"query":"query { hello }"}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test NewQuery with variables",
|
||||||
|
fields: fields{},
|
||||||
|
args: args{
|
||||||
|
q: []any{
|
||||||
|
"query { hello }",
|
||||||
|
map[string]interface{}{"name": "John"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &Query{
|
||||||
|
compiledQuery: []byte(`{"variables":{"name":"John"},"query":"query { hello }"}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := NewConnection()
|
||||||
|
gotQuery := c.NewQuery(tt.args.q...)
|
||||||
|
assert.Equal(t, tt.want.query, gotQuery.query)
|
||||||
|
assert.Equal(t, tt.want.variables, gotQuery.variables)
|
||||||
|
assert.Equal(t, tt.want.compiledQuery, gotQuery.compiledQuery)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseClient_decodeResponse(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
responseType string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
jsonData []byte
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
fields fields
|
||||||
|
want any
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Test decodeResponse - string",
|
||||||
|
fields: fields{
|
||||||
|
responseType: "string",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
jsonData: []byte(`{"data":{"hello":"world"}}`),
|
||||||
|
},
|
||||||
|
want: `{"data":{"hello":"world"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test decodeResponse - byte",
|
||||||
|
fields: fields{
|
||||||
|
responseType: "byte",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
jsonData: []byte(`{"data":{"hello":"world"}}`),
|
||||||
|
},
|
||||||
|
want: []byte(`{"data":{"hello":"world"}}`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := NewConnection()
|
||||||
|
c.responseType = tt.fields.responseType
|
||||||
|
if got := c.decodeResponse(tt.args.jsonData); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("BaseClient.decodeResponse() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseClient_Query(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
graphql_endpoint string
|
||||||
|
repeat int
|
||||||
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
queryVariables interface{}
|
queryVariables interface{}
|
||||||
queryHeaders map[string]interface{}
|
queryHeaders map[string]interface{}
|
||||||
queryContent string
|
queryContent string
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
want any
|
||||||
args args
|
args args
|
||||||
name string
|
name string
|
||||||
endpoint string
|
fields fields
|
||||||
wantResult string
|
|
||||||
isLocal bool
|
|
||||||
cache_enabled bool
|
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid query, no cache",
|
name: "Test Query - failing",
|
||||||
endpoint: "https://telegram-bot.app/v1/graphql",
|
fields: fields{},
|
||||||
isLocal: true,
|
|
||||||
cache_enabled: false,
|
|
||||||
args: args{
|
args: args{
|
||||||
queryContent: `query packages_prices {
|
queryContent: "query { hello }",
|
||||||
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
|
|
||||||
aggregate {
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
queryVariables: nil,
|
|
||||||
},
|
},
|
||||||
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid query, no cache #2",
|
|
||||||
endpoint: "https://telegram-bot.app/v1/graphql",
|
|
||||||
isLocal: true,
|
|
||||||
cache_enabled: false,
|
|
||||||
args: args{
|
|
||||||
queryContent: `query packages_prices {
|
|
||||||
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
|
|
||||||
aggregate {
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
queryVariables: nil,
|
|
||||||
},
|
|
||||||
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid query, with cache empty",
|
|
||||||
endpoint: "https://telegram-bot.app/v1/graphql",
|
|
||||||
isLocal: true,
|
|
||||||
cache_enabled: true,
|
|
||||||
args: args{
|
|
||||||
queryContent: `query packages_prices {
|
|
||||||
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
|
|
||||||
aggregate {
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
queryVariables: nil,
|
|
||||||
},
|
|
||||||
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid query, with cache per query empty",
|
|
||||||
endpoint: "https://telegram-bot.app/v1/graphql",
|
|
||||||
isLocal: true,
|
|
||||||
cache_enabled: false,
|
|
||||||
args: args{
|
|
||||||
queryContent: `query packages_prices_no_inlinecache {
|
|
||||||
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
|
|
||||||
aggregate {
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
queryVariables: nil,
|
|
||||||
queryHeaders: map[string]interface{}{
|
|
||||||
"gqlcache": false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid query, with cache per query",
|
|
||||||
endpoint: "https://telegram-bot.app/v1/graphql",
|
|
||||||
isLocal: true,
|
|
||||||
cache_enabled: false,
|
|
||||||
args: args{
|
|
||||||
queryContent: `query packages_prices_inlinecache {
|
|
||||||
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
|
|
||||||
aggregate {
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
queryVariables: nil,
|
|
||||||
queryHeaders: map[string]interface{}{
|
|
||||||
"gqlcache": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid query, with cache per query [verify]",
|
|
||||||
endpoint: "https://telegram-bot.app/v1/graphql",
|
|
||||||
isLocal: true,
|
|
||||||
cache_enabled: false,
|
|
||||||
args: args{
|
|
||||||
queryContent: `query packages_prices_inlinecache {
|
|
||||||
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
|
|
||||||
aggregate {
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
queryVariables: nil,
|
|
||||||
queryHeaders: map[string]interface{}{
|
|
||||||
"gqlcache": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid query, with cache filled",
|
|
||||||
endpoint: "https://telegram-bot.app/v1/graphql",
|
|
||||||
isLocal: true,
|
|
||||||
cache_enabled: true,
|
|
||||||
args: args{
|
|
||||||
queryContent: `query packages_prices {
|
|
||||||
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
|
|
||||||
aggregate {
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
queryVariables: nil,
|
|
||||||
},
|
|
||||||
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid query",
|
|
||||||
endpoint: "https://telegram-bot.app/v1/graphql",
|
|
||||||
isLocal: true,
|
|
||||||
args: args{
|
|
||||||
queryContent: `query packages_pricez {
|
|
||||||
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
|
|
||||||
aggregate {
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
queryVariables: nil,
|
|
||||||
},
|
|
||||||
wantResult: ``,
|
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Valid query to https endpoint",
|
name: "Test Query - success",
|
||||||
endpoint: "https://telegram-bot.app/v1/graphql",
|
fields: fields{
|
||||||
isLocal: false,
|
graphql_endpoint: "https://spacex-production.up.railway.app/",
|
||||||
args: args{
|
|
||||||
queryContent: `query packages_prices {
|
|
||||||
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
|
|
||||||
aggregate {
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
queryVariables: nil,
|
|
||||||
},
|
},
|
||||||
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid query to github endpoint",
|
|
||||||
endpoint: "https://api.github.com/graphql",
|
|
||||||
isLocal: false,
|
|
||||||
args: args{
|
args: args{
|
||||||
queryContent: `query {
|
queryContent: `query Dragons {
|
||||||
repository(name: "semver-generator", owner: "lukaszraczylo", followRenames: true) {
|
dragons {
|
||||||
releases(last: 2) {
|
|
||||||
nodes {
|
|
||||||
tag {
|
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
}
|
}`,
|
||||||
}
|
queryHeaders: map[string]interface{}{
|
||||||
|
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
want: `{"dragons":[{"name":"Dragon 1"},{"name":"Dragon 2"}]}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Query - success",
|
||||||
|
fields: fields{
|
||||||
|
repeat: 2,
|
||||||
|
graphql_endpoint: "https://spacex-production.up.railway.app/",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
queryContent: `query Dragons {
|
||||||
|
dragons {
|
||||||
|
name
|
||||||
|
first_flight
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
queryVariables: nil,
|
|
||||||
queryHeaders: map[string]interface{}{
|
queryHeaders: map[string]interface{}{
|
||||||
"Authorization": "Bearer " + os.Getenv("GITHUB_TOKEN"),
|
"x-apollo-operation-name": "Missions-Potato-Test-Golang-Cached",
|
||||||
|
"content-type": "application/json",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResult: `{"repository":{"releases":{"nodes":[`,
|
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
|
want: `{"dragons":[{"first_flight":"2010-12-08","name":"Dragon 1"},{"first_flight":"2019-03-02","name":"Dragon 2"}]}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
suite.T().Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// if tt.isLocal {
|
c := NewConnection()
|
||||||
// os.Setenv("GRAPHQL_ENDPOINT", tt.endpoint)
|
|
||||||
// }
|
if tt.fields.graphql_endpoint != "" {
|
||||||
g.Endpoint = tt.endpoint
|
c.endpoint = tt.fields.graphql_endpoint
|
||||||
g.Cache = tt.cache_enabled
|
}
|
||||||
gotResult, gotErr := g.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
|
|
||||||
if tt.wantErr {
|
repeat := 1
|
||||||
assert.Error(t, gotErr)
|
if tt.fields.repeat != 0 {
|
||||||
|
repeat = tt.fields.repeat
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < repeat; i++ {
|
||||||
|
|
||||||
|
got, err := c.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("BaseClient.Query() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("BaseClient.Query() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
assert.Contains(t, gotResult, tt.wantResult)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fmt.Println(g.CacheStore.Capacity())
|
}
|
||||||
|
|
||||||
|
/// benchmarks
|
||||||
|
|
||||||
|
func BenchmarkBaseClient_convertToJson(b *testing.B) {
|
||||||
|
c := NewConnection()
|
||||||
|
query := c.NewQuery("query { hello }")
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = c.convertToJson(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNewQuery(b *testing.B) {
|
||||||
|
c := NewConnection()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = c.NewQuery("query { hello }")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecodeResponse(b *testing.B) {
|
||||||
|
c := NewConnection()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = c.decodeResponse([]byte(`{"data":{"hello":"world"}}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkQueryNoCache(b *testing.B) {
|
||||||
|
c := NewConnection()
|
||||||
|
c.cache.enabled = false
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = c.Query(`query MyQuery {
|
||||||
|
bots {
|
||||||
|
bot_name
|
||||||
|
}
|
||||||
|
}`, nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkQueryWithCache(b *testing.B) {
|
||||||
|
c := NewConnection()
|
||||||
|
c.cache.enabled = true
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = c.Query(`query MyQuery {
|
||||||
|
bots {
|
||||||
|
bot_name
|
||||||
|
}
|
||||||
|
}`, nil, nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
package gql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *BaseClient) executeQuery(query []byte, headers any) (result any, err error) {
|
||||||
|
var queryResult queryResults
|
||||||
|
httpRequest, err := http.NewRequest("POST", c.endpoint, bytes.NewBuffer(query))
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Error(c, "Error while creating http request;", "error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range headers.(map[string]interface{}) {
|
||||||
|
httpRequest.Header.Add(key, value.(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpResponse *http.Response
|
||||||
|
|
||||||
|
httpResponse, err = c.client.Do(httpRequest)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Error(c, "Error while executing http request;", "error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer httpResponse.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(httpResponse.Body)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Error(c, "Error while reading http response;", "error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(body, &queryResult)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Error(c, "Error while unmarshalling http response;", "error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(queryResult.Errors) > 0 {
|
||||||
|
return nil, fmt.Errorf("%v", queryResult.Errors)
|
||||||
|
}
|
||||||
|
return queryResult.Data, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
gql := graphql.NewConnection()
|
||||||
|
|
||||||
|
query := `query MyQuery {
|
||||||
|
bots {
|
||||||
|
bot_name
|
||||||
|
ddd
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
_, err := gql.Query(query, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error returned from query:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = `query MyQuery {
|
||||||
|
badwords(distinct_on: word) {
|
||||||
|
word
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
gql.Query(query, nil, nil)
|
||||||
|
|
||||||
|
// Repeated to use the cached response
|
||||||
|
query = `query MyQuery {
|
||||||
|
badwords(distinct_on: word) {
|
||||||
|
word
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
gql.Query(query, nil, nil)
|
||||||
|
}
|
||||||
+2
-1
@@ -1,8 +1,9 @@
|
|||||||
version: 1
|
version: 1
|
||||||
force:
|
force:
|
||||||
major: 1
|
major: 1
|
||||||
minor: 0
|
minor: 1
|
||||||
patch: 0
|
patch: 0
|
||||||
|
commit: 6a8f99f6ef8f65a303dad74c272ff12bf9fe526f
|
||||||
wording:
|
wording:
|
||||||
minor:
|
minor:
|
||||||
- minor-release
|
- minor-release
|
||||||
|
|||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
package gql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/lukaszraczylo/go-simple-graphql/utils/concurrency"
|
||||||
|
"github.com/lukaszraczylo/go-simple-graphql/utils/logger"
|
||||||
|
|
||||||
|
"github.com/allegro/bigcache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cacheStore struct {
|
||||||
|
client *bigcache.BigCache
|
||||||
|
ttl int
|
||||||
|
enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseClient struct {
|
||||||
|
Logger Logger
|
||||||
|
LoggerWriter Writer
|
||||||
|
client *http.Client
|
||||||
|
concurrencyManager *concurrency.Pool
|
||||||
|
endpoint string
|
||||||
|
responseType string
|
||||||
|
cache cacheStore
|
||||||
|
MaxGoRoutines int
|
||||||
|
LoggingLevel logger.LogLevel
|
||||||
|
LoggerColorful bool
|
||||||
|
validate bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query struct {
|
||||||
|
context context.Context
|
||||||
|
variables map[string]any `json:"variables"`
|
||||||
|
query string `json:"query"`
|
||||||
|
compiledQuery []byte `json:"compiledQuery"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type request struct {
|
||||||
|
Variables any `json:"variables"`
|
||||||
|
Query string `json:"query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type queryResults struct {
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
Errors []struct {
|
||||||
|
Message interface{} `json:"message"`
|
||||||
|
} `json:"errors"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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...)
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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...)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StringToBytes(s string) []byte {
|
||||||
|
return unsafe.Slice(unsafe.StringData(s), len(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func BytesToString(b []byte) string {
|
||||||
|
return unsafe.String(unsafe.SliceData(b), len(b))
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStringToBytes(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
s string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "TestStringToBytes",
|
||||||
|
args: args{
|
||||||
|
s: "hello world",
|
||||||
|
},
|
||||||
|
want: []byte("hello world"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := StringToBytes(tt.args.s); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("StringToBytes() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBytesToString(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
b []byte
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want string
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "TestBytesToString",
|
||||||
|
args: args{
|
||||||
|
b: []byte("hello world"),
|
||||||
|
},
|
||||||
|
want: "hello world",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := BytesToString(tt.args.b); got != tt.want {
|
||||||
|
t.Errorf("BytesToString() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStringToBytes(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
StringToBytes("hello world")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkBytesToString(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
BytesToString([]byte("hello world"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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 (
|
||||||
|
reset = "\033[0m"
|
||||||
|
redBold = "\033[1;31m"
|
||||||
|
greenBold = "\033[1;32m"
|
||||||
|
yellowBold = "\033[1;33m"
|
||||||
|
blueBold = "\033[1;34m"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogLevel int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Silent LogLevel = iota
|
||||||
|
Error
|
||||||
|
Warn
|
||||||
|
Info
|
||||||
|
Debug
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Colorful bool
|
||||||
|
LogLevel LogLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
func Serialize(v any) (string, error) {
|
||||||
|
marshal, err := json.Marshal(internalSerialize(v))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return helpers.BytesToString(marshal), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func internalSerialize(v any) any {
|
||||||
|
if v != nil {
|
||||||
|
switch reflect.TypeOf(v).Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
r := ordered_map.New()
|
||||||
|
name := reflect.TypeOf(v).Name()
|
||||||
|
if name != "" {
|
||||||
|
r.Set("_", reflect.TypeOf(v).String())
|
||||||
|
}
|
||||||
|
for i := 0; i < reflect.ValueOf(v).NumField(); i++ {
|
||||||
|
r.Set(reflect.TypeOf(v).Field(i).Name, internalSerialize(reflect.ValueOf(v).Field(i).Interface()))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
case reflect.Ptr:
|
||||||
|
if !reflect.ValueOf(v).IsNil() {
|
||||||
|
return internalSerialize(reflect.ValueOf(v).Elem().Interface())
|
||||||
|
}
|
||||||
|
case reflect.Slice:
|
||||||
|
var r []any
|
||||||
|
tmpSlice := reflect.ValueOf(v)
|
||||||
|
for i := 0; i < tmpSlice.Len(); i++ {
|
||||||
|
r = append(r, internalSerialize(tmpSlice.Index(i).Interface()))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
type Person struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSerializeStruct(t *testing.T) {
|
||||||
|
person := Person{Name: "Alice", Age: 30}
|
||||||
|
expected := `{"_":"logger.Person","Name":"Alice","Age":30}`
|
||||||
|
actual, err := Serialize(person)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
if actual != expected {
|
||||||
|
t.Errorf("Expected: %v\nActual: %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSerializeNilPointer(t *testing.T) {
|
||||||
|
var person *Person
|
||||||
|
expected := `null`
|
||||||
|
actual, err := Serialize(person)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
if actual != expected {
|
||||||
|
t.Errorf("Expected: %v\nActual: %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSerializeNonNilPointer(t *testing.T) {
|
||||||
|
person := &Person{Name: "Alice", Age: 30}
|
||||||
|
expected := `{"_":"logger.Person","Name":"Alice","Age":30}`
|
||||||
|
actual, err := Serialize(person)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
if actual != expected {
|
||||||
|
t.Errorf("Expected: %v\nActual: %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSerializeSlice(t *testing.T) {
|
||||||
|
people := []Person{
|
||||||
|
{Name: "Alice", Age: 30},
|
||||||
|
{Name: "Bob", Age: 40},
|
||||||
|
}
|
||||||
|
expected := `[{"_":"logger.Person","Name":"Alice","Age":30},{"_":"logger.Person","Name":"Bob","Age":40}]`
|
||||||
|
actual, err := Serialize(people)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
if actual != expected {
|
||||||
|
t.Errorf("Expected: %v\nActual: %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSerializeSliceOfPointers(t *testing.T) {
|
||||||
|
people := []*Person{
|
||||||
|
{Name: "Alice", Age: 30},
|
||||||
|
{Name: "Bob", Age: 40},
|
||||||
|
}
|
||||||
|
expected := `[{"_":"logger.Person","Name":"Alice","Age":30},{"_":"logger.Person","Name":"Bob","Age":40}]`
|
||||||
|
|
||||||
|
actual, err := Serialize(people)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %v", err)
|
||||||
|
}
|
||||||
|
if actual != expected {
|
||||||
|
t.Errorf("Expected: %v\nActual: %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSerializeUnsupportedType(t *testing.T) {
|
||||||
|
type UnsupportedType chan int
|
||||||
|
unsupportedType := make(UnsupportedType)
|
||||||
|
_, err := Serialize(unsupportedType)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected an error, but got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package ordered_map
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
|
||||||
|
type OrderedMap struct {
|
||||||
|
values map[string]interface{}
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *OrderedMap {
|
||||||
|
o := OrderedMap{}
|
||||||
|
o.keys = []string{}
|
||||||
|
o.values = map[string]interface{}{}
|
||||||
|
return &o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OrderedMap) Set(key string, value interface{}) {
|
||||||
|
_, exists := o.values[key]
|
||||||
|
if !exists {
|
||||||
|
o.keys = append(o.keys, key)
|
||||||
|
}
|
||||||
|
o.values[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o OrderedMap) MarshalJSON() ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteByte('{')
|
||||||
|
encoder := json.NewEncoder(&buf)
|
||||||
|
encoder.SetEscapeHTML(false)
|
||||||
|
encoder.SetIndent("", "")
|
||||||
|
for i, k := range o.keys {
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
buf.WriteByte('"')
|
||||||
|
buf.WriteString(k)
|
||||||
|
buf.WriteString(`":`)
|
||||||
|
customEncoder := &customJsonEncoder{buf: &buf}
|
||||||
|
if err := customEncoder.Encode(o.values[k]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteByte('}')
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type customJsonEncoder struct {
|
||||||
|
buf *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *customJsonEncoder) Encode(v interface{}) error {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.buf.Write(data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package ordered_map
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
want *OrderedMap
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "TestNew",
|
||||||
|
want: &OrderedMap{
|
||||||
|
keys: []string{},
|
||||||
|
values: map[string]interface{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := New(); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("New() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrderedMap_Set(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
values map[string]interface{}
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
value interface{}
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
args args
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "TestOrderedMap_Set",
|
||||||
|
fields: fields{
|
||||||
|
values: map[string]interface{}{},
|
||||||
|
keys: []string{},
|
||||||
|
},
|
||||||
|
args: args{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TestOrderedMap_Set2",
|
||||||
|
fields: fields{
|
||||||
|
values: map[string]interface{}{
|
||||||
|
"hello": "world",
|
||||||
|
},
|
||||||
|
keys: []string{"hello"},
|
||||||
|
},
|
||||||
|
args: args{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
o := &OrderedMap{
|
||||||
|
values: tt.fields.values,
|
||||||
|
keys: tt.fields.keys,
|
||||||
|
}
|
||||||
|
o.Set(tt.args.key, tt.args.value)
|
||||||
|
for _, key := range o.keys {
|
||||||
|
if _, ok := o.values[key]; !ok {
|
||||||
|
t.Errorf("OrderedMap.Set() key not found in values map")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrderedMap_MarshalJSON(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
values map[string]interface{}
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want []byte
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "TestOrderedMap_MarshalJSON",
|
||||||
|
fields: fields{
|
||||||
|
values: map[string]interface{}{
|
||||||
|
"hello": "world",
|
||||||
|
},
|
||||||
|
keys: []string{"hello"},
|
||||||
|
},
|
||||||
|
want: []byte(`{"hello":"world"}`),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TestOrderedMap_MarshalJSON2",
|
||||||
|
fields: fields{
|
||||||
|
values: map[string]interface{}{
|
||||||
|
"hello": "world",
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
keys: []string{"hello", "foo"},
|
||||||
|
},
|
||||||
|
want: []byte(`{"hello":"world","foo":"bar"}`),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TestOrderedMap_MarshalJSON_with_error",
|
||||||
|
fields: fields{
|
||||||
|
values: map[string]interface{}{
|
||||||
|
"hello": "world",
|
||||||
|
"foo": make(chan int),
|
||||||
|
},
|
||||||
|
keys: []string{"hello", "foo"},
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
o := OrderedMap{
|
||||||
|
values: tt.fields.values,
|
||||||
|
keys: tt.fields.keys,
|
||||||
|
}
|
||||||
|
got, err := o.MarshalJSON()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("OrderedMap.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("OrderedMap.MarshalJSON() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkOrderedMapTest(b *testing.B) {
|
||||||
|
m := New()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
m.Set("hello", "world")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkOrderedMarshalJSON(b *testing.B) {
|
||||||
|
m := New()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
m.Set("hello", "world")
|
||||||
|
m.MarshalJSON()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMapTest(b *testing.B) {
|
||||||
|
m := make(map[string]interface{})
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
m["hello"] = "world"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user