Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support experimental manager #572

Merged
merged 36 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9ffd104
Add Embedder.URL
polyfloyd Aug 22, 2024
5eba25b
feat: add gzip and deflate encoding
Ja7ad Aug 22, 2024
9a6b1f6
feat: client support content encoding for body and response
Ja7ad Aug 22, 2024
db521a0
feat: add option for enable content encoding
Ja7ad Aug 22, 2024
59cc80d
feat: add content encoding and compress level type
Ja7ad Aug 22, 2024
d5a44d2
feat: add content encoding on constructor
Ja7ad Aug 22, 2024
d3de4f1
fix: header set for user agent
Ja7ad Aug 22, 2024
97e6d77
feat: add brotli encoding
Ja7ad Aug 23, 2024
b8a9f5a
fix: support request and response encoding same type encoding opt
Ja7ad Aug 23, 2024
ed267b4
fix: add unit test for options
Ja7ad Aug 23, 2024
238a029
feat: Add experimental features support
A7bari Aug 23, 2024
04aba93
generating type marshalers and unmarshalers
A7bari Aug 23, 2024
d9713ec
add: zero allocation copy for writer
Ja7ad Aug 24, 2024
7cb77b2
add: unit test for zero allocation copy
Ja7ad Aug 24, 2024
c521579
feat: transcode get and update curl to golang example on code-samples…
A7bari Aug 24, 2024
55bee99
fix: remove encoding option on internal request object
Ja7ad Aug 25, 2024
578ea94
chore: add unit test for content encoding
Ja7ad Aug 25, 2024
24cbcb4
fix: used built-in flate package
Ja7ad Aug 25, 2024
d868fd3
fix: used built-in gzip package
Ja7ad Aug 25, 2024
e08d216
fix: data race on test zero alloc
Ja7ad Aug 25, 2024
14c606d
fix: test case issue
Ja7ad Aug 26, 2024
227dbe2
fix: add some test stage for improve coverage
Ja7ad Aug 26, 2024
6c56bf0
fix: remove brotli test stage
Ja7ad Aug 26, 2024
a4da3e4
fix: add encoder for decoing encoded internal error
Ja7ad Aug 26, 2024
e09d86e
Merge #568
meili-bors[bot] Sep 15, 2024
617e53b
Merge branch 'main' into feat/content-encoding
Ja7ad Sep 16, 2024
e41c3e9
fix: currntly support delfate compression by using zlib
Ja7ad Sep 16, 2024
bc43a14
fix: add writer nil error on test for prevent panic
Ja7ad Sep 16, 2024
d132bc1
Add multi-search federation (fixes #573)
polyfloyd Aug 20, 2024
63161f2
Merge #563
meili-bors[bot] Sep 18, 2024
5ecb35a
Merge branch 'main' into feat/content-encoding
Ja7ad Sep 18, 2024
2ee4206
Merge #570
meili-bors[bot] Sep 23, 2024
de5c913
feat: Add experimental features support
A7bari Aug 23, 2024
5df6051
feat: transcode get and update curl to golang example on code-samples…
A7bari Aug 24, 2024
01716ae
Merge branch 'Support-experimental-manager' of https://github.com/A7b…
A7bari Sep 26, 2024
e98fd5e
feat: Add support for EditDocumentsByFunction and ContainsFilter in E…
A7bari Sep 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 61 additions & 29 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@ import (
)

type client struct {
client *http.Client
host string
apiKey string
bufferPool *sync.Pool
client *http.Client
host string
apiKey string
bufferPool *sync.Pool
encoder encoder
contentEncoding ContentEncoding
}

type internalRequest struct {
endpoint string
method string
contentType string

endpoint string
method string
contentType string
withRequest interface{}
withResponse interface{}
withQueryParams map[string]string
Expand All @@ -33,8 +34,8 @@ type internalRequest struct {
functionName string
}

func newClient(cli *http.Client, host, apiKey string) *client {
return &client{
func newClient(cli *http.Client, host, apiKey string, ce ContentEncoding, cl EncodingCompressionLevel) *client {
c := &client{
client: cli,
host: host,
apiKey: apiKey,
Expand All @@ -44,6 +45,13 @@ func newClient(cli *http.Client, host, apiKey string) *client {
},
},
}

if !ce.IsZero() {
c.contentEncoding = ce
c.encoder = newEncoding(ce, cl)
}

return c
}

func (c *client) executeRequest(ctx context.Context, req *internalRequest) error {
Expand All @@ -57,6 +65,7 @@ func (c *client) executeRequest(ctx context.Context, req *internalRequest) error
Message: "empty meilisearch message",
},
StatusCodeExpected: req.acceptedStatusCodes,
encoder: c.encoder,
}

resp, err := c.sendRequest(ctx, req, internalError)
Expand Down Expand Up @@ -118,10 +127,11 @@ func (c *client) sendRequest(
}

rawRequest := req.withRequest

buf := c.bufferPool.Get().(*bytes.Buffer)
buf.Reset()

if b, ok := rawRequest.([]byte); ok {
// If the request body is already a []byte then use it directly
buf := c.bufferPool.Get().(*bytes.Buffer)
buf.Reset()
buf.Write(b)
body = buf
} else if reader, ok := rawRequest.(io.Reader); ok {
Expand All @@ -136,22 +146,31 @@ func (c *client) sendRequest(
if marshaler, ok := rawRequest.(json.Marshaler); ok {
data, err = marshaler.MarshalJSON()
if err != nil {
return nil, internalError.WithErrCode(ErrCodeMarshalRequest, fmt.Errorf("failed to marshal with MarshalJSON: %w", err))
return nil, internalError.WithErrCode(ErrCodeMarshalRequest,
fmt.Errorf("failed to marshal with MarshalJSON: %w", err))
}
if data == nil {
return nil, internalError.WithErrCode(ErrCodeMarshalRequest, errors.New("MarshalJSON returned nil data"))
return nil, internalError.WithErrCode(ErrCodeMarshalRequest,
errors.New("MarshalJSON returned nil data"))
}
} else {
data, err = json.Marshal(rawRequest)
if err != nil {
return nil, internalError.WithErrCode(ErrCodeMarshalRequest, fmt.Errorf("failed to marshal with json.Marshal: %w", err))
return nil, internalError.WithErrCode(ErrCodeMarshalRequest,
fmt.Errorf("failed to marshal with json.Marshal: %w", err))
}
}
buf := c.bufferPool.Get().(*bytes.Buffer)
buf.Reset()
buf.Write(data)
body = buf
}

if !c.contentEncoding.IsZero() {
body, err = c.encoder.Encode(body)
if err != nil {
return nil, internalError.WithErrCode(ErrCodeMarshalRequest,
fmt.Errorf("failed to marshal with json.Marshal: %w", err))
}
}
}

// Create the HTTP request
Expand All @@ -168,6 +187,14 @@ func (c *client) sendRequest(
request.Header.Set("Authorization", "Bearer "+c.apiKey)
}

if req.withResponse != nil && !c.contentEncoding.IsZero() {
request.Header.Set("Accept-Encoding", c.contentEncoding.String())
}

if req.withRequest != nil && !c.contentEncoding.IsZero() {
request.Header.Set("Content-Encoding", c.contentEncoding.String())
}

request.Header.Set("User-Agent", GetQualifiedVersion())

resp, err := c.client.Do(request)
Expand Down Expand Up @@ -210,18 +237,23 @@ func (c *client) handleStatusCode(req *internalRequest, statusCode int, body []b

func (c *client) handleResponse(req *internalRequest, body []byte, internalError *Error) (err error) {
if req.withResponse != nil {

internalError.ResponseToString = string(body)

var err error
if resp, ok := req.withResponse.(json.Unmarshaler); ok {
err = resp.UnmarshalJSON(body)
req.withResponse = resp
if !c.contentEncoding.IsZero() {
if err := c.encoder.Decode(body, req.withResponse); err != nil {
return internalError.WithErrCode(ErrCodeResponseUnmarshalBody, err)
}
} else {
err = json.Unmarshal(body, req.withResponse)
}
if err != nil {
return internalError.WithErrCode(ErrCodeResponseUnmarshalBody, err)
internalError.ResponseToString = string(body)

var err error
if resp, ok := req.withResponse.(json.Unmarshaler); ok {
err = resp.UnmarshalJSON(body)
req.withResponse = resp
} else {
err = json.Unmarshal(body, req.withResponse)
}
if err != nil {
return internalError.WithErrCode(ErrCodeResponseUnmarshalBody, err)
}
}
}
return nil
Expand Down
170 changes: 162 additions & 8 deletions client_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package meilisearch

import (
"bytes"
"context"
"encoding/json"
"errors"
"github.com/stretchr/testify/require"
"io"
"net/http"
"net/http/httptest"
"strings"
Expand All @@ -29,9 +31,65 @@ func TestExecuteRequest(t *testing.T) {
if r.Method == http.MethodGet && r.URL.Path == "/test-get" {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"message":"get successful"}`))
} else if r.Method == http.MethodGet && r.URL.Path == "/test-get-encoding" {
encode := r.Header.Get("Accept-Encoding")
if len(encode) != 0 {
enc := newEncoding(ContentEncoding(encode), DefaultCompression)
d := &mockData{Name: "foo", Age: 30}

b, err := json.Marshal(d)
require.NoError(t, err)

res, err := enc.Encode(bytes.NewReader(b))
require.NoError(t, err)
_, _ = w.Write(res.Bytes())
w.WriteHeader(http.StatusOK)
return
}
_, _ = w.Write([]byte("invalid message"))
w.WriteHeader(http.StatusInternalServerError)
} else if r.Method == http.MethodPost && r.URL.Path == "/test-req-resp-encoding" {
accept := r.Header.Get("Accept-Encoding")
ce := r.Header.Get("Content-Encoding")

reqEnc := newEncoding(ContentEncoding(ce), DefaultCompression)
respEnc := newEncoding(ContentEncoding(accept), DefaultCompression)
req := new(mockData)

if len(ce) != 0 {
b, err := io.ReadAll(r.Body)
require.NoError(t, err)

err = reqEnc.Decode(b, req)
require.NoError(t, err)
}

if len(accept) != 0 {
d, err := json.Marshal(req)
require.NoError(t, err)
res, err := respEnc.Encode(bytes.NewReader(d))
require.NoError(t, err)
_, _ = w.Write(res.Bytes())
w.WriteHeader(http.StatusOK)
}
} else if r.Method == http.MethodPost && r.URL.Path == "/test-post" {
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{"message":"post successful"}`))
msg := []byte(`{"message":"post successful"}`)
_, _ = w.Write(msg)

} else if r.Method == http.MethodPost && r.URL.Path == "/test-post-encoding" {
w.WriteHeader(http.StatusCreated)
msg := []byte(`{"message":"post successful"}`)

enc := r.Header.Get("Accept-Encoding")
if len(enc) != 0 {
e := newEncoding(ContentEncoding(enc), DefaultCompression)
b, err := e.Encode(bytes.NewReader(msg))
require.NoError(t, err)
_, _ = w.Write(b.Bytes())
return
}
_, _ = w.Write(msg)
} else if r.URL.Path == "/test-bad-request" {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"message":"bad request"}`))
Expand All @@ -47,13 +105,12 @@ func TestExecuteRequest(t *testing.T) {
}))
defer ts.Close()

client := newClient(&http.Client{}, ts.URL, "testApiKey")

tests := []struct {
name string
internalReq *internalRequest
expectedResp interface{}
wantErr bool
name string
internalReq *internalRequest
expectedResp interface{}
contentEncoding ContentEncoding
wantErr bool
}{
{
name: "Successful GET request",
Expand Down Expand Up @@ -190,11 +247,108 @@ func TestExecuteRequest(t *testing.T) {
expectedResp: nil,
wantErr: true,
},
{
name: "Test request encoding gzip",
internalReq: &internalRequest{
endpoint: "/test-post-encoding",
method: http.MethodPost,
withRequest: map[string]string{"key": "value"},
contentType: contentTypeJSON,
withResponse: &mockResponse{},
acceptedStatusCodes: []int{http.StatusCreated},
},
expectedResp: &mockResponse{Message: "post successful"},
contentEncoding: GzipEncoding,
wantErr: false,
},
{
name: "Test request encoding deflate",
internalReq: &internalRequest{
endpoint: "/test-post-encoding",
method: http.MethodPost,
withRequest: map[string]string{"key": "value"},
contentType: contentTypeJSON,
withResponse: &mockResponse{},
acceptedStatusCodes: []int{http.StatusCreated},
},
expectedResp: &mockResponse{Message: "post successful"},
contentEncoding: DeflateEncoding,
wantErr: false,
},
{
name: "Test request encoding brotli",
internalReq: &internalRequest{
endpoint: "/test-post-encoding",
method: http.MethodPost,
withRequest: map[string]string{"key": "value"},
contentType: contentTypeJSON,
withResponse: &mockResponse{},
acceptedStatusCodes: []int{http.StatusCreated},
},
expectedResp: &mockResponse{Message: "post successful"},
contentEncoding: BrotliEncoding,
wantErr: false,
},
{
name: "Test response decoding gzip",
internalReq: &internalRequest{
endpoint: "/test-get-encoding",
method: http.MethodGet,
withRequest: nil,
withResponse: &mockData{},
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: &mockData{Name: "foo", Age: 30},
contentEncoding: GzipEncoding,
wantErr: false,
},
{
name: "Test response decoding deflate",
internalReq: &internalRequest{
endpoint: "/test-get-encoding",
method: http.MethodGet,
withRequest: nil,
withResponse: &mockData{},
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: &mockData{Name: "foo", Age: 30},
contentEncoding: DeflateEncoding,
wantErr: false,
},
{
name: "Test response decoding brotli",
internalReq: &internalRequest{
endpoint: "/test-get-encoding",
method: http.MethodGet,
withRequest: nil,
withResponse: &mockData{},
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: &mockData{Name: "foo", Age: 30},
contentEncoding: BrotliEncoding,
wantErr: false,
},
{
name: "Test request and response encoding",
internalReq: &internalRequest{
endpoint: "/test-req-resp-encoding",
method: http.MethodPost,
contentType: contentTypeJSON,
withRequest: &mockData{Name: "foo", Age: 30},
withResponse: &mockData{},
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: &mockData{Name: "foo", Age: 30},
contentEncoding: GzipEncoding,
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := client.executeRequest(context.Background(), tt.internalReq)
c := newClient(&http.Client{}, ts.URL, "testApiKey", tt.contentEncoding, DefaultCompression)

err := c.executeRequest(context.Background(), tt.internalReq)
if tt.wantErr {
require.Error(t, err)
} else {
Expand Down
Loading
Loading