Skip to content

Commit

Permalink
Merge pull request #16 from ekomobile/errors
Browse files Browse the repository at this point in the history
Wrap errors. Added ResponseError. Abstract transport encoding/decoding.
  • Loading branch information
turboezh authored May 7, 2023
2 parents 8528cda + 4d8d739 commit bcae394
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 12 deletions.
36 changes: 24 additions & 12 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"

"github.com/ekomobile/dadata/v2/client/transport"
)

type (
Expand All @@ -25,6 +26,8 @@ type (
httpClient *http.Client
credentialProvider CredentialProvider
endpointURL *url.URL
encoderFactory transport.EncoderFactory
decoderFactory transport.DecoderFactory
}
)

Expand All @@ -43,47 +46,56 @@ 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,
}
}

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")
request.Header.Add("Accept", "application/json")

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
Expand Down
46 changes: 46 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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(), &params)
if err != nil {
return
}

for _, s := range suggestions {
fmt.Printf("%s", s.Value)
}
}
13 changes: 13 additions & 0 deletions client/error.go
Original file line number Diff line number Diff line change
@@ -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)
}
34 changes: 34 additions & 0 deletions client/option.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package client

import (
"encoding/json"
"io"
"net/http"

"github.com/ekomobile/dadata/v2/client/transport"
)

type (
Expand All @@ -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)
}
}
}
61 changes: 61 additions & 0 deletions client/option_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package client

import (
"errors"
"io"
"net/http"
"testing"

"github.com/ekomobile/dadata/v2/client/transport"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -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{}

Expand Down
17 changes: 17 additions & 0 deletions client/transport/translator.go
Original file line number Diff line number Diff line change
@@ -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
)

0 comments on commit bcae394

Please sign in to comment.