mirror of
https://github.com/lukaszraczylo/go-simple-graphql.git
synced 2026-10-12 00:45:31 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1df2ae5d16
|
||
|
|
39f489210b
|
||
|
|
21c9119bcb
|
||
|
|
a7b385f0b7
|
||
|
|
7d3abf5464
|
||
|
|
7af4e1f96a
|
||
|
|
7623410bfe
|
||
|
|
8e26d79df7
|
||
|
|
aa25c43795
|
||
|
|
dfb082ad51
|
||
|
|
ce736958c6
|
||
|
|
4472041b21
|
||
|
|
b81e612f51
|
||
|
|
3c508eaf15
|
||
|
|
4d0c04cb26
|
||
|
|
55182f2f3d
|
||
|
|
841c136e71
|
||
|
|
833517f35c
|
||
|
|
07e4f75a0a
|
||
|
|
b8044c5d67
|
||
|
|
cf3076d701
|
||
|
|
5a222aa316
|
||
|
|
d843238d46
|
||
|
|
b9cd614ae4
|
||
|
|
eef4e39611
|
||
|
|
d764980390
|
||
|
|
689b1efc43
|
||
|
|
23f88a91d2
|
||
|
|
664a692c2d
|
||
|
|
2a628a7da7
|
||
|
|
e0913f8824
|
||
|
|
6a8f99f6ef
|
||
|
|
a5c4643a5e
|
||
|
|
42e1afbf77
|
||
|
|
9e8df7e297
|
||
|
|
405d159eb9
|
||
|
|
b3e594fc4f
|
||
|
|
32d9e0bd80
|
||
|
|
e706b07488
|
||
|
|
32091fc8cd
|
@@ -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,10 @@ Ps. It's Hasura friendly.
|
||||
- [Features](#features)
|
||||
- [Usage example](#usage-example)
|
||||
- [Environment variables](#environment-variables)
|
||||
- [Modifiers on the fly](#modifiers-on-the-fly)
|
||||
- [Cache](#cache)
|
||||
- [Example reader code](#example-reader-code)
|
||||
- [Tips](#tips)
|
||||
- [Working with results](#working-with-results)
|
||||
|
||||
## Reasoning
|
||||
@@ -30,12 +33,38 @@ 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: `string`, available: `byte`, `string`, `mapstring`
|
||||
* `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`
|
||||
|
||||
### Modifiers on the fly
|
||||
|
||||
* `gql.SetEndpoint('your-endpoint-url')` - modifies endpoint, without the need to set the environment variable
|
||||
* `gql.SetOutput('byte')` - modifies output format, without the need to set the environment variable
|
||||
|
||||
### Cache
|
||||
|
||||
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.
|
||||
* You can check the list of supported per-query modifiers below
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
// following values passed as headers will modify behaviour of the query
|
||||
// and disregard settings provided via environment variables
|
||||
headers := map[string]interface{}{
|
||||
...
|
||||
"gqlcache": true, // sets the cache as on for this query only
|
||||
"gqlretries": false, // disables retries for this query only
|
||||
}
|
||||
```
|
||||
|
||||
### Example reader code
|
||||
|
||||
@@ -46,32 +75,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 +116,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,124 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/akyoto/cache"
|
||||
"github.com/lukaszraczylo/go-simple-graphql/utils/logger"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/envutil"
|
||||
)
|
||||
|
||||
func NewConnection() *BaseClient {
|
||||
|
||||
b := &BaseClient{
|
||||
client: &http.Client{},
|
||||
MaxGoRoutines: -1,
|
||||
LoggingLevel: logger.Warn,
|
||||
LoggerColorful: true,
|
||||
}
|
||||
|
||||
if b.LoggerWriter == nil {
|
||||
b.LoggerWriter = log.New(os.Stdout, "", log.LstdFlags)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
b.enableCache()
|
||||
|
||||
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
|
||||
|
||||
httpTransport := &http.Transport{
|
||||
MaxIdleConns: 10,
|
||||
IdleConnTimeout: 15 * time.Second,
|
||||
ResponseHeaderTimeout: 15 * time.Second,
|
||||
DisableKeepAlives: false,
|
||||
}
|
||||
|
||||
if strings.HasPrefix(b.endpoint, "http://") {
|
||||
httpClient = &http.Client{
|
||||
Transport: httpTransport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
} else {
|
||||
tlsClientConfig := &tls.Config{}
|
||||
if strings.HasPrefix(b.endpoint, "https://") {
|
||||
tlsClientConfig.InsecureSkipVerify = true
|
||||
}
|
||||
http2Transport := &http2.Transport{
|
||||
AllowHTTP: true,
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
ReadIdleTimeout: 15 * time.Second,
|
||||
PingTimeout: 15 * time.Second,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (b *BaseClient) SetEndpoint(endpoint string) {
|
||||
b.endpoint = endpoint
|
||||
}
|
||||
|
||||
func (b *BaseClient) SetOutput(output string) {
|
||||
b.responseType = output
|
||||
}
|
||||
|
||||
func (b *BaseClient) enableCache() {
|
||||
var err error
|
||||
b.cache.client = cache.New(time.Duration(b.cache.ttl) * time.Second * 2)
|
||||
if err != nil {
|
||||
fmt.Println(">> Error while creating cache client;", "error", err.Error())
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseClient) disableCache() {
|
||||
b.Logger.Debug(b, "Disabling cache")
|
||||
b.cache.enabled = false
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
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
|
||||
cache bool
|
||||
endpoint string
|
||||
output string
|
||||
name string
|
||||
}{
|
||||
{
|
||||
name: "Test NewConnection https",
|
||||
endpoint: "https://api.github.com/graphql",
|
||||
output: "string",
|
||||
cache: true,
|
||||
want: &BaseClient{
|
||||
endpoint: "https://api.github.com/graphql",
|
||||
responseType: "string",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test NewConnection http",
|
||||
endpoint: "http://api.github.com/graphql",
|
||||
output: "mapstring",
|
||||
cache: false,
|
||||
want: &BaseClient{
|
||||
endpoint: "http://api.github.com/graphql",
|
||||
responseType: "mapstring",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
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", tt.endpoint)
|
||||
got := NewConnection()
|
||||
got.SetEndpoint(tt.endpoint)
|
||||
got.SetOutput(tt.output)
|
||||
if tt.cache {
|
||||
got.enableCache()
|
||||
} else {
|
||||
got.disableCache()
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want.endpoint, got.endpoint)
|
||||
assert.Equal(t, tt.want.responseType, got.responseType)
|
||||
assert.Equal(t, tt.cache, got.cache.enabled)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseClient_SetEndpoint(t *testing.T) {
|
||||
type fields struct {
|
||||
}
|
||||
type args struct {
|
||||
endpoint string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Test SetEndpoint",
|
||||
args: args{
|
||||
endpoint: "https://potato/graphql",
|
||||
},
|
||||
want: "https://potato/graphql",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b := NewConnection()
|
||||
b.SetEndpoint(tt.args.endpoint)
|
||||
assert.Equal(t, tt.want, b.endpoint)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package gql
|
||||
|
||||
func (c *BaseClient) cacheLookup(hash string) []byte {
|
||||
if c.cache.client != nil {
|
||||
obj, found := c.cache.client.Get(hash)
|
||||
if found {
|
||||
c.Logger.Debug(c, "Cache hit;", "hash", hash)
|
||||
return obj.([]byte)
|
||||
}
|
||||
// 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,31 +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/akyoto/cache v1.0.6
|
||||
github.com/avast/retry-go/v4 v4.5.0
|
||||
github.com/gookit/goutil v0.6.12
|
||||
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.4
|
||||
github.com/stretchr/testify v1.7.1
|
||||
golang.org/x/net v0.4.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/net v0.17.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.4 // 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.4.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/term v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,62 +1,55 @@
|
||||
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/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/akyoto/cache v1.0.6 h1:5XGVVYoi2i+DZLLPuVIXtsNIJ/qaAM16XT0LaBaXd2k=
|
||||
github.com/akyoto/cache v1.0.6/go.mod h1:WfxTRqKhfgAG71Xh6E3WLpjhBtZI37O53G4h5s+3iM4=
|
||||
github.com/avast/retry-go/v4 v4.5.0 h1:QoRAZZ90cj5oni2Lsgl2GW8mNTnUCnmpx/iKpwVisHg=
|
||||
github.com/avast/retry-go/v4 v4.5.0/go.mod h1:7hLEXp0oku2Nir2xBAsg0PTphp9z71bN5Aq1fboC3+I=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/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.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/gookit/goutil v0.6.12 h1:73vPUcTtVGXbhSzBOFcnSB1aJl7Jq9np3RAE50yIDZc=
|
||||
github.com/gookit/goutil v0.6.12/go.mod h1:g6krlFib8xSe3G1h02IETowOtrUGpAmetT8IevDpvpM=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/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.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec=
|
||||
github.com/sethvargo/go-retry v0.2.4/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/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.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/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/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.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"
|
||||
|
||||
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 {
|
||||
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,72 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
stdlog "log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type LogConfig struct {
|
||||
DefaultOutput *os.File
|
||||
Logger zerolog.Logger
|
||||
}
|
||||
|
||||
func NewLogger() *LogConfig {
|
||||
log := new(LogConfig)
|
||||
dll := os.Getenv("LOG_LEVEL")
|
||||
if dll == "debug" {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
} else if dll == "warn" {
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
} else if dll == "error" {
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
} else {
|
||||
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) Log(level zerolog.Level, message string, v ...map[string]interface{}) {
|
||||
contextLog := l.Logger.WithLevel(level)
|
||||
if len(v) > 0 {
|
||||
for _, m := range v {
|
||||
for k, v := range m {
|
||||
if v != nil && k != "" {
|
||||
contextLog.Str(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// contextLog.Str("_stack_trace", pandati.Trace())
|
||||
contextLog.Msg(message)
|
||||
}
|
||||
|
||||
func (l *LogConfig) Critical(message string, v ...map[string]interface{}) {
|
||||
l.Log(zerolog.FatalLevel, message, v...)
|
||||
}
|
||||
|
||||
func (l *LogConfig) Error(message string, v ...map[string]interface{}) {
|
||||
l.Log(zerolog.ErrorLevel, message, v...)
|
||||
}
|
||||
|
||||
func (l *LogConfig) Warning(message string, v ...map[string]interface{}) {
|
||||
l.Log(zerolog.WarnLevel, message, v...)
|
||||
}
|
||||
|
||||
func (l *LogConfig) Info(message string, v ...map[string]interface{}) {
|
||||
l.Log(zerolog.InfoLevel, message, v...)
|
||||
}
|
||||
|
||||
func (l *LogConfig) Debug(message string, v ...map[string]interface{}) {
|
||||
l.Log(zerolog.DebugLevel, message, v...)
|
||||
}
|
||||
@@ -1,134 +1,152 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
retry "github.com/avast/retry-go"
|
||||
"github.com/lukaszraczylo/pandati"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
"github.com/lukaszraczylo/go-simple-graphql/utils/helpers"
|
||||
|
||||
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
|
||||
}
|
||||
} else {
|
||||
g.Log.Debug("Unable to find the query in the cache", map[string]interface{}{"_query": queryHash, "_error": err.Error()})
|
||||
}
|
||||
}
|
||||
|
||||
httpRequest, err := http.NewRequest("POST", g.Endpoint, bytes.NewBuffer(query))
|
||||
// httpRequest.Header.Set("Content-Type", "application/json")
|
||||
if err != nil {
|
||||
g.Log.Error("Unable to create the request", map[string]interface{}{"_error": err.Error()})
|
||||
return "", err
|
||||
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()
|
||||
}
|
||||
|
||||
for header, value := range queryHeaders {
|
||||
httpRequest.Header.Add(fmt.Sprintf("%v", header), fmt.Sprintf("%v", value))
|
||||
if c.validate {
|
||||
c.Logger.Warn(c, "Validating query is not active")
|
||||
}
|
||||
|
||||
var httpResponse *http.Response
|
||||
query.query = ""
|
||||
c.Logger.Debug(c, "Query prepared;", "query", helpers.BytesToString(query.compiledQuery))
|
||||
|
||||
err = retry.Do(
|
||||
func() error {
|
||||
httpResponse, err = g.HttpClient.Do(httpRequest)
|
||||
return query
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
g.Log.Debug("Unable to send the query RETRY", map[string]interface{}{"_error": err.Error()})
|
||||
return err
|
||||
}
|
||||
defer httpResponse.Body.Close()
|
||||
// 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
|
||||
|
||||
if httpResponse.StatusCode <= 200 && httpResponse.StatusCode >= 204 {
|
||||
return err
|
||||
}
|
||||
func (c *BaseClient) Query(queryContent string, queryVariables interface{}, queryHeaders map[string]interface{}) (any, error) {
|
||||
|
||||
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
|
||||
}
|
||||
compiledQuery := c.NewQuery(queryContent, queryVariables)
|
||||
parseQueryHeaders, enabledCache, headersModified, enabledRetries := compiledQuery.parseQueryHeaders(queryHeaders)
|
||||
|
||||
err = json.Unmarshal(body, &queryResult)
|
||||
if err != nil {
|
||||
g.Log.Debug("Unable to unmarshal the query", map[string]interface{}{"_error": err.Error()})
|
||||
return err
|
||||
}
|
||||
var should_cache, retries_enabled bool
|
||||
var queryHash string
|
||||
|
||||
if headersModified {
|
||||
should_cache = enabledCache
|
||||
retries_enabled = enabledRetries
|
||||
} else {
|
||||
should_cache = c.cache.enabled
|
||||
retries_enabled = c.retries.enabled
|
||||
}
|
||||
|
||||
// if queryContent does not start with `query` then we don't want to cache it
|
||||
// because it's probably a mutation or subscription.
|
||||
if !strutil.HasPrefix(queryContent, "query") {
|
||||
should_cache = false
|
||||
}
|
||||
|
||||
if should_cache {
|
||||
queryHash = strutil.Md5(fmt.Sprintf("%s-%+v", compiledQuery.compiledQuery, queryHeaders))
|
||||
}
|
||||
|
||||
q := &queryExecutor{
|
||||
client: c,
|
||||
query: compiledQuery.compiledQuery,
|
||||
headers: parseQueryHeaders,
|
||||
context: context.Background(),
|
||||
should_cache: should_cache,
|
||||
retries_enabled: retries_enabled,
|
||||
hash: queryHash,
|
||||
}
|
||||
|
||||
q.execute()
|
||||
defer q.done()
|
||||
|
||||
return q.result.data, q.result.errors
|
||||
}
|
||||
|
||||
func (c *BaseClient) decodeResponse(jsonData []byte) any {
|
||||
switch c.responseType {
|
||||
case "mapstring":
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(jsonData, &response)
|
||||
if err != nil {
|
||||
c.Logger.Error(c, "Error while converting to map[string]interface{};", "error", err.Error())
|
||||
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, []byte(responseContent))
|
||||
if err != nil {
|
||||
g.Log.Error("Unable to cache the query", map[string]interface{}{"_query": queryHash, "_error": err.Error()})
|
||||
}
|
||||
}
|
||||
return response
|
||||
case "string":
|
||||
return helpers.BytesToString(jsonData)
|
||||
case "byte":
|
||||
return jsonData
|
||||
default:
|
||||
c.Logger.Error(c, "Unknown response type", "response", c.responseType)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
func (q *Query) parseQueryHeaders(queryHeaders map[string]interface{}) (returnHeaders map[string]interface{}, cache_enabled bool, headers_modified bool, retries_enabled bool) {
|
||||
returnHeaders = make(map[string]interface{})
|
||||
var err error
|
||||
|
||||
for k, v := range queryHeaders {
|
||||
if k == "gqlcache" {
|
||||
cache_enabled, err = strconv.ParseBool(fmt.Sprintf("%v", v))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Unable to parse gqlcache value %s", err.Error()))
|
||||
}
|
||||
headers_modified = true
|
||||
continue
|
||||
}
|
||||
if k == "gqlretries" {
|
||||
retries_enabled, _ = strconv.ParseBool(fmt.Sprintf("%v", v))
|
||||
continue
|
||||
}
|
||||
returnHeaders[k] = v
|
||||
}
|
||||
return returnHeaders, cache_enabled, headers_modified, retries_enabled
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
+538
-187
@@ -1,219 +1,570 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"math/rand"
|
||||
"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"}}`),
|
||||
},
|
||||
{
|
||||
name: "Test NewQuery with modifier",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
v: map[string]interface{}{
|
||||
"query": "query @cached(ttl: 120) { hello }",
|
||||
"variables": map[string]interface{}{"name": "John"},
|
||||
},
|
||||
},
|
||||
want: []byte(`{"query":"query @cached(ttl: 120) { hello }","variables":{"name":"John"}}`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
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 }"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test NewQuery with modifier",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
q: []any{
|
||||
"query @cached(ttl: 120) { hello }",
|
||||
map[string]interface{}{"name": "John"},
|
||||
},
|
||||
},
|
||||
want: &Query{
|
||||
compiledQuery: []byte(`{"variables":{"name":"John"},"query":"query @cached(ttl: 120) { hello }"}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
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"}}`),
|
||||
},
|
||||
{
|
||||
name: "Test decodeResponse - mapstring",
|
||||
fields: fields{
|
||||
responseType: "mapstring",
|
||||
},
|
||||
args: args{
|
||||
jsonData: []byte(`{"data":{"hello":"world"}}`),
|
||||
},
|
||||
want: map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"hello": "world",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
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, 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 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseClient_parseQueryHeaders(t *testing.T) {
|
||||
type fields struct {
|
||||
}
|
||||
type args struct {
|
||||
queryHeaders map[string]interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
fields fields
|
||||
args args
|
||||
wantReturnHeaders map[string]interface{}
|
||||
name string
|
||||
}{
|
||||
{
|
||||
name: "Test parseQueryHeaders - no change",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
queryHeaders: map[string]interface{}{
|
||||
"x-test-header": "test",
|
||||
"x-test-header-next": true,
|
||||
},
|
||||
},
|
||||
wantReturnHeaders: map[string]interface{}{
|
||||
"x-test-header": "test",
|
||||
"x-test-header-next": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test parseQueryHeaders - lib-headers removed",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
queryHeaders: map[string]interface{}{
|
||||
"x-test-header": "test",
|
||||
"gqlcache": true,
|
||||
},
|
||||
},
|
||||
wantReturnHeaders: map[string]interface{}{
|
||||
"x-test-header": "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
q := &Query{}
|
||||
if gotReturnHeaders, _, _, _ := q.parseQueryHeaders(tt.args.queryHeaders); !reflect.DeepEqual(gotReturnHeaders, tt.wantReturnHeaders) {
|
||||
t.Errorf("BaseClient.parseQueryHeaders() = %v, want %v", gotReturnHeaders, tt.wantReturnHeaders)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseClient_QueryCache(t *testing.T) {
|
||||
type fields struct {
|
||||
graphql_endpoint string
|
||||
cache_enabled bool
|
||||
}
|
||||
type args struct {
|
||||
queryVariables interface{}
|
||||
queryHeaders map[string]interface{}
|
||||
queryContent string
|
||||
}
|
||||
tests := []struct {
|
||||
want any
|
||||
args args
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test QueryCache - enabled",
|
||||
fields: fields{
|
||||
graphql_endpoint: "https://spacex-production.up.railway.app/",
|
||||
cache_enabled: true,
|
||||
},
|
||||
args: args{
|
||||
queryContent: `query Dragons {
|
||||
dragons {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
queryHeaders: map[string]interface{}{
|
||||
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test QueryCache - disabled",
|
||||
fields: fields{
|
||||
graphql_endpoint: "https://spacex-production.up.railway.app/",
|
||||
cache_enabled: false,
|
||||
},
|
||||
args: args{
|
||||
queryContent: `query Dragons {
|
||||
dragons {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
queryHeaders: map[string]interface{}{
|
||||
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := NewConnection()
|
||||
c.cache.enabled = tt.fields.cache_enabled
|
||||
|
||||
if tt.fields.graphql_endpoint != "" {
|
||||
c.endpoint = tt.fields.graphql_endpoint
|
||||
}
|
||||
|
||||
_, err := c.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
|
||||
if !tt.wantErr {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseClient_QueryCacheRandomizedRace(t *testing.T) {
|
||||
type fields struct {
|
||||
graphql_endpoint string
|
||||
cache_enabled bool
|
||||
}
|
||||
type args struct {
|
||||
queryVariables interface{}
|
||||
queryHeaders map[string]interface{}
|
||||
queryContent string
|
||||
}
|
||||
tests := []struct {
|
||||
want any
|
||||
args args
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test QueryCache - enabled",
|
||||
fields: fields{
|
||||
graphql_endpoint: "https://spacex-production.up.railway.app/",
|
||||
},
|
||||
args: args{
|
||||
queryContent: `query Dragons {
|
||||
dragons {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
queryHeaders: map[string]interface{}{
|
||||
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := NewConnection()
|
||||
if tt.fields.graphql_endpoint != "" {
|
||||
c.endpoint = tt.fields.graphql_endpoint
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
c.cache.enabled = rand.Intn(2) == 0
|
||||
go func() {
|
||||
_, err := c.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
|
||||
if !tt.wantErr {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseClient_QueryCacheRandomizedRaceViaHeader(t *testing.T) {
|
||||
type fields struct {
|
||||
graphql_endpoint string
|
||||
cache_enabled bool
|
||||
}
|
||||
type args struct {
|
||||
queryVariables interface{}
|
||||
queryHeaders map[string]interface{}
|
||||
queryContent string
|
||||
}
|
||||
tests := []struct {
|
||||
want any
|
||||
args args
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test QueryCache - enabled",
|
||||
fields: fields{
|
||||
graphql_endpoint: "https://spacex-production.up.railway.app/",
|
||||
},
|
||||
args: args{
|
||||
queryContent: `query Dragons {
|
||||
dragons {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
queryHeaders: map[string]interface{}{
|
||||
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := NewConnection()
|
||||
if tt.fields.graphql_endpoint != "" {
|
||||
c.endpoint = tt.fields.graphql_endpoint
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go func() {
|
||||
tt.args.queryHeaders["gqlcache"] = rand.Intn(2) == 0
|
||||
_, err := c.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
|
||||
if !tt.wantErr {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
assert.Contains(t, gotResult, tt.wantResult)
|
||||
})
|
||||
}
|
||||
fmt.Println(g.CacheStore.Capacity())
|
||||
}
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"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, fmt.Sprintf("%s", value))
|
||||
}
|
||||
|
||||
var retries_available = c.retries.max
|
||||
if !c.retries.enabled {
|
||||
retries_available = 1
|
||||
}
|
||||
|
||||
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 io.Copy(io.Discard, httpResponse.Body) // equivalent to `cp body /dev/null`
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
if httpResponse.StatusCode <= 200 && httpResponse.StatusCode >= 204 {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := io.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
|
||||
},
|
||||
retry.OnRetry(func(n uint, err error) {
|
||||
c.Logger.Error(c, "Retrying query", "error", err.Error())
|
||||
}),
|
||||
retry.Attempts(uint(retries_available)),
|
||||
retry.DelayType(retry.BackOffDelay),
|
||||
retry.Delay(time.Duration(c.retries.delay)*time.Second),
|
||||
retry.LastErrorOnly(true),
|
||||
)
|
||||
|
||||
if len(queryResult.Errors) > 0 {
|
||||
return nil, fmt.Errorf("%v", queryResult.Errors)
|
||||
}
|
||||
return queryResult.Data, nil
|
||||
}
|
||||
|
||||
func (q *queryExecutor) execute() {
|
||||
if q.should_cache {
|
||||
cachedResponse := q.client.cacheLookup(q.hash)
|
||||
if cachedResponse != nil {
|
||||
q.client.Logger.Debug(q.client, "Found cached response")
|
||||
q.result.data = q.client.decodeResponse(cachedResponse)
|
||||
return // return cached response
|
||||
} else {
|
||||
q.client.Logger.Debug(q.client, "No cached response found")
|
||||
}
|
||||
}
|
||||
|
||||
if q.retries_enabled {
|
||||
q.client.Logger.Debug(q.client, "Retries enabled")
|
||||
q.client.retries.enabled = true
|
||||
}
|
||||
|
||||
response, err := q.client.executeQuery(q.query, q.headers)
|
||||
if err != nil {
|
||||
q.client.Logger.Error(q.client, "Error while executing query;", "error", err.Error())
|
||||
q.result.errors = err
|
||||
return
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
q.client.Logger.Error(q.client, "Error while converting to json;", "error", err.Error())
|
||||
q.result.errors = err
|
||||
return
|
||||
}
|
||||
|
||||
if q.should_cache {
|
||||
q.client.cache.client.Set(q.hash, jsonData, time.Duration(q.client.cache.ttl)*time.Second)
|
||||
}
|
||||
|
||||
q.result.data = q.client.decodeResponse(jsonData)
|
||||
}
|
||||
|
||||
func (q *queryExecutor) done() {
|
||||
q = nil
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/akyoto/cache"
|
||||
"github.com/lukaszraczylo/go-simple-graphql/utils/logger"
|
||||
)
|
||||
|
||||
type cacheStore struct {
|
||||
client *cache.Cache
|
||||
ttl int
|
||||
enabled bool
|
||||
}
|
||||
|
||||
type retriesConfig struct {
|
||||
enabled bool
|
||||
max int
|
||||
delay int
|
||||
}
|
||||
|
||||
type BaseClient struct {
|
||||
Logger Logger
|
||||
LoggerWriter Writer
|
||||
client *http.Client
|
||||
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 queryExecutor struct {
|
||||
result queryExecutorResult
|
||||
context context.Context
|
||||
client *BaseClient
|
||||
headers map[string]interface{}
|
||||
hash string
|
||||
query []byte
|
||||
should_cache bool
|
||||
retries_enabled bool
|
||||
}
|
||||
|
||||
type queryExecutorResult struct {
|
||||
data interface{}
|
||||
errors error
|
||||
}
|
||||
|
||||
type request struct {
|
||||
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,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,66 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
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 interface{}) ([]byte, error) {
|
||||
b, err := json.Marshal(internalSerialize(v))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func internalSerialize(v interface{}) interface{} {
|
||||
if v != nil {
|
||||
switch reflect.TypeOf(v).Kind() {
|
||||
case reflect.Struct:
|
||||
r := make(map[string]interface{})
|
||||
name := reflect.TypeOf(v).Name()
|
||||
if name != "" {
|
||||
r["_"] = reflect.TypeOf(v).String()
|
||||
}
|
||||
for i := 0; i < reflect.ValueOf(v).NumField(); i++ {
|
||||
r[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 []interface{}
|
||||
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,121 @@
|
||||
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 {
|
||||
if v == nil {
|
||||
e.buf.WriteString("null")
|
||||
return nil
|
||||
}
|
||||
|
||||
switch val := v.(type) {
|
||||
case *OrderedMap:
|
||||
data, err := json.Marshal(val.values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.buf.Write(data)
|
||||
default:
|
||||
data, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.buf.Write(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o OrderedMap) Keys() []string {
|
||||
return o.keys
|
||||
}
|
||||
|
||||
func (o OrderedMap) Values() []interface{} {
|
||||
var values []interface{}
|
||||
for _, k := range o.keys {
|
||||
values = append(values, o.values[k])
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func (o *OrderedMap) Delete(key string) {
|
||||
delete(o.values, key)
|
||||
for i, k := range o.keys {
|
||||
if k == key {
|
||||
o.keys = append(o.keys[:i], o.keys[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o OrderedMap) Exists(key string) bool {
|
||||
_, exists := o.values[key]
|
||||
return exists
|
||||
}
|
||||
|
||||
func (o *OrderedMap) UnmarshalJSON(data []byte) error {
|
||||
var values map[string]interface{}
|
||||
err := json.Unmarshal(data, &values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.keys = []string{}
|
||||
o.values = map[string]interface{}{}
|
||||
for k, v := range values {
|
||||
o.Set(k, v)
|
||||
}
|
||||
|
||||
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