diff --git a/netbox/client.go b/netbox/client.go index 333fea6230b0fc8359cbf2543fc3a4633b14049a..1cf4c12cf127ccb486dd2414b6316faee983c603 100644 --- a/netbox/client.go +++ b/netbox/client.go @@ -15,12 +15,15 @@ package netbox import ( + "bytes" "encoding/json" + "errors" "fmt" + "io" "io/ioutil" "net/http" "net/url" - "path" + "strings" ) // 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) { 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) if err != nil { return nil, err @@ -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 // request. func (c *Client) NewRequest(method string, endpoint string, options Valuer) (*http.Request, error) { - rel, err := url.Parse(endpoint) - if err != nil { - return nil, err - } + return c.NewDataRequest(method, endpoint, options, nil) +} +// 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 // resides at a path like http://example.com/netbox/, API requests will // be sent to http://example.com/netbox/api/... // // 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) // If no valuer specified, create a request with no query parameters if options == nil { - return http.NewRequest(method, u.String(), nil) + return http.NewRequest(method, u.String(), body) } values, err := options.Values() @@ -95,7 +120,38 @@ func (c *Client) NewRequest(method string, endpoint string, options Valuer) (*ht } 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 diff --git a/netbox/client_test.go b/netbox/client_test.go index 75f8e0b33049584ba88ce4ceeb954b358a7aa67d..02d65f2ff7f1f4982a66248a0297e8e3fb5f884f 100644 --- a/netbox/client_test.go +++ b/netbox/client_test.go @@ -18,6 +18,7 @@ import ( "encoding/json" "errors" "fmt" + "io/ioutil" "net/http" "net/http/httptest" "net/url" @@ -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) { c := &Client{ u: &url.URL{},