Compare commits

...
4 Commits
12 changed files with 175 additions and 229 deletions
+23 -45
View File
@@ -27,16 +27,9 @@ func (qe *QueryExecutor) executeQuery() ([]byte, error) {
defer bufferPool.Put(buf) defer bufferPool.Put(buf)
buf.Reset() buf.Reset()
_, err := buf.Write(qe.Query) buf.Write(qe.Query)
if err != nil {
qe.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't write to buffer",
Pairs: map[string]interface{}{"error": err.Error()},
})
return nil, err
}
httpRequest, err := http.NewRequest(http.MethodPost, qe.endpoint, buf) httpRequest, err := http.NewRequest(http.MethodPost, qe.endpoint, nil)
if err != nil { if err != nil {
qe.Logger.Error(&libpack_logger.LogMessage{ qe.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't create HTTP request", Message: "Can't create HTTP request",
@@ -48,78 +41,62 @@ func (qe *QueryExecutor) executeQuery() ([]byte, error) {
for key, value := range qe.Headers { for key, value := range qe.Headers {
httpRequest.Header.Set(key, fmt.Sprint(value)) httpRequest.Header.Set(key, fmt.Sprint(value))
} }
// Set Content-Type header if not already set
if httpRequest.Header.Get("Content-Type") == "" {
httpRequest.Header.Set("Content-Type", "application/json")
}
retriesMax := 1 retriesMax := 1
if qe.Retries { if qe.Retries {
qe.Logger.Debug(&libpack_logger.LogMessage{
Message: "Retries enabled",
Pairs: map[string]interface{}{"retries": qe.retries_number},
})
retriesMax = qe.retries_number retriesMax = qe.retries_number
} else {
qe.Logger.Debug(&libpack_logger.LogMessage{
Message: "Retries disabled",
Pairs: map[string]interface{}{"retries": 1},
})
} }
var queryResult queryResults var queryResult queryResults
err = retry.Do( err = retry.Do(
func() error { func() error {
// Reset buffer before each retry
buf.Reset()
buf.Write(qe.Query)
// Set the body of the request
httpRequest.Body = io.NopCloser(bytes.NewReader(buf.Bytes()))
httpRequest.ContentLength = int64(buf.Len())
httpResponse, err := qe.client.Do(httpRequest) httpResponse, err := qe.client.Do(httpRequest)
if err != nil { if err != nil {
qe.Logger.Debug(&libpack_logger.LogMessage{
Message: "Error executing HTTP request",
Pairs: map[string]interface{}{"error": err.Error()},
})
return err return err
} }
defer func() { defer func() {
_, err := io.Copy(io.Discard, httpResponse.Body) io.Copy(io.Discard, httpResponse.Body)
if err != nil {
qe.Logger.Debug(&libpack_logger.LogMessage{
Message: "Error discarding HTTP response body",
Pairs: map[string]interface{}{"error": err.Error()},
})
}
httpResponse.Body.Close() httpResponse.Body.Close()
}() }()
if httpResponse.StatusCode < http.StatusOK || httpResponse.StatusCode >= http.StatusNoContent { if httpResponse.StatusCode < http.StatusOK || httpResponse.StatusCode >= http.StatusMultipleChoices {
return fmt.Errorf("HTTP error - status code: %s for %s", httpResponse.Status, httpRequest.URL) return fmt.Errorf("HTTP error - status code: %s for %s", httpResponse.Status, httpRequest.URL)
} }
var reader io.ReadCloser var reader io.Reader
encoding := httpResponse.Header.Get("Content-Encoding") encoding := httpResponse.Header.Get("Content-Encoding")
if encoding == "gzip" { if encoding == "gzip" {
reader, err = gzip.NewReader(httpResponse.Body) gzipReader, err := gzip.NewReader(httpResponse.Body)
if err != nil { if err != nil {
qe.Logger.Debug(&libpack_logger.LogMessage{
Message: "Error creating gzip reader",
Pairs: map[string]interface{}{"error": err.Error()},
})
return fmt.Errorf("error creating gzip reader: %w", err) return fmt.Errorf("error creating gzip reader: %w", err)
} }
defer reader.Close() defer gzipReader.Close()
reader = gzipReader
} else { } else {
reader = httpResponse.Body reader = httpResponse.Body
} }
// Read all response body
body, err := io.ReadAll(reader) body, err := io.ReadAll(reader)
if err != nil { if err != nil {
qe.Logger.Debug(&libpack_logger.LogMessage{
Message: "Error reading HTTP response",
Pairs: map[string]interface{}{"error": err.Error()},
})
return fmt.Errorf("error reading HTTP response: %w", err) return fmt.Errorf("error reading HTTP response: %w", err)
} }
// Unmarshal response
err = json.Unmarshal(body, &queryResult) err = json.Unmarshal(body, &queryResult)
if err != nil { if err != nil {
qe.Logger.Debug(&libpack_logger.LogMessage{
Message: "Error unmarshalling HTTP response",
Pairs: map[string]interface{}{"error": err.Error()},
})
return fmt.Errorf("error unmarshalling HTTP response: %w", err) return fmt.Errorf("error unmarshalling HTTP response: %w", err)
} }
@@ -160,6 +137,7 @@ func (qe *QueryExecutor) executeQuery() ([]byte, error) {
return nil, errors.New("error executing query: no data") return nil, errors.New("error executing query: no data")
} }
// Marshal queryResult.Data
jsonData, err := json.Marshal(queryResult.Data) jsonData, err := json.Marshal(queryResult.Data)
if err != nil { if err != nil {
qe.Logger.Debug(&libpack_logger.LogMessage{ qe.Logger.Debug(&libpack_logger.LogMessage{
+1
View File
@@ -5,6 +5,7 @@ go 1.21
require ( require (
github.com/avast/retry-go/v4 v4.6.0 github.com/avast/retry-go/v4 v4.6.0
github.com/goccy/go-json v0.10.3 github.com/goccy/go-json v0.10.3
github.com/goccy/go-reflect v1.2.0
github.com/gookit/goutil v0.6.16 github.com/gookit/goutil v0.6.16
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
golang.org/x/net v0.29.0 golang.org/x/net v0.29.0
+2
View File
@@ -4,6 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-reflect v1.2.0 h1:O0T8rZCuNmGXewnATuKYnkL0xm6o8UNOJZd/gOkb9ms=
github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gookit/goutil v0.6.16 h1:9fRMCF4X9abdRD5+2HhBS/GwafjBlTUBjRtA5dgkvuw= github.com/gookit/goutil v0.6.16 h1:9fRMCF4X9abdRD5+2HhBS/GwafjBlTUBjRtA5dgkvuw=
+2 -2
View File
@@ -9,7 +9,7 @@ import (
libpack_logger "github.com/lukaszraczylo/go-simple-graphql/logging" libpack_logger "github.com/lukaszraczylo/go-simple-graphql/logging"
) )
func searchForKeysInMapStringInterface(msi map[string]interface{}, key string) (value any) { func searchForKeysInMapStringInterface(msi map[string]interface{}, key string) interface{} {
if msi == nil { if msi == nil {
return nil return nil
} }
@@ -17,7 +17,7 @@ func searchForKeysInMapStringInterface(msi map[string]interface{}, key string) (
} }
func calculateHash(query *Query) string { func calculateHash(query *Query) string {
hash := md5.Sum([]byte(query.JsonQuery)) hash := md5.Sum(query.JsonQuery)
return hex.EncodeToString(hash[:]) return hex.EncodeToString(hash[:])
} }
+2 -1
View File
@@ -1,8 +1,9 @@
package gql package gql
import ( import (
"reflect"
"testing" "testing"
"github.com/goccy/go-reflect"
) )
func (suite *Tests) Test_searchForKeysInMapStringInterface() { func (suite *Tests) Test_searchForKeysInMapStringInterface() {
+1 -1
View File
@@ -4,11 +4,11 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"os" "os"
"reflect"
"testing" "testing"
"time" "time"
"github.com/goccy/go-json" "github.com/goccy/go-json"
"github.com/goccy/go-reflect"
) )
func captureStderr(f func()) string { func captureStderr(f func()) string {
+5
View File
@@ -1,6 +1,7 @@
package gql package gql
import ( import (
"net/http"
"time" "time"
"github.com/gookit/goutil/envutil" "github.com/gookit/goutil/envutil"
@@ -38,3 +39,7 @@ func (b *BaseClient) SetOutput(responseType string) {
// TODO: implement // TODO: implement
b.responseType = responseType b.responseType = responseType
} }
func (b *BaseClient) SetHTTPClient(client *http.Client) {
b.client = client
}
+67
View File
@@ -0,0 +1,67 @@
// mock_server.go
package gql
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"golang.org/x/net/http2"
)
func StartMockServer() *httptest.Server {
// Create your handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Your existing handler code
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
var req struct {
Variables map[string]interface{} `json:"variables"`
Query string `json:"query"`
}
err = json.Unmarshal(bodyBytes, &req)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
// Simple validation to check for unbalanced braces as a syntax error
if !strings.HasPrefix(strings.TrimSpace(req.Query), "query") || !strings.HasSuffix(strings.TrimSpace(req.Query), "}") {
resp := []byte(`{"errors":[{"message":"Syntax Error: Invalid query"}]}`)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
w.Write(resp)
return
}
var resp []byte
if strings.Contains(req.Query, "viewer") {
resp = []byte(`{"data":{"viewer":{"login":"mockuser"}}}`)
} else if strings.Contains(req.Query, "dragons") {
resp = []byte(`{"data":{"dragons":[{"name":"Mock Dragon 1"},{"name":"Mock Dragon 2"}]}}`)
} else {
resp = []byte(`{"errors":[{"message":"Unknown query"}]}`)
w.WriteHeader(http.StatusBadRequest)
}
w.Header().Set("Content-Type", "application/json")
w.Write(resp)
})
// Create an unstarted server
server := httptest.NewUnstartedServer(handler)
// Configure the server to support HTTP/2
http2.ConfigureServer(server.Config, &http2.Server{})
server.TLS = server.Config.TLSConfig
// Start the server with TLS
server.StartTLS()
return server
}
+16 -21
View File
@@ -1,8 +1,8 @@
package gql package gql
import ( import (
"bytes"
"fmt" "fmt"
"sync"
"github.com/goccy/go-json" "github.com/goccy/go-json"
"github.com/gookit/goutil" "github.com/gookit/goutil"
@@ -10,17 +10,13 @@ import (
libpack_logger "github.com/lukaszraczylo/go-simple-graphql/logging" libpack_logger "github.com/lukaszraczylo/go-simple-graphql/logging"
) )
var jsonBufferPool = sync.Pool{
New: func() interface{} {
return new([]byte)
},
}
func (b *BaseClient) convertToJSON(v any) []byte { func (b *BaseClient) convertToJSON(v any) []byte {
jsonBuffer := jsonBufferPool.Get().(*[]byte) // Reuse buffer to reduce allocations
defer jsonBufferPool.Put(jsonBuffer) buf := bufferPool.Get().(*bytes.Buffer)
defer bufferPool.Put(buf)
buf.Reset()
*jsonBuffer = (*jsonBuffer)[:0] // Use json.Marshal to avoid adding extra newline
jsonData, err := json.Marshal(v) jsonData, err := json.Marshal(v)
if err != nil { if err != nil {
b.Logger.Error(&libpack_logger.LogMessage{ b.Logger.Error(&libpack_logger.LogMessage{
@@ -29,8 +25,14 @@ func (b *BaseClient) convertToJSON(v any) []byte {
}) })
return nil return nil
} }
*jsonBuffer = append(*jsonBuffer, jsonData...)
return *jsonBuffer // Copy the bytes to the buffer
buf.Write(jsonData)
// Copy the bytes to a new slice before returning
result := make([]byte, buf.Len())
copy(result, buf.Bytes())
return result
} }
func (b *BaseClient) compileQuery(queryPartials ...any) *Query { func (b *BaseClient) compileQuery(queryPartials ...any) *Query {
@@ -92,7 +94,7 @@ func (b *BaseClient) Query(query string, variables map[string]interface{}, heade
Message: "Cache hit", Message: "Cache hit",
Pairs: map[string]interface{}{"query": compiledQuery}, Pairs: map[string]interface{}{"query": compiledQuery},
}) })
return cachedValue, nil return b.decodeResponse(cachedValue)
} }
b.Logger.Debug(&libpack_logger.LogMessage{ b.Logger.Debug(&libpack_logger.LogMessage{
Message: "Cache miss", Message: "Cache miss",
@@ -100,13 +102,6 @@ func (b *BaseClient) Query(query string, variables map[string]interface{}, heade
}) })
} }
if enableRetries || b.retries_enable {
b.Logger.Debug(&libpack_logger.LogMessage{
Message: "Retries enabled",
Pairs: nil,
})
}
q := &QueryExecutor{ q := &QueryExecutor{
BaseClient: b, BaseClient: b,
Query: compiledQuery.JsonQuery, Query: compiledQuery.JsonQuery,
@@ -129,7 +124,7 @@ func (b *BaseClient) Query(query string, variables map[string]interface{}, heade
return nil, err return nil, err
} }
return q.decodeResponse(rv) return b.decodeResponse(rv)
} }
func (q *Query) parseHeadersAndVariables(headers map[string]interface{}) (enableCache, enableRetries, recompileRequired bool) { func (q *Query) parseHeadersAndVariables(headers map[string]interface{}) (enableCache, enableRetries, recompileRequired bool) {
+19 -2
View File
@@ -36,10 +36,15 @@ func Benchmark_convertToJson(bn *testing.B) {
b.convertToJSON(query) b.convertToJSON(query)
} }
} }
func Benchmark_Query(bn *testing.B) { func Benchmark_Query(bn *testing.B) {
mockServer := StartMockServer()
bn.Cleanup(func() {
mockServer.Close()
})
b := NewConnection() b := NewConnection()
query := `query { viewer { login } }` b.SetEndpoint(mockServer.URL)
query := "query { viewer { login } }"
variables := map[string]interface{}{ variables := map[string]interface{}{
"var1": "value1", "var1": "value1",
} }
@@ -54,7 +59,13 @@ func Benchmark_Query(bn *testing.B) {
} }
func Benchmark_QueryNoCacheNoRetry(bn *testing.B) { func Benchmark_QueryNoCacheNoRetry(bn *testing.B) {
mockServer := StartMockServer()
bn.Cleanup(func() {
mockServer.Close()
})
b := NewConnection() b := NewConnection()
b.SetEndpoint(mockServer.URL)
query := `query { viewer { login } }` query := `query { viewer { login } }`
variables := map[string]interface{}{ variables := map[string]interface{}{
"var1": "value1", "var1": "value1",
@@ -69,7 +80,13 @@ func Benchmark_QueryNoCacheNoRetry(bn *testing.B) {
} }
func Benchmark_QueryWithEmptyVariables(bn *testing.B) { func Benchmark_QueryWithEmptyVariables(bn *testing.B) {
mockServer := StartMockServer()
bn.Cleanup(func() {
mockServer.Close()
})
b := NewConnection() b := NewConnection()
b.SetEndpoint(mockServer.URL)
query := `query { viewer { login } }` query := `query { viewer { login } }`
headers := map[string]interface{}{ headers := map[string]interface{}{
"Authorization": "Bearer token", "Authorization": "Bearer token",
+22 -156
View File
@@ -1,8 +1,9 @@
package gql package gql
import ( import (
"reflect"
"testing" "testing"
"github.com/goccy/go-reflect"
) )
func (suite *Tests) TestBaseClient_convertToJSON() { func (suite *Tests) TestBaseClient_convertToJSON() {
@@ -115,182 +116,48 @@ func (suite *Tests) TestBaseClient_Query() {
args args args args
wantReturned_value any wantReturned_value any
name string name string
graphQLServer string
wantType string wantType string
wantErr bool wantErr bool
}{ }{
{ {
name: "TestBaseClient_Query_spacex_working", name: "TestBaseClient_Query_viewer",
graphQLServer: "https://spacex-production.up.railway.app/",
args: args{ args: args{
query: `query Dragons { query: "query { viewer { login } }",
dragons {
name
}
}`,
variables: nil, variables: nil,
headers: map[string]interface{}{ headers: map[string]interface{}{
"x-apollo-operation-name": "Dragons/github.com/lukaszraczylo/go-simple-graphql", "x-apollo-operation-name": "ViewerQuery",
"content-type": "application/json", "content-type": "application/json",
}, },
}, },
wantReturned_value: string(`{"dragons":[{"name":"Dragon 1"},{"name":"Dragon 2"}]}`), wantReturned_value: `{"viewer":{"login":"mockuser"}}`, // Adjusted expected value
wantErr: false, wantErr: false,
}, },
{ {
name: "TestBaseClient_Query_spacex_invalid_query", name: "TestBaseClient_Query_dragons",
graphQLServer: "https://spacex-production.up.railway.app/",
args: args{ args: args{
query: `query Dragons { query: `query Dragons {
dragons { dragons {
name name
} }
`, }`,
variables: nil, variables: nil,
headers: map[string]interface{}{ headers: map[string]interface{}{
"x-apollo-operation-name": "Dragons/github.com/lukaszraczylo/go-simple-graphql", "x-apollo-operation-name": "DragonsQuery",
"content-type": "application/json", "content-type": "application/json",
}, },
}, },
wantReturned_value: nil, wantReturned_value: `{"dragons":[{"name":"Mock Dragon 1"},{"name":"Mock Dragon 2"}]}`, // Adjusted expected value
wantErr: true,
},
{
name: "TestBaseClient_Query_tgbotapp_working",
graphQLServer: "https://telegram-bot.app/v1/graphql",
args: args{
query: `query {
__type(name: "Query") {
name
}
}`,
},
wantReturned_value: string(`{"__type":null}`),
wantErr: false, wantErr: false,
}, },
{ {
name: "TestBaseClient_Query_tgbotapp_invalid_query", name: "TestBaseClient_Query_invalid_query",
graphQLServer: "https://telegram-bot.app/v1/graphql",
args: args{ args: args{
query: `query { query: "query { potato { login } ",
__type(name: "Query") { variables: nil,
name headers: map[string]interface{}{
} "x-apollo-operation-name": "InvalidQuery",
`, "content-type": "application/json",
},
wantReturned_value: nil,
wantErr: true,
},
{
name: "TestBaseClient_Query_tgbotapp_invalid_wrong_field",
graphQLServer: "https://telegram-bot.app/v1/graphql",
args: args{
query: `query {
__type(name: "Query") {
name2
}
}`,
},
wantReturned_value: nil,
wantErr: true,
},
{
name: "TestBaseClient_Query_tgbotapp_wrong_url",
graphQLServer: "https://telegram-bot.app/v0/graphql",
args: args{
query: `query {
__type(name: "Query") {
name
}
}`,
},
wantReturned_value: nil,
wantErr: true,
},
{
name: "TestBaseClient_Query_tgbotapp_valid_query_mapstring",
graphQLServer: "https://telegram-bot.app/v1/graphql",
wantType: "mapstring",
args: args{
query: `query {
__type(name: "Query") {
name
}
}`,
},
wantReturned_value: map[string]interface{}{"__type": nil},
wantErr: false,
},
{
name: "TestBaseClient_Query_tgbotapp_valid_query_byte",
graphQLServer: "https://telegram-bot.app/v1/graphql",
wantType: "byte",
args: args{
query: `query {
__type(name: "Query") {
name
}
}`,
},
wantReturned_value: []byte(`{"__type":null}`),
wantErr: false,
},
{
name: "TestBaseClient_Query_tgbotapp_valid_query_string",
graphQLServer: "https://telegram-bot.app/v1/graphql",
wantType: "string",
args: args{
query: `query {
__type(name: "Query") {
name
}
}`,
},
wantReturned_value: string(`{"__type":null}`),
wantErr: false,
},
{
name: "TestBaseClient_Query_tgbotapp_valid_query_invalid_type",
graphQLServer: "https://telegram-bot.app/v1/graphql",
wantType: "potato",
args: args{
query: `query {
__type(name: "Query") {
name
}
}`,
},
wantReturned_value: nil,
wantErr: true,
},
{
name: "TestBaseClient_Query_tgbotapp_valid_query_cache",
graphQLServer: "https://telegram-bot.app/v1/graphql",
args: args{
query: `query {
__type(name: "Query") {
name
}
}`,
variables: map[string]interface{}{
"gqlcache": true,
},
},
wantReturned_value: string(`{"__type":null}`),
wantErr: false,
},
{
name: "TestBaseClient_Query_tgbotapp_valid_query_with_retry",
graphQLServer: "https://telegram-bot.app/v0/graphql",
wantType: "byte",
args: args{
query: `query {
__type(name: "Query") {
name
}
}`,
variables: map[string]interface{}{
"gqlretries": true,
}, },
}, },
wantReturned_value: nil, wantReturned_value: nil,
@@ -300,9 +167,8 @@ func (suite *Tests) TestBaseClient_Query() {
for _, tt := range tests { for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) { suite.T().Run(tt.name, func(t *testing.T) {
b := NewConnection() b := NewConnection()
if tt.graphQLServer != "" { b.SetEndpoint(mockServer.URL)
b.SetEndpoint(tt.graphQLServer) b.SetHTTPClient(mockServer.Client())
}
if tt.wantType != "" { if tt.wantType != "" {
b.SetOutput(tt.wantType) b.SetOutput(tt.wantType)
} }
+15 -1
View File
@@ -1,6 +1,7 @@
package gql package gql
import ( import (
"net/http/httptest"
"testing" "testing"
assertions "github.com/stretchr/testify/assert" assertions "github.com/stretchr/testify/assert"
@@ -12,9 +13,22 @@ type Tests struct {
} }
var ( var (
assert *assertions.Assertions assert *assertions.Assertions
mockServer *httptest.Server
) )
func (suite *Tests) SetupSuite() {
// Start mock GraphQL server
mockServer = StartMockServer()
}
func (suite *Tests) TearDownSuite() {
// Close the mock server
if mockServer != nil {
mockServer.Close()
}
}
func (suite *Tests) SetupTest() { func (suite *Tests) SetupTest() {
assert = assertions.New(suite.T()) assert = assertions.New(suite.T())
} }