Merge pull request #16 from ekomobile/errors
Wrap errors. Added ResponseError. Abstract transport encoding/decoding.
This commit is contained in:
commit
bcae394b5d
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
13
client/error.go
Normal file
13
client/error.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
|
||||
|
|
17
client/transport/translator.go
Normal file
17
client/transport/translator.go
Normal file
|
@ -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
|
||||
)
|
Loading…
Reference in New Issue
Block a user