gregoryv/trip

View on GitHub
command.go

Summary

Maintainability
A
0 mins
Test Coverage
// package trip implements a round-trip pattern for http requests
package trip

import (
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "net/http/httputil"
    "time"
)

const BadResponse = 590

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

type Command struct {
    Client   Client
    Request  *http.Request
    Response *http.Response
    // IsOk should return false if the response is considered wrong, according
    // to status and headers. The body should not be parsed.
    IsOk func(*http.Response) bool
    // Parse should convert the response into the given model. By default json.Unmarshal is
    // used. Parse should close the reader when done or on error.
    Parse func(io.ReadCloser, interface{}) error

    // Duration to wait before next retry
    Pause     time.Duration
    lastError error
}

// NewCommand returns a command using the http.DefaultClient.
// By default requests that have a status code larger or equal to 400 return an error
func NewCommand(request *http.Request) (cmd *Command) {
    cmd = &Command{
        Client:  http.DefaultClient,
        Request: request,
        Pause:   time.Second * 2,
    }
    // A response that is not ok will result in a BadResponse in the execution chain
    cmd.IsOk = func(r *http.Response) bool {
        // r is never nil, that is checked for us
        return r.StatusCode < 400
    }
    cmd.Parse = parseJson
    return
}

func parseJson(body io.ReadCloser, model interface{}) (err error) {
    defer body.Close()
    // Default parser is json to model
    var buf []byte
    buf, _ = ioutil.ReadAll(body) // Ignore the error
    err = json.Unmarshal(buf, model)
    return
}

// Run, calls the Output method with no model
func (cmd *Command) Run() (err error) {
    return cmd.Output(nil)
}

// Try, runs command with Run() and retries if it fails
func (cmd *Command) Try(times int) (err error) {
    for i := times; i > 0; i-- {
        err = cmd.Run()
        if err == nil {
            break
        }
        time.Sleep(cmd.Pause)
    }
    return
}

// Output sends the request and does a status validation against considered status codes.
// Failing to send the request altogether results in a 590. Parsing errors result in 591
func (cmd *Command) Output(model interface{}) (err error) {
    defer func() { cmd.lastError = err }()
    cmd.Response, err = cmd.Client.Do(cmd.Request)
    if err != nil {
        return err
    }
    if !cmd.IsOk(cmd.Response) {
        return fmt.Errorf("%s", cmd.Response.Status)
    }
    if model != nil {
        err = cmd.Parse(cmd.Response.Body, model)
        if err != nil {
            return err
        }
    }
    return err
}

// Dump writes request and response information, if body has already been read body=true has no affect.
func (cmd *Command) Dump(w io.Writer, body bool) {
    var dump []byte
    if cmd.Request != nil {
        dump, _ = httputil.DumpRequestOut(cmd.Request, body)
        fmt.Fprintf(w, "%s\n", dump)
    }
    if cmd.Response != nil {
        dump, _ = httputil.DumpResponse(cmd.Response, body)
        fmt.Fprintf(w, "%s\n", dump)
    }
}

func (cmd Command) Error() string {
    return cmd.lastError.Error()
}