← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/gomaasapi-baseURL into lp:~maas-maintainers/maas/gomaasapi

 

Raphaël Badin has proposed merging lp:~rvb/maas/gomaasapi-baseURL into lp:~maas-maintainers/maas/gomaasapi.

Commit message:
Move BaseURL into the Client; add the AppendSlash utility to make sure we generate Django-compatible urls.

Requested reviews:
  MAAS Maintainers (maas-maintainers)

For more details, see:
https://code.launchpad.net/~rvb/maas/gomaasapi-baseURL/+merge/145180

- Base URL is now a field in the Client objects (and not the Server object [i.e. a maasobject representing a MAAS server]).
- use url.URL internally to manipulate url objects.
- AppendSlash allows us, when using GetSubObject, to create urls ending with a slash.  This is to keep Django [as configured by MAAS] happy (see https://docs.djangoproject.com/en/dev/ref/settings/#append-slash for details).
-- 
https://code.launchpad.net/~rvb/maas/gomaasapi-baseURL/+merge/145180
Your team MAAS Maintainers is requested to review the proposed merge of lp:~rvb/maas/gomaasapi-baseURL into lp:~maas-maintainers/maas/gomaasapi.
=== modified file 'client.go'
--- client.go	2013-01-28 08:18:15 +0000
+++ client.go	2013-01-28 13:30:59 +0000
@@ -13,7 +13,8 @@
 )
 
 type Client struct {
-	Signer OAuthSigner
+	BaseURL *url.URL
+	Signer  OAuthSigner
 }
 
 const (
@@ -37,7 +38,11 @@
 	return body, nil
 }
 
-func (client Client) Get(URL string, operation string, parameters url.Values) ([]byte, error) {
+func (client Client) GetURL(URI *url.URL) *url.URL {
+	return client.BaseURL.ResolveReference(URI)
+}
+
+func (client Client) Get(URI *url.URL, operation string, parameters url.Values) ([]byte, error) {
 	opParameter := parameters.Get(operationParamName)
 	if opParameter != "" {
 		errString := fmt.Sprintf("The parameters contain a value for '%s' which is reserved parameter.")
@@ -46,8 +51,9 @@
 	if operation != "" {
 		parameters.Set(operationParamName, operation)
 	}
-	queryUrl := URL + "?" + parameters.Encode()
-	request, err := http.NewRequest("GET", queryUrl, nil)
+	queryUrl := client.GetURL(URI)
+	queryUrl.RawQuery = parameters.Encode()
+	request, err := http.NewRequest("GET", queryUrl.String(), nil)
 	if err != nil {
 		return nil, err
 	}
@@ -55,30 +61,29 @@
 }
 
 // nonIdempotentRequest is a utility method to issue a PUT or a POST request.
-func (client Client) nonIdempotentRequest(method string, URL string, parameters url.Values) ([]byte, error) {
-	request, err := http.NewRequest(method, URL, strings.NewReader(string(parameters.Encode())))
+func (client Client) nonIdempotentRequest(method string, URI *url.URL, parameters url.Values) ([]byte, error) {
+	URL := client.GetURL(URI)
+	request, err := http.NewRequest(method, URL.String(), strings.NewReader(string(parameters.Encode())))
 	if err != nil {
 		return nil, err
 	}
 	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-	if err != nil {
-		return nil, err
-	}
 	return client.dispatchRequest(request)
 }
 
-func (client Client) Post(URL string, operation string, parameters url.Values) ([]byte, error) {
+func (client Client) Post(URI *url.URL, operation string, parameters url.Values) ([]byte, error) {
 	queryParams := url.Values{operationParamName: {operation}}
-	queryURL := URL + "?" + queryParams.Encode()
-	return client.nonIdempotentRequest("POST", queryURL, parameters)
-}
-
-func (client Client) Put(URL string, parameters url.Values) ([]byte, error) {
-	return client.nonIdempotentRequest("PUT", URL, parameters)
-}
-
-func (client Client) Delete(URL string) error {
-	request, err := http.NewRequest("DELETE", URL, strings.NewReader(""))
+	URI.RawQuery = queryParams.Encode()
+	return client.nonIdempotentRequest("POST", URI, parameters)
+}
+
+func (client Client) Put(URI *url.URL, parameters url.Values) ([]byte, error) {
+	return client.nonIdempotentRequest("PUT", URI, parameters)
+}
+
+func (client Client) Delete(URI *url.URL) error {
+	URL := client.GetURL(URI)
+	request, err := http.NewRequest("DELETE", URL.String(), strings.NewReader(""))
 	if err != nil {
 		return err
 	}
@@ -99,14 +104,18 @@
 var _ OAuthSigner = (*anonSigner)(nil)
 
 // NewAnonymousClient creates a client that issues anonymous requests.
-func NewAnonymousClient() (*Client, error) {
-	return &Client{Signer: &anonSigner{}}, nil
+func NewAnonymousClient(BaseURL string) (*Client, error) {
+	parsedBaseURL, err := url.Parse(BaseURL)
+	if err != nil {
+		return nil, err
+	}
+	return &Client{Signer: &anonSigner{}, BaseURL: parsedBaseURL}, nil
 }
 
 // NewAuthenticatedClient parses the given MAAS API key into the individual
 // OAuth tokens and creates an Client that will use these tokens to sign the
 // requests it issues.
-func NewAuthenticatedClient(apiKey string) (*Client, error) {
+func NewAuthenticatedClient(BaseURL string, apiKey string) (*Client, error) {
 	elements := strings.Split(apiKey, ":")
 	if len(elements) != 3 {
 		errString := "Invalid API key. The format of the key must be \"<consumer secret>:<token key>:<token secret>\"."
@@ -124,5 +133,9 @@
 	if err != nil {
 		return nil, err
 	}
-	return &Client{Signer: signer}, nil
+	parsedBaseURL, err := url.Parse(BaseURL)
+	if err != nil {
+		return nil, err
+	}
+	return &Client{Signer: signer, BaseURL: parsedBaseURL}, nil
 }

=== modified file 'client_test.go'
--- client_test.go	2013-01-25 17:49:25 +0000
+++ client_test.go	2013-01-28 13:30:59 +0000
@@ -13,9 +13,9 @@
 func (suite *GomaasapiTestSuite) TestClientdispatchRequestReturnsError(c *C) {
 	URI := "/some/url/?param1=test"
 	expectedResult := "expected:result"
-	client, _ := NewAnonymousClient()
 	server := newSingleServingServer(URI, expectedResult, http.StatusBadRequest)
 	defer server.Close()
+	client, _ := NewAnonymousClient(server.URL)
 	request, err := http.NewRequest("GET", server.URL+URI, nil)
 
 	result, err := client.dispatchRequest(request)
@@ -27,9 +27,9 @@
 func (suite *GomaasapiTestSuite) TestClientdispatchRequestSignsRequest(c *C) {
 	URI := "/some/url/?param1=test"
 	expectedResult := "expected:result"
-	client, _ := NewAuthenticatedClient("the:api:key")
 	server := newSingleServingServer(URI, expectedResult, http.StatusOK)
 	defer server.Close()
+	client, _ := NewAuthenticatedClient(server.URL, "the:api:key")
 	request, err := http.NewRequest("GET", server.URL+URI, nil)
 
 	result, err := client.dispatchRequest(request)
@@ -40,44 +40,44 @@
 }
 
 func (suite *GomaasapiTestSuite) TestClientGetFormatsGetParameters(c *C) {
-	URI := "/some/url"
+	URI, _ := url.Parse("/some/url")
 	expectedResult := "expected:result"
-	client, _ := NewAnonymousClient()
 	params := url.Values{"test": {"123"}}
-	fullURI := URI + "?test=123"
+	fullURI := URI.String() + "?test=123"
 	server := newSingleServingServer(fullURI, expectedResult, http.StatusOK)
 	defer server.Close()
+	client, _ := NewAnonymousClient(server.URL)
 
-	result, err := client.Get(server.URL+URI, "", params)
+	result, err := client.Get(URI, "", params)
 
 	c.Check(err, IsNil)
 	c.Check(string(result), Equals, expectedResult)
 }
 
 func (suite *GomaasapiTestSuite) TestClientGetFormatsOperationAsGetParameter(c *C) {
-	URI := "/some/url"
+	URI, _ := url.Parse("/some/url")
 	expectedResult := "expected:result"
-	client, _ := NewAnonymousClient()
-	fullURI := URI + "?op=list"
+	fullURI := URI.String() + "?op=list"
 	server := newSingleServingServer(fullURI, expectedResult, http.StatusOK)
 	defer server.Close()
+	client, _ := NewAnonymousClient(server.URL)
 
-	result, err := client.Get(server.URL+URI, "list", url.Values{})
+	result, err := client.Get(URI, "list", url.Values{})
 
 	c.Check(err, IsNil)
 	c.Check(string(result), Equals, expectedResult)
 }
 
 func (suite *GomaasapiTestSuite) TestClientPostSendsRequest(c *C) {
-	URI := "/some/url"
+	URI, _ := url.Parse("/some/url")
 	expectedResult := "expected:result"
-	client, _ := NewAnonymousClient()
-	fullURI := URI + "?op=list"
+	fullURI := URI.String() + "?op=list"
 	params := url.Values{"test": {"123"}}
 	server := newSingleServingServer(fullURI, expectedResult, http.StatusOK)
 	defer server.Close()
+	client, _ := NewAnonymousClient(server.URL)
 
-	result, err := client.Post(server.URL+URI, "list", params)
+	result, err := client.Post(URI, "list", params)
 
 	c.Check(err, IsNil)
 	c.Check(string(result), Equals, expectedResult)
@@ -85,14 +85,14 @@
 }
 
 func (suite *GomaasapiTestSuite) TestClientPutSendsRequest(c *C) {
-	URI := "/some/url"
+	URI, _ := url.Parse("/some/url")
 	expectedResult := "expected:result"
-	client, _ := NewAnonymousClient()
 	params := url.Values{"test": {"123"}}
-	server := newSingleServingServer(URI, expectedResult, http.StatusOK)
+	server := newSingleServingServer(URI.String(), expectedResult, http.StatusOK)
 	defer server.Close()
+	client, _ := NewAnonymousClient(server.URL)
 
-	result, err := client.Put(server.URL+URI, params)
+	result, err := client.Put(URI, params)
 
 	c.Check(err, IsNil)
 	c.Check(string(result), Equals, expectedResult)
@@ -100,13 +100,13 @@
 }
 
 func (suite *GomaasapiTestSuite) TestClientDeleteSendsRequest(c *C) {
-	URI := "/some/url"
+	URI, _ := url.Parse("/some/url")
 	expectedResult := "expected:result"
-	client, _ := NewAnonymousClient()
-	server := newSingleServingServer(URI, expectedResult, http.StatusOK)
+	server := newSingleServingServer(URI.String(), expectedResult, http.StatusOK)
 	defer server.Close()
+	client, _ := NewAnonymousClient(server.URL)
 
-	err := client.Delete(server.URL + URI)
+	err := client.Delete(URI)
 
 	c.Check(err, IsNil)
 }
@@ -120,7 +120,7 @@
 	keyElements := []string{consumerKey, tokenKey, tokenSecret}
 	apiKey := strings.Join(keyElements, ":")
 
-	client, err := NewAuthenticatedClient(apiKey)
+	client, err := NewAuthenticatedClient("http://example.com/api";, apiKey)
 
 	c.Check(err, IsNil)
 	signer := client.Signer.(_PLAINTEXTOAuthSigner)
@@ -130,7 +130,7 @@
 }
 
 func (suite *GomaasapiTestSuite) TestNewAuthenticatedClientFailsIfInvalidKey(c *C) {
-	client, err := NewAuthenticatedClient("invalid-key")
+	client, err := NewAuthenticatedClient("", "invalid-key")
 
 	c.Check(err, ErrorMatches, "Invalid API key.*")
 	c.Check(client, IsNil)

=== modified file 'example/live_example.go'
--- example/live_example.go	2013-01-28 08:18:15 +0000
+++ example/live_example.go	2013-01-28 13:30:59 +0000
@@ -26,14 +26,14 @@
 }
 
 func main() {
-	authClient, err := gomaasapi.NewAuthenticatedClient(apiKey)
+	authClient, err := gomaasapi.NewAuthenticatedClient(apiURL, apiKey)
 	if err != nil {
 		panic(err)
 	}
 
-	maas, err := gomaasapi.NewMAAS(apiURL, *authClient)
+	maas, err := gomaasapi.NewMAAS(*authClient)
 
-	nodeListing := maas.GetSubObject("/nodes/")
+	nodeListing := maas.GetSubObject("nodes")
 
 	// List nodes.
 	fmt.Println("Fetching list of nodes...")
@@ -46,7 +46,8 @@
 	for index, nodeObj := range listNodes {
 		node, _ := nodeObj.GetMAASObject()
 		hostname, _ := node.GetField("hostname")
-		fmt.Printf("Node #%d is named '%v' (%v)\n", index, hostname, node.URL())
+		nodeURL, _ := node.URL()
+		fmt.Printf("Node #%d is named '%v' (%v)\n", index, hostname, nodeURL)
 	}
 
 	// Create a node.
@@ -58,7 +59,8 @@
 	}
 	newNode, _ := newNodeObj.GetMAASObject()
 	newNodeName, _ := newNode.GetField("hostname")
-	fmt.Printf("New node created: %s (%s)\n", newNodeName, newNode.URL())
+	newNodeURL, _ := newNode.URL()
+	fmt.Printf("New node created: %s (%s)\n", newNodeName, newNodeURL)
 
 	// Update the new node.
 	fmt.Println("Updating the new node...")

=== modified file 'jsonobject.go'
--- jsonobject.go	2013-01-25 17:49:25 +0000
+++ jsonobject.go	2013-01-28 13:30:59 +0000
@@ -66,7 +66,7 @@
 // JSONObject (with the appropriate implementation of course).
 // This function is recursive.  Maps and arrays are deep-copied, with each
 // individual value being converted to a JSONObject type.
-func maasify(client Client, baseURL string, value interface{}) JSONObject {
+func maasify(client Client, value interface{}) JSONObject {
 	if value == nil {
 		return nil
 	}
@@ -81,19 +81,19 @@
 		original := value.(map[string]interface{})
 		result := make(map[string]JSONObject, len(original))
 		for key, value := range original {
-			result[key] = maasify(client, baseURL, value)
+			result[key] = maasify(client, value)
 		}
 		if _, ok := result[resource_uri]; ok {
 			// If the map contains "resource-uri", we can treat
 			// it as a MAAS object.
-			return jsonMAASObject{result, client, baseURL}
+			return jsonMAASObject{result, client}
 		}
 		return jsonMap(result)
 	case []interface{}:
 		original := value.([]interface{})
 		result := make([]JSONObject, len(original))
 		for index, value := range original {
-			result[index] = maasify(client, baseURL, value)
+			result[index] = maasify(client, value)
 		}
 		return jsonArray(result)
 	case bool:
@@ -104,13 +104,13 @@
 }
 
 // Parse a JSON blob into a JSONObject.
-func Parse(client Client, baseURL string, input []byte) (JSONObject, error) {
+func Parse(client Client, input []byte) (JSONObject, error) {
 	var obj interface{}
 	err := json.Unmarshal(input, &obj)
 	if err != nil {
 		return nil, err
 	}
-	return maasify(client, baseURL, obj), nil
+	return maasify(client, obj), nil
 }
 
 // Return error value for failed type conversion.

=== modified file 'jsonobject_test.go'
--- jsonobject_test.go	2013-01-25 17:49:25 +0000
+++ jsonobject_test.go	2013-01-28 13:30:59 +0000
@@ -9,37 +9,37 @@
 
 // maasify() converts nil.
 func (suite *GomaasapiTestSuite) TestMaasifyConvertsNil(c *C) {
-	c.Check(maasify(Client{}, "", nil), Equals, nil)
+	c.Check(maasify(Client{}, nil), Equals, nil)
 }
 
 // maasify() converts strings.
 func (suite *GomaasapiTestSuite) TestMaasifyConvertsString(c *C) {
 	const text = "Hello"
-	c.Check(string(maasify(Client{}, "", text).(jsonString)), Equals, text)
+	c.Check(string(maasify(Client{}, text).(jsonString)), Equals, text)
 }
 
 // maasify() converts float64 numbers.
 func (suite *GomaasapiTestSuite) TestMaasifyConvertsNumber(c *C) {
 	const number = 3.1415926535
-	c.Check(float64(maasify(Client{}, "", number).(jsonFloat64)), Equals, number)
+	c.Check(float64(maasify(Client{}, number).(jsonFloat64)), Equals, number)
 }
 
 // Any number converts to float64, even integers.
 func (suite *GomaasapiTestSuite) TestMaasifyConvertsIntegralNumber(c *C) {
 	const number = 1
-	c.Check(float64(maasify(Client{}, "", number).(jsonFloat64)), Equals, float64(number))
+	c.Check(float64(maasify(Client{}, number).(jsonFloat64)), Equals, float64(number))
 }
 
 // maasify() converts array slices.
 func (suite *GomaasapiTestSuite) TestMaasifyConvertsArray(c *C) {
 	original := []interface{}{3.0, 2.0, 1.0}
-	output := maasify(Client{}, "", original).(jsonArray)
+	output := maasify(Client{}, original).(jsonArray)
 	c.Check(len(output), Equals, len(original))
 }
 
 // When maasify() converts an array slice, the result contains JSONObjects.
 func (suite *GomaasapiTestSuite) TestMaasifyArrayContainsJSONObjects(c *C) {
-	arr := maasify(Client{}, "", []interface{}{9.9}).(jsonArray)
+	arr := maasify(Client{}, []interface{}{9.9}).(jsonArray)
 	var entry JSONObject
 	entry = arr[0]
 	c.Check((float64)(entry.(jsonFloat64)), Equals, 9.9)
@@ -48,13 +48,13 @@
 // maasify() converts maps.
 func (suite *GomaasapiTestSuite) TestMaasifyConvertsMap(c *C) {
 	original := map[string]interface{}{"1": "one", "2": "two", "3": "three"}
-	output := maasify(Client{}, "", original).(jsonMap)
+	output := maasify(Client{}, original).(jsonMap)
 	c.Check(len(output), Equals, len(original))
 }
 
 // When maasify() converts a map, the result contains JSONObjects.
 func (suite *GomaasapiTestSuite) TestMaasifyMapContainsJSONObjects(c *C) {
-	mp := maasify(Client{}, "", map[string]interface{}{"key": "value"}).(jsonMap)
+	mp := maasify(Client{}, map[string]interface{}{"key": "value"}).(jsonMap)
 	var entry JSONObject
 	entry = mp["key"]
 	c.Check((string)(entry.(jsonString)), Equals, "value")
@@ -66,67 +66,59 @@
 		"resource_uri": "http://example.com/foo";,
 		"size":         "3",
 	}
-	output := maasify(Client{}, "", original).(jsonMAASObject)
+	output := maasify(Client{}, original).(jsonMAASObject)
 	c.Check(len(output.jsonMap), Equals, len(original))
 	c.Check((string)(output.jsonMap["size"].(jsonString)), Equals, "3")
 }
 
-// maasify() passes its info (client and baseURL) to a MAASObject it creates.
+// maasify() passes its client to a MAASObject it creates.
 func (suite *GomaasapiTestSuite) TestMaasifyPassesInfoToMAASObject(c *C) {
 	client := Client{}
 	original := map[string]interface{}{"resource_uri": "/foo"}
-	baseURL := "http://example.com";
-	output := maasify(client, baseURL, original).(jsonMAASObject)
+	output := maasify(client, original).(jsonMAASObject)
 	c.Check(output.client, Equals, client)
-	c.Check(output.baseURL, Equals, baseURL)
 }
 
-// maasify() passes its info (client and baseURL) into an array of MAASObjects it creates.
+// maasify() passes its client into an array of MAASObjects it creates.
 func (suite *GomaasapiTestSuite) TestMaasifyPassesInfoIntoArray(c *C) {
 	client := Client{}
 	obj := map[string]interface{}{"resource_uri": "/foo"}
-	baseURL := "http://example.com";
 	list := []interface{}{obj}
-	output := maasify(client, baseURL, list).(jsonArray)
+	output := maasify(client, list).(jsonArray)
 	c.Check(output[0].(jsonMAASObject).client, Equals, client)
-	c.Check(output[0].(jsonMAASObject).baseURL, Equals, baseURL)
 }
 
-// maasify() passes its info (client and baseURL) into a map of MAASObjects it creates.
+// maasify() passes its client into a map of MAASObjects it creates.
 func (suite *GomaasapiTestSuite) TestMaasifyPassesInfoIntoMap(c *C) {
 	client := Client{}
 	obj := map[string]interface{}{"resource_uri": "/foo"}
-	baseURL := "http://example.com";
 	mp := map[string]interface{}{"key": obj}
-	output := maasify(client, baseURL, mp).(jsonMap)
+	output := maasify(client, mp).(jsonMap)
 	c.Check(output["key"].(jsonMAASObject).client, Equals, client)
-	c.Check(output["key"].(jsonMAASObject).baseURL, Equals, baseURL)
 }
 
-// maasify() passes its info (client and baseURL) all the way down into any 
-// MAASObjects in the object structure it creates.
+// maasify() passes its client all the way down into any MAASObjects in the
+// object structure it creates.
 func (suite *GomaasapiTestSuite) TestMaasifyPassesInfoAllTheWay(c *C) {
 	client := Client{}
 	obj := map[string]interface{}{"resource_uri": "/foo"}
-	baseURL := "http://example.com";
 	mp := map[string]interface{}{"key": obj}
 	list := []interface{}{mp}
-	output := maasify(client, baseURL, list).(jsonArray)
+	output := maasify(client, list).(jsonArray)
 	maasobj := output[0].(jsonMap)["key"]
 	c.Check(maasobj.(jsonMAASObject).client, Equals, client)
-	c.Check(maasobj.(jsonMAASObject).baseURL, Equals, baseURL)
 }
 
 // maasify() converts Booleans.
 func (suite *GomaasapiTestSuite) TestMaasifyConvertsBool(c *C) {
-	c.Check(bool(maasify(Client{}, "", true).(jsonBool)), Equals, true)
-	c.Check(bool(maasify(Client{}, "", false).(jsonBool)), Equals, false)
+	c.Check(bool(maasify(Client{}, true).(jsonBool)), Equals, true)
+	c.Check(bool(maasify(Client{}, false).(jsonBool)), Equals, false)
 }
 
 // Parse takes you from a JSON blob to a JSONObject.
 func (suite *GomaasapiTestSuite) TestParseMaasifiesJSONBlob(c *C) {
 	blob := []byte("[12]")
-	obj, err := Parse(Client{}, "", blob)
+	obj, err := Parse(Client{}, blob)
 	c.Check(err, IsNil)
 	c.Check(float64(obj.(jsonArray)[0].(jsonFloat64)), Equals, 12.0)
 }

=== modified file 'maasobject.go'
--- maasobject.go	2013-01-28 08:18:15 +0000
+++ maasobject.go	2013-01-28 13:30:59 +0000
@@ -18,9 +18,11 @@
 
 	// Utility method to extract a string field from this MAAS object.
 	GetField(name string) (string, error)
+	// URL for this MAAS object.
+	URL() (*url.URL, error)
 	// Resource URI for this MAAS object.
-	URL() string
-	// Retrieve the MAAS object located at thisObject.URL()+name.
+	URI() (*url.URL, error)
+	// Retrieve the MAAS object located at thisObject.URI()+name.
 	GetSubObject(name string) MAASObject
 	// Retrieve this MAAS object.
 	Get() (MAASObject, error)
@@ -43,8 +45,7 @@
 // jsonMAASObject implements both JSONObject and MAASObject.
 type jsonMAASObject struct {
 	jsonMap
-	client  Client
-	baseURL string
+	client Client
 }
 
 var _ JSONObject = (*jsonMAASObject)(nil)
@@ -65,39 +66,48 @@
 	return obj.jsonMap[name].GetString()
 }
 
-func (obj jsonMAASObject) _URI() (string, error) {
+func (obj jsonMAASObject) URI() (*url.URL, error) {
 	contents, err := obj.GetMap()
 	if err != nil {
 		panic("Unexpected failure converting jsonMAASObject to maasMap.")
 	}
-	return contents[resource_uri].GetString()
+	urlString, err := contents[resource_uri].GetString()
+	if err != nil {
+		return &url.URL{}, err
+	}
+	return url.Parse(urlString)
 }
 
-func (obj jsonMAASObject) URL() string {
-	uri, err := obj._URI()
+func (obj jsonMAASObject) URL() (*url.URL, error) {
+	uri, err := obj.URI()
 	if err != nil {
-		panic("Unexpected failure reading jsonMAASObject's URL.")
+		return &url.URL{}, err
 	}
-	return obj.baseURL + uri
+	return obj.client.GetURL(uri), nil
 }
 
 func (obj jsonMAASObject) GetSubObject(name string) MAASObject {
-	uri, err := obj._URI()
+	uri, err := obj.URI()
 	if err != nil {
 		panic("Unexpected failure reading jsonMAASObject's URL.")
 	}
-	input := map[string]JSONObject{resource_uri: jsonString(uri + name)}
-	return jsonMAASObject{jsonMap: jsonMap(input), client: obj.client, baseURL: obj.baseURL}
+	uri.Path = AppendSlash(JoinURLs(uri.Path, name))
+	input := map[string]JSONObject{resource_uri: jsonString(uri.String())}
+	return jsonMAASObject{jsonMap: jsonMap(input), client: obj.client}
 }
 
 var NotImplemented = errors.New("Not implemented")
 
 func (obj jsonMAASObject) Get() (MAASObject, error) {
-	result, err := obj.client.Get(obj.URL(), "", url.Values{})
-	if err != nil {
-		return nil, err
-	}
-	jsonObj, err := Parse(obj.client, obj.baseURL, result)
+	uri, err := obj.URI()
+	if err != nil {
+		return nil, err
+	}
+	result, err := obj.client.Get(uri, "", url.Values{})
+	if err != nil {
+		return nil, err
+	}
+	jsonObj, err := Parse(obj.client, result)
 	if err != nil {
 		return nil, err
 	}
@@ -105,19 +115,27 @@
 }
 
 func (obj jsonMAASObject) Post(params url.Values) (JSONObject, error) {
-	result, err := obj.client.Post(obj.URL(), "", params)
-	if err != nil {
-		return nil, err
-	}
-	return Parse(obj.client, obj.baseURL, result)
+	uri, err := obj.URI()
+	if err != nil {
+		return nil, err
+	}
+	result, err := obj.client.Post(uri, "", params)
+	if err != nil {
+		return nil, err
+	}
+	return Parse(obj.client, result)
 }
 
 func (obj jsonMAASObject) Update(params url.Values) (MAASObject, error) {
-	result, err := obj.client.Put(obj.URL(), params)
-	if err != nil {
-		return nil, err
-	}
-	jsonObj, err := Parse(obj.client, obj.baseURL, result)
+	uri, err := obj.URI()
+	if err != nil {
+		return nil, err
+	}
+	result, err := obj.client.Put(uri, params)
+	if err != nil {
+		return nil, err
+	}
+	jsonObj, err := Parse(obj.client, result)
 	if err != nil {
 		return nil, err
 	}
@@ -125,21 +143,33 @@
 }
 
 func (obj jsonMAASObject) Delete() error {
-	return obj.client.Delete(obj.URL())
+	uri, err := obj.URI()
+	if err != nil {
+		return err
+	}
+	return obj.client.Delete(uri)
 }
 
 func (obj jsonMAASObject) CallGet(operation string, params url.Values) (JSONObject, error) {
-	result, err := obj.client.Get(obj.URL(), operation, params)
-	if err != nil {
-		return nil, err
-	}
-	return Parse(obj.client, obj.baseURL, result)
+	uri, err := obj.URI()
+	if err != nil {
+		return nil, err
+	}
+	result, err := obj.client.Get(uri, operation, params)
+	if err != nil {
+		return nil, err
+	}
+	return Parse(obj.client, result)
 }
 
 func (obj jsonMAASObject) CallPost(operation string, params url.Values) (JSONObject, error) {
-	result, err := obj.client.Post(obj.URL(), operation, params)
-	if err != nil {
-		return nil, err
-	}
-	return Parse(obj.client, obj.baseURL, result)
+	uri, err := obj.URI()
+	if err != nil {
+		return nil, err
+	}
+	result, err := obj.client.Post(uri, operation, params)
+	if err != nil {
+		return nil, err
+	}
+	return Parse(obj.client, result)
 }

=== modified file 'maasobject_test.go'
--- maasobject_test.go	2013-01-28 08:18:15 +0000
+++ maasobject_test.go	2013-01-28 13:30:59 +0000
@@ -7,6 +7,7 @@
 	"fmt"
 	. "launchpad.net/gocheck"
 	"math/rand"
+	"net/url"
 )
 
 func makeFakeResourceURI() string {
@@ -45,19 +46,33 @@
 }
 
 func (suite *GomaasapiTestSuite) TestURL(c *C) {
+	baseURL, _ := url.Parse("http://example.com/";)
 	uri := "http://example.com/a/resource";
+	resourceURL, _ := url.Parse(uri)
 	input := map[string]JSONObject{resource_uri: jsonString(uri)}
-	obj := jsonMAASObject{jsonMap: jsonMap(input)}
-	c.Check(obj.URL(), Equals, uri)
+	client := Client{BaseURL: baseURL}
+	obj := jsonMAASObject{jsonMap: jsonMap(input), client: client}
+
+	URL, err := obj.URL()
+
+	c.Check(err, IsNil)
+	c.Check(URL, DeepEquals, resourceURL)
 }
 
 func (suite *GomaasapiTestSuite) TestGetSubObject(c *C) {
-	uri := "http://example.com/a/resource";
+	uri := "http://example.com/a/resource/";
 	input := map[string]JSONObject{resource_uri: jsonString(uri)}
 	obj := jsonMAASObject{jsonMap: jsonMap(input)}
 	subName := "/test"
+
 	subObj := obj.GetSubObject(subName)
-	c.Check(subObj.URL(), Equals, uri+subName)
+	subURL, err := subObj.URL()
+
+	c.Check(err, IsNil)
+	// uri ends with a slash and subName starts with one, but the two paths
+	// should be concatenated as "http://example.com/a/resource/test/";.
+	expectedSubURL, _ := url.Parse("http://example.com/a/resource/test/";)
+	c.Check(subURL, DeepEquals, expectedSubURL)
 }
 
 func (suite *GomaasapiTestSuite) TestGetField(c *C) {

=== modified file 'server.go'
--- server.go	2013-01-28 08:18:15 +0000
+++ server.go	2013-01-28 13:30:59 +0000
@@ -3,19 +3,8 @@
 
 package gomaasapi
 
-import (
-	"fmt"
-	"net/url"
-)
-
 // NewMAAS returns an interface to the MAAS API as a MAASObject.
-func NewMAAS(URL string, client Client) (MAASObject, error) {
-	parsed, err := url.Parse(URL)
-	if err != nil {
-		return nil, err
-	}
-	baseURL := fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host)
-	resourceURI := parsed.Path
-	input := map[string]JSONObject{resource_uri: jsonString(resourceURI)}
-	return jsonMAASObject{jsonMap: jsonMap(input), client: client, baseURL: baseURL}, nil
+func NewMAAS(client Client) (MAASObject, error) {
+	input := map[string]JSONObject{resource_uri: jsonString(client.BaseURL.String())}
+	return jsonMAASObject{jsonMap: jsonMap(input), client: client}, nil
 }

=== modified file 'server_test.go'
--- server_test.go	2013-01-28 08:18:15 +0000
+++ server_test.go	2013-01-28 13:30:59 +0000
@@ -5,16 +5,16 @@
 
 import (
 	. "launchpad.net/gocheck"
+	"net/url"
 )
 
-func (suite *GomaasapiTestSuite) TestNewMAASParsesURL(c *C) {
-	maas, err := NewMAAS("https://server.com:888/path/to/api";, Client{})
-
-	c.Check(err, IsNil)
-	c.Check(maas.URL(), Equals, "https://server.com:888/path/to/api";)
-	jsonObj := maas.(jsonMAASObject)
-	uri, err := jsonObj._URI()
-	c.Check(err, IsNil)
-	c.Check(uri, Equals, "/path/to/api")
-	c.Check(jsonObj.baseURL, Equals, "https://server.com:888";)
+func (suite *GomaasapiTestSuite) TestNewMAASUsesBaseURLFromClient(c *C) {
+	baseURLString := "https://server.com:888/path/to/api";
+	baseURL, _ := url.Parse(baseURLString)
+	client := Client{BaseURL: baseURL}
+	maas, err := NewMAAS(client)
+	c.Check(err, IsNil)
+	URL, err := maas.URL()
+	c.Check(err, IsNil)
+	c.Check(URL, DeepEquals, baseURL)
 }

=== modified file 'util.go'
--- util.go	2013-01-28 09:37:10 +0000
+++ util.go	2013-01-28 13:30:59 +0000
@@ -14,3 +14,15 @@
 func JoinURLs(baseURL, path string) string {
 	return strings.TrimRight(baseURL, "/") + "/" + strings.TrimLeft(path, "/")
 }
+
+// AppendSlash appends a slash at the end of the given string unless there
+// already is one.
+// This is used to create the kind of normalized URLs that Django expects.
+// (to avoid Django's redirection when an URL does not ends with a slash.)
+func AppendSlash(URL string) string {
+	length := len(URL)
+	if length > 0 && string(URL[length-1]) == "/" {
+		return URL
+	}
+	return URL + "/"
+}

=== modified file 'util_test.go'
--- util_test.go	2013-01-28 09:15:13 +0000
+++ util_test.go	2013-01-28 13:30:59 +0000
@@ -18,3 +18,15 @@
 func (suite *GomaasapiTestSuite) TestJoinURLsNormalizesDoubleSlash(c *C) {
 	c.Check(JoinURLs("http://example.com/base/";, "/szot"), Equals, "http://example.com/base/szot";)
 }
+
+func (suite *GomaasapiTestSuite) TestAppendSlashAppendsSlashIfMissing(c *C) {
+	c.Check(AppendSlash("test"), Equals, "test/")
+}
+
+func (suite *GomaasapiTestSuite) TestAppendSlashDoesNotAppendsIfPresent(c *C) {
+	c.Check(AppendSlash("test/"), Equals, "test/")
+}
+
+func (suite *GomaasapiTestSuite) TestAppendSlashReturnsSlashIfEmpty(c *C) {
+	c.Check(AppendSlash(""), Equals, "/")
+}


Follow ups