mirror of
https://github.com/lukaszraczylo/go-simple-graphql.git
synced 2026-10-12 00:45:31 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23f88a91d2
|
||
|
|
664a692c2d
|
||
|
|
2a628a7da7
|
||
|
|
e0913f8824
|
||
|
|
6a8f99f6ef
|
||
|
|
a5c4643a5e
|
||
|
|
42e1afbf77
|
||
|
|
9e8df7e297
|
||
|
|
405d159eb9
|
||
|
|
b3e594fc4f
|
||
|
|
32d9e0bd80
|
||
|
|
e706b07488
|
||
|
|
32091fc8cd
|
||
|
|
edc695fe2b
|
||
|
|
bd0c5ec373
|
@@ -11,7 +11,7 @@ help: ## display this help
|
||||
|
||||
.PHONY: test
|
||||
test: ## run tests on library
|
||||
@LOG_LEVEL=debug go test $(ADDITIONAL_BUILD_FLAGS) -v -cover ./...
|
||||
@LOG_LEVEL=debug go test $(ADDITIONAL_BUILD_FLAGS) -v -cover ./... -race
|
||||
|
||||
.PHONY: test-packages
|
||||
test-packages: ## run tests on packages
|
||||
@@ -23,6 +23,7 @@ all: test-packages test
|
||||
.PHONY: update
|
||||
update: ## update dependencies
|
||||
@go get -u -v ./...
|
||||
@go mod tidy -v
|
||||
|
||||
sonar:
|
||||
sonar-scanner \
|
||||
|
||||
@@ -9,7 +9,9 @@ Ps. It's Hasura friendly.
|
||||
- [Features](#features)
|
||||
- [Usage example](#usage-example)
|
||||
- [Environment variables](#environment-variables)
|
||||
- [Cache](#cache)
|
||||
- [Example reader code](#example-reader-code)
|
||||
- [Tips](#tips)
|
||||
- [Working with results](#working-with-results)
|
||||
|
||||
## Reasoning
|
||||
@@ -30,12 +32,29 @@ Therefore, I present you the simple client to which you can copy & paste your gr
|
||||
### Environment variables
|
||||
|
||||
* `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`
|
||||
* `LOG_LEVEL` - Logging level. Default: `info`
|
||||
* `RETRIES_ENABLE` - Should retries be enabled? Default: `false`
|
||||
* `RETRIES_NUMBER` - Number of retries: Default: `1`
|
||||
* `RETRIES_DELAY` - Delay in retries in milliseconds. Default: `250`
|
||||
* `GRAPHQL_OUTPUT` - Output format. Default: `byte`, available: `byte`, `string`
|
||||
* `LOG_LEVEL` - Logging level. Default: `info` available: `debug`, `info`, `warn`, `error`
|
||||
* `GRAPHQL_RETRIES_ENABLE` - Should retries be enabled? Default: `true`
|
||||
* `GRAPHQL_RETRIES_NUMBER` - Number of retries: Default: `1`
|
||||
* `GRAPHQL_RETRIES_DELAY` - Delay in retries in milliseconds. Default: `250`
|
||||
|
||||
### Cache
|
||||
|
||||
You have two options to enable the cache:
|
||||
|
||||
* 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.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
headers := map[string]interface{}{
|
||||
...
|
||||
"gqlcache": true,
|
||||
}
|
||||
```
|
||||
|
||||
### Example reader code
|
||||
|
||||
@@ -46,32 +65,39 @@ import (
|
||||
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
||||
)
|
||||
|
||||
headers := map[string]interface{}{
|
||||
"x-hasura-user-id": 37,
|
||||
"x-hasura-user-uuid": "bde1962e-b42e-1212-ac10-d43fa27f44a5",
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"fileHash": "123deadc0w321",
|
||||
}
|
||||
|
||||
query := `query searchFileKnown($fileHash: String) {
|
||||
tbl_file_scans(where: {file_hash: {_eq: $fileHash}}) {
|
||||
racy
|
||||
violence
|
||||
virus
|
||||
func main() {
|
||||
headers := map[string]interface{}{
|
||||
"x-hasura-user-id": 37,
|
||||
"x-hasura-user-uuid": "bde1962e-b42e-1212-ac10-d43fa27f44a5",
|
||||
}
|
||||
}`
|
||||
|
||||
gql := graphql.NewConnection()
|
||||
result, err := gql.Query(query, variables, headers)
|
||||
if err != nil {
|
||||
fmt.Println("Query error", err)
|
||||
return
|
||||
variables := map[string]interface{}{
|
||||
"fileHash": "123deadc0w321",
|
||||
}
|
||||
|
||||
query := `query searchFileKnown($fileHash: String) {
|
||||
tbl_file_scans(where: {file_hash: {_eq: $fileHash}}) {
|
||||
racy
|
||||
violence
|
||||
virus
|
||||
}
|
||||
}`
|
||||
|
||||
gql := graphql.NewConnection()
|
||||
result, err := gql.Query(query, variables, headers)
|
||||
if err != nil {
|
||||
fmt.Println("Query error", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(result)
|
||||
}
|
||||
fmt.Println(result)
|
||||
```
|
||||
|
||||
### Tips
|
||||
|
||||
* Connection handler ( `gql := graphql.NewConnection()` ) should be created once and reused in the application especially if you run dozens of queries per second. It will allow you also to use cache and http2 to its full potential.
|
||||
|
||||
|
||||
**Result**
|
||||
|
||||
```json
|
||||
@@ -80,7 +106,9 @@ fmt.Println(result)
|
||||
|
||||
## Working with results
|
||||
|
||||
I'm using an amazing library [tidwall/gjson](https://github.com/tidwall/gjson) to parse the results and extract the information required in further steps and I strongly recommend this approach as the easiest and close to painless, for example:
|
||||
Currently attempting to switch to the fork of the [`ask` library](https://github.com/lukaszraczylo/ask)
|
||||
|
||||
Before, I used an amazing library [tidwall/gjson](https://github.com/tidwall/gjson) to parse the results and extract the information required in further steps and I strongly recommend this approach as the easiest and close to painless, for example:
|
||||
|
||||
```go
|
||||
result := gjson.Get(result, "tbl_user_group_admins.0.is_admin").Bool()
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
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)
|
||||
|
||||
b.retries.enabled = envutil.GetBool("GRAPHQL_RETRIES_ENABLED", true)
|
||||
b.retries.max = envutil.GetInt("GRAPHQL_RETRIES_MAX", 1)
|
||||
b.retries.delay = envutil.GetInt("GRAPHQL_RETRIES_DELAY", 300)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,32 +1,29 @@
|
||||
module github.com/lukaszraczylo/go-simple-graphql
|
||||
|
||||
go 1.18
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/allegro/bigcache/v3 v3.1.0
|
||||
github.com/avast/retry-go v3.0.0+incompatible
|
||||
github.com/buger/jsonparser v1.1.1
|
||||
github.com/allegro/bigcache v1.2.1
|
||||
github.com/avast/retry-go/v4 v4.3.3
|
||||
github.com/gookit/goutil v0.6.8
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/lukaszraczylo/pandati v0.0.15
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/sethvargo/go-retry v0.2.3
|
||||
github.com/stretchr/testify v1.7.1
|
||||
golang.org/x/net v0.2.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
golang.org/x/net v0.9.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/gookit/color v1.5.3 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/wI2L/jsondiff v0.3.0 // indirect
|
||||
golang.org/x/sys v0.3.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/term v0.7.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,64 +1,69 @@
|
||||
github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk=
|
||||
github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
|
||||
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
|
||||
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/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc=
|
||||
github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
||||
github.com/avast/retry-go/v4 v4.3.3 h1:G56Bp6mU0b5HE1SkaoVjscZjlQb0oy4mezwY/cGH19w=
|
||||
github.com/avast/retry-go/v4 v4.3.3/go.mod h1:rg6XFaiuFYII0Xu3RDbZQkxCofFwruZKW8oEF1jpWiU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||
github.com/gookit/color v1.5.3 h1:twfIhZs4QLCtimkP7MOxlF3A0U/5cDPseRT9M/+2SCE=
|
||||
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/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/lukaszraczylo/pandati v0.0.15 h1:1BNtbmhIQbKgs1Gm9WlH3CMms+TgkgxZcU7utHbjloY=
|
||||
github.com/lukaszraczylo/pandati v0.0.15/go.mod h1:5E0mqlNgdOps6FW2X4tPilr+3B8s9JGWFgA6ZVFixyQ=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
|
||||
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/sethvargo/go-retry v0.2.3 h1:oYlgvIvsju3jNbottWABtbnoLC+GDtLdBHxKWxQm/iU=
|
||||
github.com/sethvargo/go-retry v0.2.3/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw=
|
||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/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.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
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.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4=
|
||||
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
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.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.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/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
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.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
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 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,140 +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"
|
||||
"time"
|
||||
|
||||
"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 {
|
||||
Endpoint string
|
||||
HttpClient *http.Client
|
||||
Log *logging.LogConfig
|
||||
Cache bool // Enable caching for read queries
|
||||
CacheStore *bigcache.BigCache
|
||||
RetriesEnable bool
|
||||
RetriesNumber int
|
||||
RetriesDelay time.Duration
|
||||
BackoffSetup retry.Backoff
|
||||
}
|
||||
|
||||
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 {
|
||||
g := GraphQL{
|
||||
Endpoint: pickGraphqlEndpoint(),
|
||||
HttpClient: &http.Client{
|
||||
Transport: &http2.Transport{
|
||||
ReadIdleTimeout: 30 * time.Second,
|
||||
DisableCompression: false,
|
||||
AllowHTTP: true,
|
||||
PingTimeout: 5 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
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 {
|
||||
name string
|
||||
args args
|
||||
want *GraphQL
|
||||
}{
|
||||
{
|
||||
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
|
||||
stdOutExpect bool
|
||||
stdErrExpect bool
|
||||
loggerType string
|
||||
}{
|
||||
{
|
||||
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 {
|
||||
message string
|
||||
extraFields map[string]interface{}
|
||||
}
|
||||
extraFields := make(map[string]interface{})
|
||||
extraFields["_full_message"] = "full message"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantLevel string
|
||||
wantMessage string
|
||||
envMinLogLevel string
|
||||
stdOutExpect bool
|
||||
stdErrExpect bool
|
||||
loggerType string
|
||||
}{
|
||||
{
|
||||
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,134 +1,117 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"context"
|
||||
|
||||
retry "github.com/avast/retry-go"
|
||||
"github.com/lukaszraczylo/pandati"
|
||||
"github.com/lukaszraczylo/go-simple-graphql/utils/helpers"
|
||||
|
||||
"github.com/gookit/goutil/strutil"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
type requestBase struct {
|
||||
Query string `json:"query"`
|
||||
Variables interface{} `json:"variables"`
|
||||
}
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
type queryResults struct {
|
||||
Data interface{} `json:"data"`
|
||||
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)
|
||||
func (c *BaseClient) convertToJson(v any) []byte {
|
||||
json, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
g.Log.Critical("Unable to marshal the query", map[string]interface{}{"_error": err.Error(), "_query": queryContent, "_variables": queryVariables})
|
||||
return []byte{}, err
|
||||
c.Logger.Error(c, "Can't convert to json;", "error", err.Error())
|
||||
return nil
|
||||
}
|
||||
return j2, err
|
||||
return json
|
||||
}
|
||||
|
||||
func (g *GraphQL) Query(queryContent string, queryVariables interface{}, queryHeaders map[string]interface{}) (responseContent string, err error) {
|
||||
g.Log.Debug("Query details", map[string]interface{}{"_query": queryContent, "_variables": queryVariables})
|
||||
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
|
||||
}
|
||||
func (c *BaseClient) NewQuery(q ...any) *Query {
|
||||
query := &Query{}
|
||||
|
||||
var body []byte
|
||||
var queryResult *queryResults
|
||||
queryHash := fmt.Sprintf("%x", md5.Sum(query))
|
||||
|
||||
if g.Cache {
|
||||
g.Log.Debug("Checking the cache for the query", map[string]interface{}{"_query": queryHash})
|
||||
if entry, entryInfo, err := g.CacheStore.GetWithInfo(queryHash); err == nil {
|
||||
g.Log.Debug("Found the query in the cache", map[string]interface{}{"_query": queryHash})
|
||||
if pandati.IsZero(entryInfo.EntryStatus) {
|
||||
return string(entry), nil
|
||||
if len(q) > 0 {
|
||||
for _, v := range q {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
query.query = val
|
||||
case map[string]interface{}:
|
||||
query.variables = val
|
||||
case context.Context:
|
||||
query.context = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query.compiledQuery = c.convertToJson(request{
|
||||
Query: query.query,
|
||||
Variables: query.variables,
|
||||
})
|
||||
|
||||
c.Logger.Debug(c, "Clearing previously prepared variables and query")
|
||||
query.variables = nil
|
||||
|
||||
if query.context == nil {
|
||||
query.context = context.Background()
|
||||
}
|
||||
|
||||
if c.validate {
|
||||
c.Logger.Warn(c, "Validating query is not active")
|
||||
}
|
||||
|
||||
query.query = ""
|
||||
c.Logger.Debug(c, "Query prepared;", "query", helpers.BytesToString(query.compiledQuery))
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
func (c *BaseClient) Query(queryContent string, queryVariables interface{}, queryHeaders map[string]interface{}) (any, error) {
|
||||
query := c.NewQuery(queryContent, queryVariables)
|
||||
|
||||
var queryHash string
|
||||
var cachedResponse []byte
|
||||
|
||||
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 {
|
||||
g.Log.Debug("Unable to find the query in the cache", map[string]interface{}{"_query": queryHash, "_error": err.Error()})
|
||||
c.Logger.Debug(c, "No cached response found")
|
||||
}
|
||||
}
|
||||
|
||||
httpRequest, err := http.NewRequest("POST", g.Endpoint, bytes.NewBuffer(query))
|
||||
// httpRequest.Header.Set("Content-Type", "application/json")
|
||||
response, err := c.executeQuery(query.compiledQuery, queryHeaders)
|
||||
if err != nil {
|
||||
g.Log.Error("Unable to create the request", map[string]interface{}{"_error": err.Error()})
|
||||
return "", err
|
||||
c.Logger.Error(c, "Error while executing query;", "error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for header, value := range queryHeaders {
|
||||
httpRequest.Header.Add(fmt.Sprintf("%v", header), fmt.Sprintf("%v", value))
|
||||
}
|
||||
|
||||
var httpResponse *http.Response
|
||||
|
||||
err = retry.Do(
|
||||
func() error {
|
||||
httpResponse, err = g.HttpClient.Do(httpRequest)
|
||||
|
||||
if err != nil {
|
||||
g.Log.Debug("Unable to send the query RETRY", map[string]interface{}{"_error": err.Error()})
|
||||
return err
|
||||
}
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
if httpResponse.StatusCode <= 200 && httpResponse.StatusCode >= 204 {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err = ioutil.ReadAll(httpResponse.Body)
|
||||
if err != nil {
|
||||
g.Log.Debug("Unable to read the response", map[string]interface{}{"_error": err.Error()})
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &queryResult)
|
||||
if err != nil {
|
||||
g.Log.Debug("Unable to unmarshal the query", map[string]interface{}{"_error": err.Error()})
|
||||
return err
|
||||
}
|
||||
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)
|
||||
jsonData, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
g.Log.Error("Invalid data result", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_result": responseContent, "_response_code": httpResponse.StatusCode})
|
||||
return "", err
|
||||
c.Logger.Error(c, "Error while converting to json;", "error", err.Error())
|
||||
return nil, 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, []byte(responseContent))
|
||||
if err != nil {
|
||||
g.Log.Error("Unable to cache the query", map[string]interface{}{"_query": queryHash, "_error": err.Error()})
|
||||
}
|
||||
}
|
||||
if c.cache.enabled {
|
||||
c.cache.client.Set(queryHash, jsonData)
|
||||
}
|
||||
|
||||
return
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
+259
-169
@@ -1,201 +1,291 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func (suite *TestSuite) Test_GraphQL_queryBuilder() {
|
||||
|
||||
func TestBaseClient_convertToJson(t *testing.T) {
|
||||
type fields struct {
|
||||
}
|
||||
type args struct {
|
||||
queryContent string
|
||||
queryVariables interface{}
|
||||
v any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []byte
|
||||
}{
|
||||
{
|
||||
name: "Test convertToJson",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
v: map[string]interface{}{
|
||||
"query": "query { hello }",
|
||||
},
|
||||
},
|
||||
want: []byte(`{"query":"query { hello }"}`),
|
||||
},
|
||||
{
|
||||
name: "Test convertToJson with variables",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
v: map[string]interface{}{
|
||||
"query": "query { hello }",
|
||||
"variables": map[string]interface{}{"name": "John"},
|
||||
},
|
||||
},
|
||||
want: []byte(`{"query":"query { hello }","variables":{"name":"John"}}`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &BaseClient{}
|
||||
if got := c.convertToJson(tt.args.v); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("BaseClient.convertToJson() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseClient_NewQuery(t *testing.T) {
|
||||
type fields struct {
|
||||
}
|
||||
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 {
|
||||
queryVariables interface{}
|
||||
queryHeaders map[string]interface{}
|
||||
queryContent string
|
||||
}
|
||||
tests := []struct {
|
||||
want any
|
||||
args args
|
||||
want []byte
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid query",
|
||||
args: args{
|
||||
queryContent: `query listUserBots { tbl_bots { bot_name } }`,
|
||||
queryVariables: nil,
|
||||
name: "Test Query - failing",
|
||||
fields: fields{
|
||||
graphql_endpoint: "https://spacex-production.up.railway.app/",
|
||||
},
|
||||
want: []byte(`{"query":"query listUserBots { tbl_bots { bot_name } }","variables":null}`),
|
||||
wantErr: false,
|
||||
args: args{
|
||||
queryContent: "query { hello }",
|
||||
},
|
||||
want: ``,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Valid query with variables",
|
||||
args: args{
|
||||
queryContent: `query listUserBots { tbl_bots { bot_name } }`,
|
||||
queryVariables: map[string]interface{}{"user_id": 1},
|
||||
name: "Test Query - success",
|
||||
fields: fields{
|
||||
graphql_endpoint: "https://spacex-production.up.railway.app/",
|
||||
},
|
||||
want: []byte(`{"query":"query listUserBots { tbl_bots { bot_name } }","variables":{"user_id":1}}`),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
q := NewConnection()
|
||||
got, gotErr := q.queryBuilder(tt.args.queryContent, tt.args.queryVariables)
|
||||
assert.Equal(t, tt.want, got)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, gotErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TestSuite) Test_GraphQL_Query() {
|
||||
if testing.Short() {
|
||||
suite.T().Skip("Skipping test in short / CI mode")
|
||||
}
|
||||
g := NewConnection()
|
||||
|
||||
type args struct {
|
||||
queryContent string
|
||||
queryVariables interface{}
|
||||
queryHeaders map[string]interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
isLocal bool
|
||||
cache_enabled bool
|
||||
args args
|
||||
wantResult string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid query, no cache",
|
||||
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
|
||||
}
|
||||
queryContent: `query Dragons {
|
||||
dragons {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
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 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,
|
||||
},
|
||||
{
|
||||
name: "Valid query to https endpoint",
|
||||
endpoint: "https://telegram-bot.app/v1/graphql",
|
||||
isLocal: 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 to github endpoint",
|
||||
endpoint: "https://api.github.com/graphql",
|
||||
isLocal: false,
|
||||
args: args{
|
||||
queryContent: `query {
|
||||
repository(name: "semver-generator", owner: "lukaszraczylo", followRenames: true) {
|
||||
releases(last: 2) {
|
||||
nodes {
|
||||
tag {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
queryVariables: nil,
|
||||
queryHeaders: map[string]interface{}{
|
||||
"Authorization": "Bearer " + os.Getenv("GITHUB_TOKEN"),
|
||||
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
wantResult: `{"repository":{"releases":{"nodes":[`,
|
||||
wantErr: false,
|
||||
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
|
||||
}
|
||||
}`,
|
||||
queryHeaders: map[string]interface{}{
|
||||
"x-apollo-operation-name": "Missions-Potato-Test-Golang-Cached",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
want: `{"dragons":[{"first_flight":"2010-12-08","name":"Dragon 1"},{"first_flight":"2019-03-02","name":"Dragon 2"}]}`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
// if tt.isLocal {
|
||||
// os.Setenv("GRAPHQL_ENDPOINT", tt.endpoint)
|
||||
// }
|
||||
g.Endpoint = tt.endpoint
|
||||
g.Cache = tt.cache_enabled
|
||||
gotResult, gotErr := g.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, gotErr)
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := NewConnection()
|
||||
|
||||
if tt.fields.graphql_endpoint != "" {
|
||||
c.endpoint = tt.fields.graphql_endpoint
|
||||
}
|
||||
|
||||
repeat := 1
|
||||
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 {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
err = retry.Do(
|
||||
func() error {
|
||||
httpResponse, err = c.client.Do(httpRequest)
|
||||
if err != nil {
|
||||
c.Logger.Error(c, "Error while executing http request;", "error", err.Error())
|
||||
return err
|
||||
}
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
if httpResponse.StatusCode <= 200 && httpResponse.StatusCode >= 204 {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(httpResponse.Body)
|
||||
if err != nil {
|
||||
c.Logger.Error(c, "Error while reading http response;", "error", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &queryResult)
|
||||
if err != nil {
|
||||
c.Logger.Error(c, "Error while unmarshalling http response;", "error", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
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
|
||||
force:
|
||||
major: 1
|
||||
minor: 0
|
||||
minor: 1
|
||||
patch: 0
|
||||
commit: 6a8f99f6ef8f65a303dad74c272ff12bf9fe526f
|
||||
wording:
|
||||
minor:
|
||||
- minor-release
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
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 retriesConfig struct {
|
||||
enabled bool
|
||||
max int
|
||||
delay int
|
||||
}
|
||||
|
||||
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
|
||||
retries retriesConfig
|
||||
}
|
||||
|
||||
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