patch

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 13, 2020 License: MIT Imports: 11 Imported by: 1

README

Patch

Patch is an HTTP client built on top of net/http that that helps you make API requests without the boilerplate.

Features
  • Automatic encoding & decoding of bodies
  • Option to bring your own http.Client{}
  • Easy asynchronous requests
  • Response status code validation

Installation

go get github.com/jakewright/patch

Usage

Creating a client

The New function will return a client with sensible defaults

c := patch.New()

The defaults can be overridden with Options.

c := patch.New(
    // The default timeout is 30 seconds. This can be
    // changed. Setting a timeout of 0 means no timeout. 
    patch.WithTimeout(10 * time.Second),
    
    // The default status validator returns true for
    // any 2xx status code. To remove the status
    // validator, pass nil instead of a func.
    patch.WithStatusValidator(func(status int) bool {
        return status == 200
    }),
    
    // By default, request bodies are encoded as JSON.
    // This can be changed by providing a different 
    // Encoder. If a request has its own Encoder set, 
    // it will override the client's Encoder.
    patch.WithEncoder(&patch.EncoderFormURL{}),
)

Custom base client

Patch creates an http.Client{} which it uses to make requests. If you'd like to provide your own instance, use the NewFromBaseClient function.

bc := http.Client{}
c := NewFromBaseClient(&bc)

For flexibility, a custom base client doesn't have to be of type http.Client{}. It just has to implement the following interface. Note that the WithTimeout option won't work with non-standard base client types.

An http.Client can be wrapped in a custom Doer implementation to build middleware.

