Merge pull request #16 from ekomobile/errors

Wrap errors. Added ResponseError. Abstract transport encoding/decoding.
This commit is contained in:
Alex 2023-05-08 00:15:46 +03:00 committed by GitHub
commit bcae394b5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 195 additions and 12 deletions

View File

@ -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

View File

@ -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(), &params)
if err != nil {
return
}
for _, s := range suggestions {
fmt.Printf("%s", s.Value)
}
}

13
client/error.go Normal file
View 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)
}

View File

@ -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)
}
}
}

View File

@ -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{}

View 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
)