From 4d8d739615af9f7130a7c21fffc06ec9c20aadba Mon Sep 17 00:00:00 2001 From: Alexander Zhuravlev Date: Sun, 7 May 2023 14:15:49 +0300 Subject: [PATCH] Wrap errors. Added ResponseError. Abstract transport encoding/decoding. --- client/client.go | 36 +++++++++++++------- client/client_test.go | 46 +++++++++++++++++++++++++ client/error.go | 13 ++++++++ client/option.go | 34 +++++++++++++++++++ client/option_test.go | 61 ++++++++++++++++++++++++++++++++++ client/transport/translator.go | 17 ++++++++++ 6 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 client/error.go create mode 100644 client/transport/translator.go diff --git a/client/client.go b/client/client.go index ea84982..26d34c0 100644 --- a/client/client.go +++ b/client/client.go @@ -3,10 +3,11 @@ package client import ( "bytes" "context" - "encoding/json" "fmt" "net/http" "net/url" + + "github.com/ekomobile/dadata/v2/client/transport" ) type ( @@ -25,6 +26,8 @@ type ( httpClient *http.Client credentialProvider CredentialProvider endpointURL *url.URL + encoderFactory transport.EncoderFactory + decoderFactory transport.DecoderFactory } ) @@ -43,6 +46,14 @@ func NewClient(endpointURL *url.URL, opts ...Option) *Client { applyOptions(&options, opts...) + if options.encoderFactory == nil { + options.encoderFactory = defaultJsonEncoderFactory() + } + + if options.decoderFactory == nil { + options.decoderFactory = defaultJsonDecoderFactory() + } + return &Client{ options: options, } @@ -50,22 +61,20 @@ func NewClient(endpointURL *url.URL, opts ...Option) *Client { func (c *Client) doRequest(ctx context.Context, method string, url *url.URL, body interface{}, result interface{}) (err error) { if err = ctx.Err(); err != nil { - return fmt.Errorf("doRequest: ctx.Err return err=%v", err) + return fmt.Errorf("doRequest: context err: %w", err) } buffer := &bytes.Buffer{} - if err = json.NewEncoder(buffer).Encode(body); err != nil { - return fmt.Errorf("doRequest: json.Encode return err = %v", err) + if err = c.options.encoderFactory(buffer)(body); err != nil { + return fmt.Errorf("doRequest: request body ecnode err: %w", err) } - request, err := http.NewRequest(method, url.String(), buffer) + request, err := http.NewRequestWithContext(ctx, method, url.String(), buffer) if err != nil { - return fmt.Errorf("doRequest: http.NewRequest return err = %v", err) + return fmt.Errorf("doRequest: new request err: %w", err) } - request = request.WithContext(ctx) - request.Header.Add("Authorization", fmt.Sprintf("Token %s", c.options.credentialProvider.ApiKey())) request.Header.Add("X-Secret", c.options.credentialProvider.SecretKey()) request.Header.Add("Content-Type", "application/json") @@ -73,17 +82,20 @@ func (c *Client) doRequest(ctx context.Context, method string, url *url.URL, bod response, err := c.options.httpClient.Do(request) if err != nil { - return fmt.Errorf("doRequest: httpClient.Do return err = %v", err) + return fmt.Errorf("doRequest: request do err: %w", err) } defer response.Body.Close() if http.StatusOK != response.StatusCode { - return fmt.Errorf("doRequest: Request error %v", response.Status) + return fmt.Errorf( + "doRequest: Response not OK: %w", + &ResponseError{Status: response.Status, StatusCode: response.StatusCode}, + ) } - if err = json.NewDecoder(response.Body).Decode(&result); err != nil { - return fmt.Errorf("doRequest: json.Decode return err = %v", err) + if err = c.options.decoderFactory(response.Body)(&result); err != nil { + return fmt.Errorf("doRequest: response body decode err: %w", err) } return diff --git a/client/client_test.go b/client/client_test.go index 4b42235..22595c2 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -2,10 +2,13 @@ package client import ( "context" + "encoding/json" "fmt" + "io" "net/url" "github.com/ekomobile/dadata/v2/api/suggest" + "github.com/ekomobile/dadata/v2/client/transport" ) func ExampleNewClient() { @@ -62,3 +65,46 @@ func ExampleCredentials() { fmt.Printf("%s", s.Value) } } + +func ExampleWithEncoderFactory() { + var err error + endpointUrl, err := url.Parse("https://suggestions.dadata.ru/suggestions/api/4_1/rs/") + if err != nil { + return + } + + // Customize json encoding + encoderFactory := func(w io.Writer) transport.Encoder { + e := json.NewEncoder(w) + e.SetIndent("", " ") + return func(v interface{}) error { + return e.Encode(v) + } + } + + // Customize json decoding + decoderFactory := func(r io.Reader) transport.Decoder { + d := json.NewDecoder(r) + d.DisallowUnknownFields() + return func(v interface{}) error { + return d.Decode(v) + } + } + + api := suggest.Api{ + Client: NewClient(endpointUrl, WithEncoderFactory(encoderFactory), WithDecoderFactory(decoderFactory)), + } + + params := suggest.RequestParams{ + Query: "ул Свободы", + } + + suggestions, err := api.Address(context.Background(), ¶ms) + if err != nil { + return + } + + for _, s := range suggestions { + fmt.Printf("%s", s.Value) + } +} diff --git a/client/error.go b/client/error.go new file mode 100644 index 0000000..c8abdd2 --- /dev/null +++ b/client/error.go @@ -0,0 +1,13 @@ +package client + +import "fmt" + +// ResponseError indicates an HTTP non-200 response code. +type ResponseError struct { + Status string // e.g. "200 OK" + StatusCode int // e.g. 200 +} + +func (e *ResponseError) Error() string { + return fmt.Sprintf("HTTP response: %s", e.Status) +} diff --git a/client/option.go b/client/option.go index 4301afa..2e01be8 100644 --- a/client/option.go +++ b/client/option.go @@ -1,7 +1,11 @@ package client import ( + "encoding/json" + "io" "net/http" + + "github.com/ekomobile/dadata/v2/client/transport" ) type ( @@ -23,8 +27,38 @@ func WithCredentialProvider(c CredentialProvider) Option { } } +func WithEncoderFactory(f transport.EncoderFactory) Option { + return func(opts *clientOptions) { + opts.encoderFactory = f + } +} + +func WithDecoderFactory(f transport.DecoderFactory) Option { + return func(opts *clientOptions) { + opts.decoderFactory = f + } +} + func applyOptions(options *clientOptions, opts ...Option) { for _, o := range opts { o(options) } } + +func defaultJsonEncoderFactory() transport.EncoderFactory { + return func(w io.Writer) transport.Encoder { + d := json.NewEncoder(w) + return func(v interface{}) error { + return d.Encode(v) + } + } +} + +func defaultJsonDecoderFactory() transport.DecoderFactory { + return func(r io.Reader) transport.Decoder { + d := json.NewDecoder(r) + return func(v interface{}) error { + return d.Decode(v) + } + } +} diff --git a/client/option_test.go b/client/option_test.go index ecc322b..97ac53e 100644 --- a/client/option_test.go +++ b/client/option_test.go @@ -1,9 +1,12 @@ package client import ( + "errors" + "io" "net/http" "testing" + "github.com/ekomobile/dadata/v2/client/transport" "github.com/stretchr/testify/assert" ) @@ -48,6 +51,64 @@ func TestWithCredentialProvider(t *testing.T) { } } +func TestWithEncoderFactory(t *testing.T) { + type args struct { + c transport.EncoderFactory + } + tests := []struct { + name string + args args + }{ + { + name: "TestWithEncoderFactory", + args: args{ + c: func(w io.Writer) transport.Encoder { + return func(v interface{}) error { + return errors.New("c164a8d0-64b6-4374-a4b4-4036fbee504b") + } + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := &clientOptions{} + WithEncoderFactory(tt.args.c)(opts) + + assert.True(t, tt.args.c(nil)(nil).Error() == opts.encoderFactory(nil)(nil).Error()) + }) + } +} + +func TestWithDecoderFactory(t *testing.T) { + type args struct { + c transport.DecoderFactory + } + tests := []struct { + name string + args args + }{ + { + name: "TestWithDecoderFactory", + args: args{ + c: func(r io.Reader) transport.Decoder { + return func(v interface{}) error { + return errors.New("b02ef946-15c8-40e0-b94d-efc3b26a8f75") + } + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := &clientOptions{} + WithDecoderFactory(tt.args.c)(opts) + + assert.True(t, tt.args.c(nil)(nil).Error() == opts.decoderFactory(nil)(nil).Error()) + }) + } +} + func Test_applyOptions(t *testing.T) { cp := &Credentials{} diff --git a/client/transport/translator.go b/client/transport/translator.go new file mode 100644 index 0000000..9b636f6 --- /dev/null +++ b/client/transport/translator.go @@ -0,0 +1,17 @@ +package transport + +import "io" + +type ( + // EncoderFactory creates new request encoder + EncoderFactory func(w io.Writer) Encoder + + // DecoderFactory creates new response decoder + DecoderFactory func(r io.Reader) Decoder + + // Encoder encodes request from v + Encoder func(v interface{}) error + + // Decoder decodes response into v. + Decoder func(v interface{}) error +)