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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/ekomobile/dadata/v2/client/transport"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -25,6 +26,8 @@ type (
|
|||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
credentialProvider CredentialProvider
|
credentialProvider CredentialProvider
|
||||||
endpointURL *url.URL
|
endpointURL *url.URL
|
||||||
|
encoderFactory transport.EncoderFactory
|
||||||
|
decoderFactory transport.DecoderFactory
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,6 +46,14 @@ func NewClient(endpointURL *url.URL, opts ...Option) *Client {
|
|||||||
|
|
||||||
applyOptions(&options, opts...)
|
applyOptions(&options, opts...)
|
||||||
|
|
||||||
|
if options.encoderFactory == nil {
|
||||||
|
options.encoderFactory = defaultJsonEncoderFactory()
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.decoderFactory == nil {
|
||||||
|
options.decoderFactory = defaultJsonDecoderFactory()
|
||||||
|
}
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
options: options,
|
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) {
|
func (c *Client) doRequest(ctx context.Context, method string, url *url.URL, body interface{}, result interface{}) (err error) {
|
||||||
if err = ctx.Err(); err != nil {
|
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{}
|
buffer := &bytes.Buffer{}
|
||||||
|
|
||||||
if err = json.NewEncoder(buffer).Encode(body); err != nil {
|
if err = c.options.encoderFactory(buffer)(body); err != nil {
|
||||||
return fmt.Errorf("doRequest: json.Encode return err = %v", err)
|
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 {
|
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("Authorization", fmt.Sprintf("Token %s", c.options.credentialProvider.ApiKey()))
|
||||||
request.Header.Add("X-Secret", c.options.credentialProvider.SecretKey())
|
request.Header.Add("X-Secret", c.options.credentialProvider.SecretKey())
|
||||||
request.Header.Add("Content-Type", "application/json")
|
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)
|
response, err := c.options.httpClient.Do(request)
|
||||||
if err != nil {
|
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()
|
defer response.Body.Close()
|
||||||
|
|
||||||
if http.StatusOK != response.StatusCode {
|
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 {
|
if err = c.options.decoderFactory(response.Body)(&result); err != nil {
|
||||||
return fmt.Errorf("doRequest: json.Decode return err = %v", err)
|
return fmt.Errorf("doRequest: response body decode err: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -2,10 +2,13 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/ekomobile/dadata/v2/api/suggest"
|
"github.com/ekomobile/dadata/v2/api/suggest"
|
||||||
|
"github.com/ekomobile/dadata/v2/client/transport"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ExampleNewClient() {
|
func ExampleNewClient() {
|
||||||
@ -62,3 +65,46 @@ func ExampleCredentials() {
|
|||||||
fmt.Printf("%s", s.Value)
|
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
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ekomobile/dadata/v2/client/transport"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
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) {
|
func applyOptions(options *clientOptions, opts ...Option) {
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(options)
|
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
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ekomobile/dadata/v2/client/transport"
|
||||||
"github.com/stretchr/testify/assert"
|
"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) {
|
func Test_applyOptions(t *testing.T) {
|
||||||
cp := &Credentials{}
|
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