Sugar is a DECLARATIVE http client providing elegant APIs for Golang.
- Elegant APIs
- Plugins
- Chained invocations
- Highly extensible
go get -add github.com/pojozhang/sugar
Firstly you need to import the package.
import . "github.com/pojozhang/sugar"
And now you are ready to easily send any request to any corner on this blue planet.
// POST /books HTTP/1.1
// Host: api.example.com
// Content-Type: text/plain
Post(ctx, "http://api.example.com/books", "bookA")
// GET /books/123 HTTP/1.1
// Host: api.example.com
Get(ctx, "http://api.example.com/books/:id", Path{"id": 123})
Get(ctx, "http://api.example.com/books/:id", P{"id": 123})
// GET /books?name=bookA HTTP/1.1
// Host: api.example.com
Get(ctx, "http://api.example.com/books", Query{"name": "bookA"})
Get(ctx, "http://api.example.com/books", Q{"name": "bookA"})
// list
// GET /books?name=bookA&name=bookB HTTP/1.1
// Host: api.example.com
Get(ctx, "http://api.example.com/books", Query{"name": List{"bookA", "bookB"}})
Get(ctx, "http://api.example.com/books", Q{"name": L{"bookA", "bookB"}})
// GET /books HTTP/1.1
// Host: api.example.com
// Cookie: name=bookA
Get(ctx, "http://api.example.com/books", Cookie{"name": "bookA"})
Get(ctx, "http://api.example.com/books", C{"name": "bookA"})
// GET /books HTTP/1.1
// Host: api.example.com
// Name: bookA
Get(ctx, "http://api.example.com/books", Header{"name": "bookA"})
Get(ctx, "http://api.example.com/books", H{"name": "bookA"})
// POST /books HTTP/1.1
// Host: api.example.com
// Content-Type: application/json;charset=UTF-8
// {"name":"bookA"}
Post(ctx, "http://api.example.com/books", Json{`{"name":"bookA"}`})
Post(ctx, "http://api.example.com/books", J{`{"name":"bookA"}`})
// map
Post(ctx, "http://api.example.com/books", Json{Map{"name": "bookA"}})
Post(ctx, "http://api.example.com/books", J{M{"name": "bookA"}})
// list
Post(ctx, "http://api.example.com/books", Json{List{Map{"name": "bookA"}}})
Post(ctx, "http://api.example.com/books", J{L{M{"name": "bookA"}}})
// POST /books HTTP/1.1
// Host: api.example.com
// Authorization: Basic dXNlcjpwYXNzd29yZA==
// Content-Type: application/xml; charset=UTF-8
// <book name="bookA"></book>
Post(ctx, "http://api.example.com/books", Xml{`<book name="bookA"></book>`})
Post(ctx, "http://api.example.com/books", X{`<book name="bookA"></book>`})
// POST /books HTTP/1.1
// Host: api.example.com
// Content-Type: application/x-www-form-urlencoded
Post(ctx, "http://api.example.com/books", Form{"name": "bookA"})
Post(ctx, "http://api.example.com/books", F{"name": "bookA"})
// list
Post(ctx, "http://api.example.com/books", Form{"name": List{"bookA", "bookB"}})
Post(ctx, "http://api.example.com/books", F{"name": L{"bookA", "bookB"}})
// DELETE /books HTTP/1.1
// Host: api.example.com
// Authorization: Basic dXNlcjpwYXNzd29yZA==
Delete(ctx, "http://api.example.com/books", User{"user", "password"})
Delete(ctx, "http://api.example.com/books", U{"user", "password"})
// POST /books HTTP/1.1
// Host: api.example.com
// Content-Type: multipart/form-data; boundary=19b8acc2469f1914a24fc6e0152aac72f1f92b6f5104b57477262816ab0f
//
// --19b8acc2469f1914a24fc6e0152aac72f1f92b6f5104b57477262816ab0f
// Content-Disposition: form-data; name="name"
//
// bookA
// --19b8acc2469f1914a24fc6e0152aac72f1f92b6f5104b57477262816ab0f
// Content-Disposition: form-data; name="file"; filename="text"
// Content-Type: application/octet-stream
//
// hello sugar!
// --19b8acc2469f1914a24fc6e0152aac72f1f92b6f5104b57477262816ab0f--
f, _ := os.Open("text")
Post(ctx, "http://api.example.com/books", MultiPart{"name": "bookA", "file": f})
Post(ctx, "http://api.example.com/books", MP{"name": "bookA", "file": f})
Due to Sugar's flexible design, different types of parameters can be freely combined.
Patch(ctx, "http://api.example.com/books/:id", Path{"id": 123}, Json{`{"name":"bookA"}`}, User{"user", "password"})
You can use Apply() to preset some values which will be attached to every following request. Call Reset() to clean preset values.
Apply(User{"user", "password"})
Get(ctx, "http://api.example.com/books")
Get(ctx, "http://api.example.com/books")
Reset()
Get(ctx, "http://api.example.com/books")
Get(ctx, "http://api.example.com/books", User{"user", "password"})
Get(ctx, "http://api.example.com/books", User{"user", "password"})
Get(ctx, "http://api.example.com/books")
The latter is equal to the former.
A request API always returns a value of type *Response
which also provides some nice APIs.
Raw() returns a value of type *http.Response
and an error
which is similar to standard go API.
resp, err := Post(ctx, "http://api.example.com/books", "bookA").Raw()
...
ReadBytes() is another syntax sugar to read bytes from response body. Notice that this method will close body after reading.
bytes, resp, err := Get(ctx, "http://api.example.com/books").ReadBytes()
...
Read() reads different types of response via decoder API. The following two examples read response body as plain text/JSON according to different content types.
// plain text
var text = new(string)
resp, err := Get(ctx, "http://api.example.com/text").Read(text)
// json
var books []book
resp, err := Get(ctx, "http://api.example.com/json").Read(&books)
You can also use Read() to download files.
f,_ := os.Create("tmp.png")
defer f.Close()
resp, err := Get(ctx, "http://api.example.com/logo.png").Read(f)
There are three major components in Sugar: Encoder, Decoder and Plugin.
- An encoder is used to encode your parameters and assemble requests.
- A decoder is used to decode the data from response body.
- A plugin is designed to work as an interceptor.
You can register your custom encoder which should implement Encoder
interface.
type MyEncoder struct {
}
func (r *MyEncoder) Encode(context *RequestContext, chain *EncoderChain) error {
myParams, ok := context.Param.(MyParam)
if !ok {
return chain.Next()
}
...
req := context.Request
...
return nil
}
Encoders.Add(&MyEncoder{})
Get(ctx, "http://api.example.com/books", MyParam{})
You can implement Decoder
interface so that you can convert a response body to a specific struct.
It is very convenient to get converted value via func (r *Response) Read(v interface{}) (*http.Response, error)
API.
type MyDecoder struct {
}
func (d *MyDecoder) Decode(context *ResponseContext, chain *DecoderChain) error {
// decode data from body if a content type named `my-content-type` is set in header
for _, contentType := range context.Response.Header[ContentType] {
if strings.Contains(strings.ToLower(contentType), "my-content-type") {
body, err := ioutil.ReadAll(context.Response.Body)
if err != nil {
return err
}
json.Unmarshal(body, context.Out)
...
return nil
}
}
return chain.Next()
}
Decoders.Add(&MyDecoder{})
Plugin is a new feature since V2. You can do anything as you like before the request is sent or after the response is received.
// Implementation of builtin Logger plugin
Use(func(c *Context) error {
b, _ := httputil.DumpRequest(c.Request, true)
log.Println(string(b))
defer func() {
if c.Response != nil {
b, _ := httputil.DumpResponse(c.Response, true)
log.Println(string(b))
}
}()
return c.Next()
})
You can use Logger plugin to log any request you send or any response you get.
Use(Logger)
You can use Retryer plugin to retry a request when the server returns 500 or when you get a net error.
Use(Retryer(3, time.Second, 1, time.Second))
Sometimes you may get an custom API error when a request is invalid. The following example shows you how to handle this situation via a plugin:
// your error struct
type apiError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (e apiError) Error() string {
return e.Message
}
Use(func(c *Context) error {
if err := c.Next(); err != nil {
return err
}
if c.Response != nil && c.Response.StatusCode >= http.StatusBadRequest {
defer func() { c.Response.Body.Close() }()
body, err := ioutil.ReadAll(c.Response.Body)
if err != nil {
return err
}
e := apiError{}
if err = json.Unmarshal(body, &e); err != nil {
return err
}
return e
}
return nil
})
// send request
_, err := client.Get(ctx, "some url").Read(&json{})
// type switch
switch e := err.(type) {
case apiError:
// your code
}
// your code