Skip to content
Snippets Groups Projects
Commit fb7848d1 authored by Christoph Glaubitz's avatar Christoph Glaubitz
Browse files

Prepared client for POST/PUT/PATCH/DELETE

1. Fixed issue with concating urls, removed use of path.Join

The problem here: With path.Join, the trailing slash has been removed on
each call. Even when using `NewRequest(..., "/api/XXX/", nil)`. As a result,
Do always queries `http://nebox.example.com/api/XXX`, getting back a 301
and retries the new Location. While this works well with GET, it breaks
all the pushing methods. To not override too much, or too less, of
`Client.u`, I append/prepend slashes if necessary.

2. Added two new functions to construct requests

* c.NewDataRequest to create a request with body
* c.NewJSONRequest to make posts more convenient
* Changed c.NewRequest to be a wrap for NewJSONRequest

As a result, using NewRequest stays the same, but NewJSONRequest can be
used to create "writing" functions, without the need of repeatedly
checking for errors. e.g. for `tenant-groups`:

```
func (s *TenantGroupsService) Update(group *TenantGroup) (*TenantGroup, error) {
	req, err := s.c.NewJSONRequest(
		http.MethodPatch,
		fmt.Sprintf("api/tenancy/tenant-groups/%d/", group.ID),
		nil,
		group)
	if err != nil {
		return nil, err
	}

	g := new(TenantGroup)
	err = s.c.Do(req, g)
	return g, err
}

func (s *TenantGroupsService) Delete(group *TenantGroup) error {
	req, err := s.c.NewRequest(
		http.MethodDelete,
		fmt.Sprintf("api/tenancy/tenant-groups/%d/", group.ID),
		nil)
	if err != nil {
		return err
	}

	return s.c.Do(req, nil)
}
```
parent 93500c8d
No related branches found
No related tags found
No related merge requests found
...@@ -15,12 +15,15 @@ ...@@ -15,12 +15,15 @@
package netbox package netbox
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"path" "strings"
) )
// A Client is a NetBox client. It can be used to retrieve network and // A Client is a NetBox client. It can be used to retrieve network and
...@@ -46,6 +49,13 @@ func NewClient(addr string, client *http.Client) (*Client, error) { ...@@ -46,6 +49,13 @@ func NewClient(addr string, client *http.Client) (*Client, error) {
client = &http.Client{} client = &http.Client{}
} }
// Append trailing slash there is none. This is necessary
// to be able to concat url parts in a correct manner.
// See NewRequest
if !strings.HasSuffix(addr, "/") {
addr = addr + "/"
}
u, err := url.Parse(addr) u, err := url.Parse(addr)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -68,25 +78,40 @@ func NewClient(addr string, client *http.Client) (*Client, error) { ...@@ -68,25 +78,40 @@ func NewClient(addr string, client *http.Client) (*Client, error) {
// If a nil Valuer is specified, no query parameters will be sent with the // If a nil Valuer is specified, no query parameters will be sent with the
// request. // request.
func (c *Client) NewRequest(method string, endpoint string, options Valuer) (*http.Request, error) { func (c *Client) NewRequest(method string, endpoint string, options Valuer) (*http.Request, error) {
rel, err := url.Parse(endpoint) return c.NewDataRequest(method, endpoint, options, nil)
if err != nil { }
return nil, err
}
// NewDataRequest creates a HTTP request using the input HTTP method, URL
// endpoint, a Valuer which creates URL parameters for the request, and
// a io.Reader as the body of the request.
//
// If a nil Valuer is specified, no query parameters will be sent with the
// request.
// If a nil io.Reader, no body will be sent with the request.
func (c *Client) NewDataRequest(method string, endpoint string, options Valuer, body io.Reader) (*http.Request, error) {
// Allow specifying a base path for API requests, so if a NetBox server // Allow specifying a base path for API requests, so if a NetBox server
// resides at a path like http://example.com/netbox/, API requests will // resides at a path like http://example.com/netbox/, API requests will
// be sent to http://example.com/netbox/api/... // be sent to http://example.com/netbox/api/...
// //
// Enables support of: https://github.com/digitalocean/netbox/issues/212. // Enables support of: https://github.com/digitalocean/netbox/issues/212.
if c.u.Path != "" { //
rel.Path = path.Join(c.u.Path, rel.Path) // Remove leading slash if there is one. This is necessary to be able to
// concat url parts in a correct manner. We can not use path.Join here,
// because this always trims the trailing slash, which causes the
// Do function to always run into 301 and then retrying the correct
// Location. With GET, it does work with one useless request, but it breaks
// each other http method.
// Doing this, because out-of-tree extensions are more robust.
rel, err := url.Parse(strings.TrimLeft(endpoint, "/"))
if err != nil {
return nil, err
} }
u := c.u.ResolveReference(rel) u := c.u.ResolveReference(rel)
// If no valuer specified, create a request with no query parameters // If no valuer specified, create a request with no query parameters
if options == nil { if options == nil {
return http.NewRequest(method, u.String(), nil) return http.NewRequest(method, u.String(), body)
} }
values, err := options.Values() values, err := options.Values()
...@@ -95,7 +120,38 @@ func (c *Client) NewRequest(method string, endpoint string, options Valuer) (*ht ...@@ -95,7 +120,38 @@ func (c *Client) NewRequest(method string, endpoint string, options Valuer) (*ht
} }
u.RawQuery = values.Encode() u.RawQuery = values.Encode()
return http.NewRequest(method, u.String(), nil) return http.NewRequest(method, u.String(), body)
}
// NewJSONRequest creates a HTTP request using the input HTTP method, URL
// endpoint, a Valuer which creates URL parameters for the request, and
// a io.Reader as the body of the request. For body, expecting some
// json.Marshal-able struct. nil body is not allowed.
// NewJSONRequest also sets HTTP Header
// "Content-Type: application/json; utf-8"
//
// If a nil Valuer is specified, no query parameters will be sent with the
// request.
func (c *Client) NewJSONRequest(method string, endpoint string, options Valuer, body interface{}) (*http.Request, error) {
if body == nil {
return nil, errors.New("expected body to be not nil")
}
b := new(bytes.Buffer)
err := json.NewEncoder(b).Encode(body)
if err != nil {
return nil, err
}
req, err := c.NewDataRequest(
method,
endpoint,
options,
b,
)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
return req, nil
} }
// Do executes an HTTP request and if v is not nil, Do unmarshals result // Do executes an HTTP request and if v is not nil, Do unmarshals result
......
...@@ -18,6 +18,7 @@ import ( ...@@ -18,6 +18,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
...@@ -95,6 +96,46 @@ func TestClientBadStatusCode(t *testing.T) { ...@@ -95,6 +96,46 @@ func TestClientBadStatusCode(t *testing.T) {
} }
} }
func TestNewJSONRequest(t *testing.T) {
c, done := testClient(t, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("foo"))
})
defer done()
wantBody := "{\"id\":1,\"name\":\"Test 1\"}\n"
wantHeader := "application/json; charset=utf-8"
req, err := c.NewJSONRequest(http.MethodPost, "/", nil, &struct {
ID int `json:"id"`
Name string `json:"name"`
}{
ID: 1,
Name: "Test 1",
})
if err != nil {
t.Fatal("expected no error, but an error returned")
}
res, err := ioutil.ReadAll(req.Body)
if err != nil {
t.Fatal("expected no error, but an error returned")
}
if want, got := wantBody, string(res); got != want {
t.Fatalf("unexpected body:\n- want: %v\n- got: %v", want, got)
}
if want, got := wantHeader, req.Header.Get("Content-Type"); got != want {
t.Fatalf("unexpected body:\n- want: %v\n- got: %v", want, got)
}
req, err = c.NewJSONRequest(http.MethodPost, "/", nil, nil)
if err == nil {
t.Fatal("expected an error, but there was none")
}
if req != nil {
t.Fatalf("expected a nil request, but got %v", req)
}
}
func TestClientQueryParameters(t *testing.T) { func TestClientQueryParameters(t *testing.T) {
c := &Client{ c := &Client{
u: &url.URL{}, u: &url.URL{},
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment