← Back to team overview

launchpad-reviewers team mailing list archive

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

 

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

Commit message:
Implement PUT/POST/DELETE method.  Update live_example for a full-scale test.

- Update jsonMAASObject to make it contain a baseURL.  The reason behind this is that the  'resource_uri' needs to be concatenated with the baseURL to form the complete URL.  Updated maasify and the related tests.

- Implement the client's Put/Post/Delete methods and created "delegators" methods in jsonMAASObject.

- remove ListNodes and create a utility (NewServer) to create a new server object implementing MAASObject.

- the example in example/live_example.go now does the 4 types of requests (GET/POST/DELETE/PUT).

Requested reviews:
  MAAS Maintainers (maas-maintainers)

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

This was pre-imp'ed with Jeroen this morning.

To run the example, start a MAAS dev server, grab the API key, then run:

go run example/live_example.go <<EOF
you:api:key
http://0.0.0.0:5240/api/1.0
EOF
-- 
https://code.launchpad.net/~rvb/maas/gomaasapi-junction/+merge/145001
Your team MAAS Maintainers is requested to review the proposed merge of lp:~rvb/maas/gomaasapi-junction into lp:~maas-maintainers/maas/gomaasapi.
=== modified file 'client.go'
--- client.go	2013-01-25 08:39:47 +0000
+++ client.go	2013-01-25 18:09:19 +0000
@@ -5,6 +5,7 @@
 
 import (
 	"errors"
+	"fmt"
 	"io/ioutil"
 	"net/http"
 	"net/url"
@@ -15,6 +16,10 @@
 	Signer OAuthSigner
 }
 
+const (
+	operationParamName = "op"
+)
+
 func (client Client) dispatchRequest(request *http.Request) ([]byte, error) {
 	client.Signer.OAuthSign(request)
 	httpClient := http.Client{}
@@ -32,7 +37,15 @@
 	return body, nil
 }
 
-func (client Client) Get(URL string, parameters url.Values) ([]byte, error) {
+func (client Client) Get(URL string, 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.")
+		return nil, errors.New(errString)
+	}
+	if operation != "" {
+		parameters.Set(operationParamName, operation)
+	}
 	queryUrl := URL + "?" + parameters.Encode()
 	request, err := http.NewRequest("GET", queryUrl, nil)
 	if err != nil {
@@ -41,16 +54,37 @@
 	return client.dispatchRequest(request)
 }
 
-func (client Client) Post(URL string, parameters url.Values) ([]byte, error) {
-	// Not implemented.
-	return []byte{}, nil
-}
+func (client Client) modification(method string, URL string, parameters url.Values) ([]byte, error) {
+	request, err := http.NewRequest(method, URL, 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) {
+	queryParams := url.Values{operationParamName: {operation}}
+	queryURL := URL + "?" + queryParams.Encode()
+	return client.modification("POST", queryURL, parameters)
+}
+
 func (client Client) Put(URL string, parameters url.Values) ([]byte, error) {
-	// Not implemented.
-	return []byte{}, nil
+	return client.modification("PUT", URL, parameters)
 }
-func (client Client) Delete(URL string, parameters url.Values) error {
-	// Not implemented.
+
+func (client Client) Delete(URL string) error {
+	request, err := http.NewRequest("DELETE", URL, strings.NewReader(""))
+	if err != nil {
+		return err
+	}
+	_, err2 := client.dispatchRequest(request)
+	if err2 != nil {
+		return err2
+	}
 	return nil
 }
 

=== modified file 'client_test.go'
--- client_test.go	2013-01-25 09:43:44 +0000
+++ client_test.go	2013-01-25 18:09:19 +0000
@@ -34,7 +34,7 @@
 
 	result, err := client.dispatchRequest(request)
 
-	c.Assert(err, IsNil)
+	c.Check(err, IsNil)
 	c.Check(string(result), Equals, expectedResult)
 	c.Check((*server.requestHeader)["Authorization"][0], Matches, "^OAuth .*")
 }
@@ -43,15 +43,72 @@
 	URI := "/some/url"
 	expectedResult := "expected:result"
 	client, _ := NewAnonymousClient()
-	params := url.Values{"op": {"list"}}
-	fullURI := URI + "?op=list"
-	server := newSingleServingServer(fullURI, expectedResult, http.StatusOK)
-	defer server.Close()
-
-	result, err := client.Get(server.URL+URI, params)
-
-	c.Assert(err, IsNil)
-	c.Check(string(result), Equals, expectedResult)
+	params := url.Values{"test": {"123"}}
+	fullURI := URI + "?test=123"
+	server := newSingleServingServer(fullURI, expectedResult, http.StatusOK)
+	defer server.Close()
+
+	result, err := client.Get(server.URL+URI, "", params)
+
+	c.Check(err, IsNil)
+	c.Check(string(result), Equals, expectedResult)
+}
+
+func (suite *GomaasapiTestSuite) TestClientGetFormatsOperationAsGetParameter(c *C) {
+	URI := "/some/url"
+	expectedResult := "expected:result"
+	client, _ := NewAnonymousClient()
+	fullURI := URI + "?op=list"
+	server := newSingleServingServer(fullURI, expectedResult, http.StatusOK)
+	defer server.Close()
+
+	result, err := client.Get(server.URL+URI, "list", url.Values{})
+
+	c.Check(err, IsNil)
+	c.Check(string(result), Equals, expectedResult)
+}
+
+func (suite *GomaasapiTestSuite) TestClientPostSendsRequest(c *C) {
+	URI := "/some/url"
+	expectedResult := "expected:result"
+	client, _ := NewAnonymousClient()
+	fullURI := URI + "?op=list"
+	params := url.Values{"test": {"123"}}
+	server := newSingleServingServer(fullURI, expectedResult, http.StatusOK)
+	defer server.Close()
+
+	result, err := client.Post(server.URL+URI, "list", params)
+
+	c.Check(err, IsNil)
+	c.Check(string(result), Equals, expectedResult)
+	c.Check(*server.requestContent, Equals, "test=123")
+}
+
+func (suite *GomaasapiTestSuite) TestClientPutSendsRequest(c *C) {
+	URI := "/some/url"
+	expectedResult := "expected:result"
+	client, _ := NewAnonymousClient()
+	params := url.Values{"test": {"123"}}
+	server := newSingleServingServer(URI, expectedResult, http.StatusOK)
+	defer server.Close()
+
+	result, err := client.Put(server.URL+URI, params)
+
+	c.Check(err, IsNil)
+	c.Check(string(result), Equals, expectedResult)
+	c.Check(*server.requestContent, Equals, "test=123")
+}
+
+func (suite *GomaasapiTestSuite) TestClientDeleteSendsRequest(c *C) {
+	URI := "/some/url"
+	expectedResult := "expected:result"
+	client, _ := NewAnonymousClient()
+	server := newSingleServingServer(URI, expectedResult, http.StatusOK)
+	defer server.Close()
+
+	err := client.Delete(server.URL + URI)
+
+	c.Check(err, IsNil)
 }
 
 func (suite *GomaasapiTestSuite) TestNewAuthenticatedClientParsesApiKey(c *C) {
@@ -65,7 +122,7 @@
 
 	client, err := NewAuthenticatedClient(apiKey)
 
-	c.Assert(err, IsNil)
+	c.Check(err, IsNil)
 	signer := client.Signer.(_PLAINTEXTOAuthSigner)
 	c.Check(signer.token.ConsumerKey, Equals, consumerKey)
 	c.Check(signer.token.TokenKey, Equals, tokenKey)

=== modified file 'example/live_example.go'
--- example/live_example.go	2013-01-24 09:16:35 +0000
+++ example/live_example.go	2013-01-25 18:09:19 +0000
@@ -3,15 +3,16 @@
 import (
 	"fmt"
 	"launchpad.net/gomaasapi"
+	"net/url"
 )
 
 var apiKey string
 var apiURL string
 
 func init() {
-	fmt.Print("Enter apiKey: ")
+	fmt.Print("Enter API key: ")
 	fmt.Scanf("%s", &apiKey)
-	fmt.Print("Enter apiURL: ")
+	fmt.Print("Enter API URL: ")
 	fmt.Scanf("%s", &apiURL)
 }
 
@@ -21,8 +22,62 @@
 		panic(err)
 	}
 
-	server := gomaasapi.Server{apiURL, authClient}
+	server, err := gomaasapi.NewServer(apiURL, *authClient)
+
+	nodeListing := server.SubObject("/nodes/")
+
+	// List nodes.
 	fmt.Println("Fetching list of nodes...")
-	listNodes, _ := server.ListNodes()
-	fmt.Printf("Got list of nodes: %s\n", listNodes)
+	listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
+	if err != nil {
+		panic(err)
+	}
+	listNodes, err := listNodeObjects.GetArray()
+	fmt.Printf("Got list of %v nodes\n", len(listNodes))
+	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())
+	}
+
+	// Create a node.
+	fmt.Println("Creating a new node...")
+	params := url.Values{"architecture": {"i386/generic"}, "mac_addresses": {"AA:BB:CC:DD:EE:FF"}}
+	newNodeObj, err := nodeListing.CallPost("new", params)
+	if err != nil {
+		panic(err)
+	}
+	newNode, _ := newNodeObj.GetMAASObject()
+	newNodeName, _ := newNode.GetField("hostname")
+	fmt.Printf("New node created: %s (%s)\n", newNodeName, newNode.URL())
+
+	// Update the new node.
+	fmt.Println("Updating the new node...")
+	updateParams := url.Values{"hostname": {"mynewname"}}
+	newNodeObj2, err := newNode.Update(updateParams)
+	if err != nil {
+		panic(err)
+	}
+	newNode2, _ := newNodeObj2.GetMAASObject()
+	newNodeName2, _ := newNode2.GetField("hostname")
+	fmt.Printf("New node updated, now named: %s\n", newNodeName2)
+
+	// Count the nodes.
+	listNodeObjects2, _ := nodeListing.CallGet("list", url.Values{})
+	listNodes2, err := listNodeObjects2.GetArray()
+	fmt.Printf("We've got %v nodes\n", len(listNodes2))
+
+	// Delete the new node.
+	fmt.Println("Deleting the new node...")
+	errDelete := newNode.Delete()
+	if errDelete != nil {
+		panic(errDelete)
+	}
+
+	// Count the nodes.
+	listNodeObjects3, _ := nodeListing.CallGet("list", url.Values{})
+	listNodes3, err := listNodeObjects3.GetArray()
+	fmt.Printf("We've got %v nodes\n", len(listNodes3))
+
+	fmt.Println("All done.")
 }

=== modified file 'jsonobject.go'
--- jsonobject.go	2013-01-25 11:37:56 +0000
+++ jsonobject.go	2013-01-25 18:09:19 +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, value interface{}) JSONObject {
+func maasify(client Client, baseURL string, 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, value)
+			result[key] = maasify(client, baseURL, 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}
+			return jsonMAASObject{result, client, baseURL}
 		}
 		return jsonMap(result)
 	case []interface{}:
 		original := value.([]interface{})
 		result := make([]JSONObject, len(original))
 		for index, value := range original {
-			result[index] = maasify(client, value)
+			result[index] = maasify(client, baseURL, value)
 		}
 		return jsonArray(result)
 	case bool:
@@ -104,13 +104,13 @@
 }
 
 // Parse a JSON blob into a JSONObject.
-func Parse(client Client, input []byte) (JSONObject, error) {
+func Parse(client Client, baseURL string, input []byte) (JSONObject, error) {
 	var obj interface{}
 	err := json.Unmarshal(input, &obj)
 	if err != nil {
 		return nil, err
 	}
-	return maasify(client, obj), nil
+	return maasify(client, baseURL, obj), nil
 }
 
 // Return error value for failed type conversion.

=== modified file 'jsonobject_test.go'
--- jsonobject_test.go	2013-01-25 11:37:26 +0000
+++ jsonobject_test.go	2013-01-25 18:09:19 +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,59 +66,67 @@
 		"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 Client to a MAASObject it creates.
-func (suite *GomaasapiTestSuite) TestMaasifyPassesClientToMAASObject(c *C) {
+// maasify() passes its info (client and baseURL) to a MAASObject it creates.
+func (suite *GomaasapiTestSuite) TestMaasifyPassesInfoToMAASObject(c *C) {
 	client := Client{}
-	original := map[string]interface{}{"resource_uri": "http://example.com/foo"}
-	output := maasify(client, original).(jsonMAASObject)
+	original := map[string]interface{}{"resource_uri": "/foo"}
+	baseURL := "http://example.com";
+	output := maasify(client, baseURL, original).(jsonMAASObject)
 	c.Check(output.client, Equals, client)
+	c.Check(output.baseURL, Equals, baseURL)
 }
 
-// maasify() passes its Client into an array of MAASObjects it creates.
-func (suite *GomaasapiTestSuite) TestMaasifyPassesClientIntoArray(c *C) {
+// maasify() passes its info (client and baseURL) into an array of MAASObjects it creates.
+func (suite *GomaasapiTestSuite) TestMaasifyPassesInfoIntoArray(c *C) {
 	client := Client{}
-	obj := map[string]interface{}{"resource_uri": "http://example.com/foo"}
+	obj := map[string]interface{}{"resource_uri": "/foo"}
+	baseURL := "http://example.com";
 	list := []interface{}{obj}
-	output := maasify(client, list).(jsonArray)
+	output := maasify(client, baseURL, list).(jsonArray)
 	c.Check(output[0].(jsonMAASObject).client, Equals, client)
+	c.Check(output[0].(jsonMAASObject).baseURL, Equals, baseURL)
 }
 
-// maasify() passes its Client into a map of MAASObjects it creates.
-func (suite *GomaasapiTestSuite) TestMaasifyPassesClientIntoMap(c *C) {
+// maasify() passes its info (client and baseURL) into a map of MAASObjects it creates.
+func (suite *GomaasapiTestSuite) TestMaasifyPassesInfoIntoMap(c *C) {
 	client := Client{}
-	obj := map[string]interface{}{"resource_uri": "http://example.com/foo"}
+	obj := map[string]interface{}{"resource_uri": "/foo"}
+	baseURL := "http://example.com";
 	mp := map[string]interface{}{"key": obj}
-	output := maasify(client, mp).(jsonMap)
+	output := maasify(client, baseURL, mp).(jsonMap)
 	c.Check(output["key"].(jsonMAASObject).client, Equals, client)
+	c.Check(output["key"].(jsonMAASObject).baseURL, Equals, baseURL)
 }
 
-// maasify() passes its Client all the way down into any MAASObjects in the
-// object structure it creates.
-func (suite *GomaasapiTestSuite) TestMaasifyPassesClientAllTheWay(c *C) {
+// maasify() passes its info (client and baseURL) 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": "http://example.com/foo"}
+	obj := map[string]interface{}{"resource_uri": "/foo"}
+	baseURL := "http://example.com";
 	mp := map[string]interface{}{"key": obj}
 	list := []interface{}{mp}
-	output := maasify(client, list).(jsonArray)
+	output := maasify(client, baseURL, 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-25 11:37:56 +0000
+++ maasobject.go	2013-01-25 18:09:19 +0000
@@ -16,12 +16,16 @@
 type MAASObject interface {
 	JSONObject
 
+	// Utility method to extract a string field from this MAAS object.
+	GetField(name string) (string, error)
 	// Resource URI for this MAAS object.
 	URL() string
+	// Retrieve the MAAS object located at thisObject.URL()+name.
+	SubObject(name string) MAASObject
 	// Retrieve this MAAS object.
 	Get() (MAASObject, error)
 	// Write this MAAS object.
-	Post(params url.Values) (MAASObject, error)
+	Post(params url.Values) (JSONObject, error)
 	// Update this MAAS object with the given values.
 	Update(params url.Values) (MAASObject, error)
 	// Delete this MAAS object.
@@ -39,7 +43,8 @@
 // jsonMAASObject implements both JSONObject and MAASObject.
 type jsonMAASObject struct {
 	jsonMap
-	client Client
+	client  Client
+	baseURL string
 }
 
 var _ JSONObject = (*jsonMAASObject)(nil)
@@ -56,38 +61,85 @@
 
 // MAASObject implementation for jsonMAASObject.
 
-func (obj jsonMAASObject) URL() string {
+func (obj jsonMAASObject) GetField(name string) (string, error) {
+	return obj.jsonMap[name].GetString()
+}
+
+func (obj jsonMAASObject) _URI() (string, error) {
 	contents, err := obj.GetMap()
 	if err != nil {
 		panic("Unexpected failure converting jsonMAASObject to maasMap.")
 	}
-	url, err := contents[resource_uri].GetString()
-	if err != nil {
-		panic("Unexpected failure reading jsonMAASObject's URL.")
-	}
-	return url
+	return contents[resource_uri].GetString()
+}
+
+func (obj jsonMAASObject) URL() string {
+	uri, err := obj._URI()
+	if err != nil {
+		panic("Unexpected failure reading jsonMAASObject's URL.")
+	}
+	return obj.baseURL + uri
+}
+
+func (obj jsonMAASObject) SubObject(name string) MAASObject {
+	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}
 }
 
 var NotImplemented = errors.New("Not implemented")
 
 func (obj jsonMAASObject) Get() (MAASObject, error) {
-	return jsonMAASObject{}, NotImplemented
+	result, err := obj.client.Get(obj.URL(), "", url.Values{})
+	if err != nil {
+		return nil, err
+	}
+	jsonObj, err := Parse(obj.client, obj.baseURL, result)
+	if err != nil {
+		return nil, err
+	}
+	return jsonObj.GetMAASObject()
 }
 
-func (obj jsonMAASObject) Post(params url.Values) (MAASObject, error) {
-	return jsonMAASObject{}, NotImplemented
+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)
 }
 
 func (obj jsonMAASObject) Update(params url.Values) (MAASObject, error) {
-	return jsonMAASObject{}, NotImplemented
+	result, err := obj.client.Put(obj.URL(), params)
+	if err != nil {
+		return nil, err
+	}
+	jsonObj, err := Parse(obj.client, obj.baseURL, result)
+	if err != nil {
+		return nil, err
+	}
+	return jsonObj.GetMAASObject()
 }
 
-func (obj jsonMAASObject) Delete() error { return NotImplemented }
+func (obj jsonMAASObject) Delete() error {
+	return obj.client.Delete(obj.URL())
+}
 
 func (obj jsonMAASObject) CallGet(operation string, params url.Values) (JSONObject, error) {
-	return nil, NotImplemented
+	result, err := obj.client.Get(obj.URL(), operation, params)
+	if err != nil {
+		return nil, err
+	}
+	return Parse(obj.client, obj.baseURL, result)
 }
 
 func (obj jsonMAASObject) CallPost(operation string, params url.Values) (JSONObject, error) {
-	return nil, NotImplemented
+	result, err := obj.client.Post(obj.URL(), operation, params)
+	if err != nil {
+		return nil, err
+	}
+	return Parse(obj.client, obj.baseURL, result)
 }

=== modified file 'maasobject_test.go'
--- maasobject_test.go	2013-01-25 11:06:31 +0000
+++ maasobject_test.go	2013-01-25 18:09:19 +0000
@@ -50,3 +50,16 @@
 	obj := jsonMAASObject{jsonMap: jsonMap(input)}
 	c.Check(obj.URL(), Equals, uri)
 }
+
+func (suite *GomaasapiTestSuite) TestGetField(c *C) {
+	uri := "http://example.com/a/resource";
+	fieldName := "field name"
+	fieldValue := "a value"
+	input := map[string]JSONObject{
+		resource_uri: jsonString(uri), fieldName: jsonString(fieldValue),
+	}
+	obj := jsonMAASObject{jsonMap: jsonMap(input)}
+	value, err := obj.GetField(fieldName)
+	c.Check(err, IsNil)
+	c.Check(value, Equals, fieldValue)
+}

=== modified file 'server.go'
--- server.go	2013-01-25 11:37:26 +0000
+++ server.go	2013-01-25 18:09:19 +0000
@@ -4,25 +4,17 @@
 package gomaasapi
 
 import (
+	"fmt"
 	"net/url"
 )
 
-type Server struct {
-	URL    string
-	Client *Client
-}
-
-func (server *Server) ListNodes() ([]JSONObject, error) {
-	listURL := server.URL + "/nodes/"
-	params := url.Values{}
-	params.Add("op", "list")
-	result, err := server.Client.Get(listURL, params)
-	if err != nil {
-		return nil, err
-	}
-	jsonobj, err := Parse(*server.Client, result)
-	if err != nil {
-		return nil, err
-	}
-	return jsonobj.GetArray()
+func NewServer(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
 }

=== modified file 'server_test.go'
--- server_test.go	2013-01-25 11:37:26 +0000
+++ server_test.go	2013-01-25 18:09:19 +0000
@@ -5,36 +5,16 @@
 
 import (
 	. "launchpad.net/gocheck"
-	"net/http"
 )
 
-func (suite *GomaasapiTestSuite) TestServerListNodesReturnsMAASObject(c *C) {
-	URL := "/nodes/?op=list"
-	blob := `[{"resource_uri": "/obj1"}, {"resource_uri": "/obj2"}]`
-	testServer := newSingleServingServer(URL, blob, http.StatusOK)
-	defer testServer.Close()
-	client, _ := NewAnonymousClient()
-	server := Server{testServer.URL, client}
-
-	result, err := server.ListNodes()
-
-	c.Assert(err, IsNil)
-	c.Check(result, Not(IsNil))
-	c.Check(len(result), Equals, 2)
-	obj1, err := result[0].GetMAASObject()
-	c.Assert(err, IsNil)
-	c.Check(obj1.URL(), Equals, "/obj1")
-}
-
-func (suite *GomaasapiTestSuite) TestServerListNodesReturnsServerError(c *C) {
-	URL := "/nodes/?op=list"
-	expectedResult := "expected:result"
-	testServer := newSingleServingServer(URL, expectedResult, http.StatusBadRequest)
-	defer testServer.Close()
-	client, _ := NewAnonymousClient()
-	server := Server{testServer.URL, client}
-
-	_, err := server.ListNodes()
-
-	c.Assert(err, ErrorMatches, "Error requesting the MAAS server: 400 Bad Request.*")
+func (suite *GomaasapiTestSuite) TestServerParsesURL(c *C) {
+	server, err := NewServer("https://server.com:888/path/to/api";, Client{})
+
+	c.Check(err, IsNil)
+	c.Check(server.URL(), Equals, "https://server.com:888/path/to/api";)
+	jsonObj := server.(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";)
 }

=== modified file 'testing.go'
--- testing.go	2013-01-25 08:39:47 +0000
+++ testing.go	2013-01-25 18:09:19 +0000
@@ -33,7 +33,8 @@
 		requestContent = string(res)
 		requestHeader = request.Header
 		if request.URL.String() != uri {
-			http.Error(writer, "404 page not found", http.StatusNotFound)
+			errorMsg := fmt.Sprintf("404 page not found (expected '%v', got '%v').", uri, request.URL.String())
+			http.Error(writer, errorMsg, http.StatusNotFound)
 		} else {
 			writer.WriteHeader(code)
 			fmt.Fprint(writer, response)


Follow ups