type Doer interface {
    Do(*http.Request) (*http.Response, error)
}
Making a GET request
user := struct{
    Name string `json:"name"`
    Age int `json:"age"
}{}

// The response is returned and also decoded into the last argument
rsp, err := client.Get(ctx, http://example.com/user/204, &user)
if err != nil {
    panic(err)
}

The response type embeds the original http.Response{} but provides some convenience functions.

// Read the body as a []byte or string
b, err := rsp.BodyBytes()
s, err := rsp.BodyString()

The body can be read an unlimited number of times. The underlying rsp.Body is also available as normal.

Making a POST request

The Post() function takes an extra argument: the body. By default, it will be encoded as JSON and an application/json; charset=utf-8 Content-Type header will be set.

Helper functions also exist for PUT, PATCH and DELETE.

body := struct{
    Name string `json:"name"`
    Age int `json:"age"`
}{
    Name: "Homer Simpson",
    Age: 39,
}

// If desired, the response body can be decoded into the last argument.
rsp, err := client.Post(ctx, "http://example.com/users", &body, nil)

Note that the response is not decoded if the request fails, including if status code validation fails. See the section on error handling for more information.

Making asynchronous requests

The helper functions Get, Post, Put, Patch and Delete are built on top the of Send function. You can use this directly for more control over the request, including making asynchronous requests.

req := &patch.Request{
    Method: "GET"
    URL:    "http://example.com"
}

// Send is non-blocking and returns a Future
ftr := client.Send(&req)

// Do other work

// Response blocks until the response is available
rsp, err := ftr.Response()
Encoding the request

By default, requests are encoded as JSON. The default encoding can be changed by using the WithEncoder() option when creating the client.

Encoding can also be set per-request by setting the Encoder field on the request struct. If this is not nil, it will override the client's default Encoder.

req := &patch.Request{
    Encoder: &patch.EncoderFormURL{},
}

JSON encoder

The JSON encoder uses encoding/json to marshal the body into JSON. The Content-Type header is set to application/json; charset=utf-8 but this can be changed by setting the CustomContentType field on the EncoderJSON{} struct.

Form URL encoder

The Form encoder will marshal types as follows:

  1. If the body is of type url.Values{} or map[string][]string, it is encoded using Values.Encode.
  2. If the body is of type map[string]string, it is converted to a url.Values{} and encoded as above.
  3. If the body is of any other type, it is converted to a url.Values{} by gorilla/schema and then encoded as above.

The tag alias used by gorilla/schema is configurable on the EncoderFormURL{} struct.

The Content-Type header is set to application/x-www-form-urlencoded but this can be changed by setting the CustomContentType field on the EncoderFormURL{} struct.

enc := &patch.EncoderFormURL{
    TagAlias: "url",
}

client, err := patch.New(patch.WithEncoder(enc))
if err != nil {
    panic(err)
}

// The body will be encoded as "name=Homer&age=39"

body := struct{
    Name string `url:"name"`
    Age int `url:"age"`
}{
    Name: "Homer Simpson",
    Age: 39,
}

rsp, err := client.Post(ctx, "http://example.com", &body, nil)

Custom encoder

A custom encoder can be provided. It must implement the following interface.

type Encoder interface {
    ContentType() string
    Encode(interface{}) (io.Reader, error)
}
Decoding the response

If the final argument v to Get, Post, Put, Patch or Delete is not nil, then the body will be decoded into the value pointed to by v. The decoder to use will be inferred from the response's Content-Type header. To explicitly specify a Decoder, use the convenience functions on the Response struct.

rsp, err := client.Get(ctx, "http://example.com", nil, nil)
if err != nil {
    panic(err)
}

v := struct{...}{}

// Decode will infer the decoder from the Content-Type header.
err := rsp.Decode(&v)

// DecodeJSON will decode the body as JSON, regardless of the Content-Type header.
err := rsp.DecodeJSON(&v)

// DecodeUsing will decode the body using a custom Decoder.
err := rsp.DecodeUsing(dec, &v)

Decode hooks

Sometimes, you want to decode into different targets depending on the response status code. Arguments to the decode functions can be wrapped in a DecodeHook to specify for which status codes the target should be used.

err := rsp.Decode(patch.On2xx(&result), patch.On4xx(&clientErr), patch.On5xx(&serverErr))

Decode hooks work as the final argument to the method helper functions too.

Specific status codes can be targeted using the patch.OnStatus(404, &target) hook. Of course, you can write your own hooks too.

Error handling

The method helper functions Get, Post, Put, Patch and Delete will not try to decode the body if the baseClient returned an error, of if the status validator returns false.

If the request succeeds but decoding the body fails, the decoding error will be returned.

Some errors are identifiable using errors.As(). See errors.go for a list of typed errors that can be returned.

Advanced example

Here is an example of integrating with the GitHub API to list repositories by user, inspired by dghubble/sling.

type Repository struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

type GithubError struct {
    Message string `json:"message"`
}

func (e *GithubError) Error() string {
    return e.Message
}

type RepoService struct {
    client *patch.Client
}

func NewRepoService() *RepoService {
    sv := func(status int) bool {
        if status >= 200 && status < 300 {
            return true
        }

        // Allow 4xx status codes because we
        // expect to be able to decode them
        if status >= 400 && status < 500 {
            return true
        }

        return false
    }

    return &RepoService{
        client: patch.New(
            patch.WithBaseURL("https://api.github.com"),
            patch.WithStatusValidator(sv),
        ),
    }
}

func (s *RepoService) List(ctx context.Context, username string) ([]*Repository, error) {
    path := fmt.Sprintf("/users/%s/repos", username)
    rsp, err := s.client.Get(ctx, path, nil)
    if err != nil {
        panic(err)
    }

    var repos []*Repository
    var apiErr *GithubError

    if err := rsp.DecodeJSON(On2xx(repos), On4xx(apiErr)); err != nil {
        return nil, err
    }

    return repos, apiErr
}

Inspiration

Inspired by a multitude of great HTTP clients, including but not limited to:

License

MIT License

Documentation

Index

Constants

View Source
const DefaultTimeout = 30 * time.Second

DefaultTimeout is the default time limit for requests made by the client.

Variables

View Source
var DefaultStatusValidator = func(status int) bool {
	return status >= 200 && status < 300
}

DefaultStatusValidator returns true for 2xx statuses, otherwise false.

Functions

This section is empty.

Types

type BadStatusError

type BadStatusError int

BadStatusError is returned if the client's status validator function returns false.

func (BadStatusError) Error

func (statusCode BadStatusError) Error() string

Error implements the error interface

type Client

type Client struct {
	BaseURL         string
	DefaultEncoder  Encoder
	StatusValidator func(int) bool
	BaseClient      Doer
}

Client is an HTTP client that uses the BaseClient to send requests

func New

func New(opts ...Option) *Client

New returns a new Client with sensible defaults. The defaults can be overridden by supplying Options.

func NewFromBaseClient

func NewFromBaseClient(baseClient Doer, opts ...Option) *Client

NewFromBaseClient returns a new Client that wraps BaseClient

func (*Client) Delete

func (c *Client) Delete(ctx context.Context, url string, body interface{}, v interface{}) (*Response, error)

Delete performs a DELETE request

func (*Client) Get

func (c *Client) Get(ctx context.Context, url string, v interface{}) (*Response, error)

Get performs a GET request

func (*Client) Patch

func (c *Client) Patch(ctx context.Context, url string, body interface{}, v interface{}) (*Response, error)

Patch performs a PATCH request

func (*Client) Post

func (c *Client) Post(ctx context.Context, url string, body interface{}, v interface{}) (*Response, error)

Post performs a POST request

func (*Client) Put

func (c *Client) Put(ctx context.Context, url string, body interface{}, v interface{}) (*Response, error)

Put performs a PUT request

func (*Client) Send

func (c *Client) Send(request *Request) *Future

Send performs the HTTP request and returns a Future

type ContentTypeError

type ContentTypeError string

ContentTypeError is returned if the response has a Content-Type header that cannot be handled by one of the built-in decoders and neither the client nor request have a default decoder set.

func (ContentTypeError) Error

func (contentType ContentTypeError) Error() string

Error implements the error interface

type DecodeHook

type DecodeHook func(status int) interface{}

func On2xx

func On2xx(v interface{}) DecodeHook

func On4xx

func On4xx(v interface{}) DecodeHook

func On5xx

func On5xx(v interface{}) DecodeHook

func OnNon2xx

func OnNon2xx(v interface{}) DecodeHook

func OnStatus

func OnStatus(status int, v interface{}) DecodeHook

type Decoder

type Decoder interface {
	Name() string
	Decode([]byte, interface{}) error
}

Decoder is the interface for types that can decode a response body.

type DecoderJSON

type DecoderJSON struct{}

DecoderJSON decodes JSON bodies

func (*DecoderJSON) Decode

func (d *DecoderJSON) Decode(data []byte, v interface{}) error

Decode unmarshals a JSON response body

func (*DecoderJSON) Name

func (d *DecoderJSON) Name() string

Name returns the name of the format

type Doer

type Doer interface {
	Do(*http.Request) (*http.Response, error)
}

Doer executes HTTP requests. It is implemented by http.Client{}. You can wrap an http.Client{} in a custom Doer implementation to create middleware.

type Encoder

type Encoder interface {
	ContentType() string
	Encode(interface{}) (io.Reader, error)
}

Encoder is the interface for types that can encode a request body.

type EncoderFormURL

type EncoderFormURL struct {
	// CustomContentType overrides the default ContentType
	// of application/x-www-form-urlencoded
	CustomContentType string

	// TagAlias is the tag to read on struct fields.
	// If empty, "form" will be used.
	TagAlias string
}

EncoderFormURL encodes bodies as x-www-form-urlencoded

func (EncoderFormURL) ContentType

func (e EncoderFormURL) ContentType() string

ContentType returns the ContentType header to set in an outbound request

func (EncoderFormURL) Encode

func (e EncoderFormURL) Encode(body interface{}) (io.Reader, error)

Encode marshals maps and simple structs to a URL-encoded body

type EncoderJSON

type EncoderJSON struct {
	// CustomContentType overrides the default ContentType
	// of application/json; charset=utf-8
	CustomContentType string
}

EncoderJSON encodes bodies as JSON

func (EncoderJSON) ContentType

func (e EncoderJSON) ContentType() string

ContentType returns the ContentType header to set in an outbound request

func (EncoderJSON) Encode

func (e EncoderJSON) Encode(body interface{}) (io.Reader, error)

Encode marshals an arbitrary data structure into JSON

type Future

type Future struct {
	// contains filtered or unexported fields
}

Future represents an in-flight request

func (*Future) Response

func (f *Future) Response() (*Response, error)

Response blocks until the response is available

type InvalidMethodError

type InvalidMethodError string

InvalidMethodError is returned if an unsupported HTTP method is specified

func (InvalidMethodError) Error

func (method InvalidMethodError) Error() string

Error implements the error interface

type Option

type Option func(c *Client)

func WithBaseURL

func WithBaseURL(url string) Option

func WithEncoder

func WithEncoder(enc Encoder) Option

func WithStatusValidator

func WithStatusValidator(f func(int) bool) Option

func WithTimeout

func WithTimeout(d time.Duration) Option

type Request

type Request struct {
	Ctx     context.Context
	Method  string
	URL     string
	Headers http.Header
	Body    interface{}
	Encoder Encoder
}

Request holds the information needed to make an HTTP request

type Response

type Response struct {
	*http.Response
}

Response represents the response from a request

func (*Response) BodyBytes

func (r *Response) BodyBytes() ([]byte, error)

BodyBytes returns the body as a byte slice

func (*Response) BodyString

func (r *Response) BodyString() (string, error)

BodyString returns the body as a string

func (*Response) Decode

func (r *Response) Decode(targets ...interface{}) error

func (*Response) DecodeJSON

func (r *Response) DecodeJSON(targets ...interface{}) error

func (*Response) DecodeUsing

func (r *Response) DecodeUsing(dec Decoder, targets ...interface{}) error

DecodeUsing decodes the response into the receivers using the given Decoder

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL