Merge lp:~gz/gomaasapi/gomaasapi into lp:~gophers/gomaasapi/initial_for_review
- gomaasapi
- Merge into initial_for_review
Status: | Merged |
---|---|
Merge reported by: | Martin Packman |
Merged at revision: | not available |
Proposed branch: | lp:~gz/gomaasapi/gomaasapi |
Merge into: | lp:~gophers/gomaasapi/initial_for_review |
Diff against target: |
1964 lines (+1859/-0) 20 files modified
README (+12/-0) client.go (+138/-0) client_test.go (+142/-0) example/live_example.go (+106/-0) gomaasapi.go (+3/-0) gomaasapi_test.go (+3/-0) jsonobject.go (+197/-0) jsonobject_test.go (+228/-0) maas.go (+10/-0) maas_test.go (+22/-0) maasobject.go (+167/-0) maasobject_test.go (+133/-0) oauth.go (+81/-0) templates/source.go (+4/-0) templates/source_test.go (+13/-0) testing.go (+46/-0) testservice.go (+240/-0) testservice_test.go (+255/-0) util.go (+27/-0) util_test.go (+32/-0) |
To merge this branch: | bzr merge lp:~gz/gomaasapi/gomaasapi |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
The Go Language Gophers | Pending | ||
Review via email: mp+145811@code.launchpad.net |
Commit message
Description of the change
Rollup of all relevent changes on lp:gomaasapi
This is for review only, showing the current trunk of the project in
the diff (from the initial commit of the licence and a couple of stub
go files). Note it's not possible for the Red squad to update this
proposal directly, so don't expect code changes in response to comments
to appear here, please watch for other merge proposals. :)
Martin Packman (gz) wrote : | # |
Roger Peppe (rogpeppe) wrote : | # |
seems like a good start.
lots of minor comments, but one or two more significant queries. in
particular, it looks like it's only possible to use the maas package if
you know exactly how maas works. i'd like to see all the gruesome http
details hidden behind an API that actually reflects the operations and
entities that maas provides.
i'm +1 on TheMue's comment on JSONObject. it seems like it's a lot of
code that doesn't solve any real problem.
https:/
File client.go (right):
https:/
client.go:15: type Client struct {
On 2013/02/01 10:43:41, TheMue wrote:
> Please add godoc documentation on all public types and functions.
+1
also please unexport any methods that aren't essential to using the
package.
https:/
client.go:21: operationParamName = "op"
this seems a very long name for a very short string :-)
https:/
client.go:24: func (client Client) dispatchRequest
*http.Request) ([]byte, error) {
On 2013/02/01 10:43:41, TheMue wrote:
> Here (c Client) is more usual.
further to that, i'd suggest that on type with side effects, it's
conventional to use pointer types, so you'd use *Client throughout.
https:/
client.go:26: httpClient := http.Client{}
unless there's a specific reason not to, it would be more conventional
to use http.DefaultClient here rather than creating a new instance of
http.Client. that way connections can be reused.
https:/
client.go:36: return body, errors.New("Error requesting the MAAS server:
" + response.Status + ".")
On 2013/02/01 10:43:41, TheMue wrote:
> return nil, fmt.Errorf(
> response.Status)
> In case of an error no value is returned.
+1
but we wouldn't usually include the package name in the error message
although the Go core would. either is acceptable.
https:/
client.go:54: queryUrl := client.GetURL(URI)
the distinction between "URI" and "URL" is unclear to me here. perhaps
some documentation might make things more clear?
also, we tend to use lower case names for variables, so "uri" rather
than "URI".
https:/
client.go:66: request, err := http.NewRequest
strings.
the string conversion on the result of Encode is unnecessary here.
https:/
client.go:90: _, err2 := client.
s/err2/err/
no real need to make another variable.
https:/
client.go:104: var _ OAuthSigner = (*anonSigner)(nil)
given that you're using a by-value method above, this may as well be:
var _ OAuthSigner = anonSigner{}
https:/
client.go:121: errString := "Invalid API key. The format ...
- 13. By Raphaël Badin
-
[r=jtv]
[bug=][ author= rvb] Add testservice that can be used to write tests for libraries using gomaasapi.
William Reade (fwereade) wrote : | # |
+1s to just about everything rog and frank said, and a few comments on
JSONObject:
https:/
File jsonobject.go (right):
https:/
jsonobject.go:32: GetString() (string, error)
On 2013/02/01 17:30:05, rog wrote:
> i don't really see what these methods have to offer over:
> s, ok := someValue.(string)
The errors are quite helpful, I think. But I don't think JSONObject
needs all these Get methods when we could have a bunch of GetFoo(obj
jsonObject) functions -- I think it's be a lot cleaner. See below.
https:/
jsonobject.go:65: // Internal: turn a completely untyped json.Unmarshal
result into a
It's also conventional to start doc comments with the name of the thing
being commented on; it makes it neater to use the comment as a sentence
in other contexts. I know it's academic in this case, but worth bearing
in mind in general.
https:/
jsonobject.go:119: return errors.New(msg)
On 2013/02/01 17:30:05, rog wrote:
> fmt.Errorf("cannot convert %#v to %s", obj, wanted_type)
+1
https:/
jsonobject.go:159: func (obj jsonString) GetBool() (bool, error)
{ return failBool(obj) }
This bit is awfully dense and repetitive. I think a few standalone Get*
funcs would work a bit better here, as mentioned above:
func (jsonString) Type() string { return "string" }
func (jsonFloat64) Type() string { return "float64" }
...
func GetString(obj JSONObject) (string, error) {
if obj.Type() == "string" {
return string(obj), nil
}
return "", failConversion(
}
func GetFloat64(obj JSONObject) (float64, error) {
if obj.Type() == "float64" {
return float64(obj), nil
}
return "", failConversion(
}
...
https:/
File maasobject.go (right):
https:/
maasobject.go:57: panic("Cannot create jsonMAASObject object, no
'resource_uri' key present in the given jsonMap.")
Conventional to panic with errors
Roger Peppe (rogpeppe) wrote : | # |
https:/
File jsonobject.go (right):
https:/
jsonobject.go:159: func (obj jsonString) GetBool() (bool, error)
{ return failBool(obj) }
On 2013/02/05 12:26:31, fwereade wrote:
> This bit is awfully dense and repetitive. I think a few standalone
Get* funcs
> would work a bit better here, as mentioned above:
> func (jsonString) Type() string { return "string" }
> func (jsonFloat64) Type() string { return "float64" }
> ...
> func GetString(obj JSONObject) (string, error) {
> if obj.Type() == "string" {
> return string(obj), nil
> }
> return "", failConversion(
> }
> func GetFloat64(obj JSONObject) (float64, error) {
> if obj.Type() == "float64" {
> return float64(obj), nil
> }
> return "", failConversion(
> }
> ...
if that's the case, why bother with the JSONObject type at all?
why not:
func GetString(obj interface{}) (string, error) {
if s, ok := obj.(string); ok {
return s, nil
}
return "", failConversion(
}
or even a single function:
// Get sets the value of into (which must be a pointer
// to one of float64, string, map[string]
// bool or MaasObject) to the value of jsonObj.
func Get(jsonObj interface{}, into interface{}) error {
var ok bool
switch into := into.(type) {
case *string:
*into, ok = jsonObj.(string)
case *float64:
*into, ok = jsonObj.(float64)
etc
}
if !ok {
return failConversion(
jsonObj)
}
return nil
}
but all this seems like it's trying to do something
similar to launchpad.
the nice generality that that package provides.
- 14. By Jeroen T. Vermeulen
-
[r=rvb]
[bug=][ author= jtv] Address a large number of smaller review points.
Raphaël Badin (rvb) wrote : | # |
Thanks a lot for the reviews, another batch of improvements:
https:/
- 15. By Raphaël Badin
-
[r=jtv]
[bug=][ author= rvb] Address a large number of small review points.
Roger Peppe (rogpeppe) wrote : | # |
it would be fantastic if these changes could be proposed
with lbox -cr. if that's not feasible, i'll review in the old fashioned
way, but i'd prefer to be able to comment inline.
On 6 February 2013 04:01, <email address hidden> wrote:
> And another:
> https:/
>
> https:/
- 16. By Jeroen T. Vermeulen
-
[r=rvb]
[bug=][ author= jtv] More of the simple review changes from https:/ /codereview. appspot. com/7228069/
Martin Packman (gz) wrote : | # |
Please take a look.
Preview Diff
1 | === added file 'README' |
2 | --- README 1970-01-01 00:00:00 +0000 |
3 | +++ README 2013-02-06 17:03:19 +0000 |
4 | @@ -0,0 +1,12 @@ |
5 | +.. -*- mode: rst -*- |
6 | + |
7 | +****************************** |
8 | +MAAS API client library for Go |
9 | +****************************** |
10 | + |
11 | +This library serves as a minimal client for communicating with the MAAS web |
12 | +API in Go programs. |
13 | + |
14 | +For more information see the `project homepage`_. |
15 | + |
16 | +.. _project homepage: https://launchpad.net/gomaasapi |
17 | |
18 | === added file 'client.go' |
19 | --- client.go 1970-01-01 00:00:00 +0000 |
20 | +++ client.go 2013-02-06 17:03:19 +0000 |
21 | @@ -0,0 +1,138 @@ |
22 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
23 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
24 | + |
25 | +package gomaasapi |
26 | + |
27 | +import ( |
28 | + "errors" |
29 | + "fmt" |
30 | + "io/ioutil" |
31 | + "net/http" |
32 | + "net/url" |
33 | + "strings" |
34 | +) |
35 | + |
36 | +// Client represents a way ot communicating with a MAAS API instance. |
37 | +// It is stateless, so it can have concurrent requests in progress. |
38 | +type Client struct { |
39 | + BaseURL *url.URL |
40 | + Signer OAuthSigner |
41 | +} |
42 | + |
43 | +func (client Client) dispatchRequest(request *http.Request) ([]byte, error) { |
44 | + client.Signer.OAuthSign(request) |
45 | + httpClient := http.Client{} |
46 | + response, err := httpClient.Do(request) |
47 | + if err != nil { |
48 | + return nil, err |
49 | + } |
50 | + body, err := ioutil.ReadAll(response.Body) |
51 | + if err != nil { |
52 | + return nil, err |
53 | + } |
54 | + if response.StatusCode < 200 || response.StatusCode > 299 { |
55 | + return body, fmt.Errorf("gomaasapi: got error back from server: %v", response.Status) |
56 | + } |
57 | + return body, nil |
58 | +} |
59 | + |
60 | +func (client Client) GetURL(uri *url.URL) *url.URL { |
61 | + return client.BaseURL.ResolveReference(uri) |
62 | +} |
63 | + |
64 | +func (client Client) Get(uri *url.URL, operation string, parameters url.Values) ([]byte, error) { |
65 | + opParameter := parameters.Get("op") |
66 | + if opParameter != "" { |
67 | + errString := fmt.Sprintf("The parameters contain a value for '%s' which is reserved parameter.") |
68 | + return nil, errors.New(errString) |
69 | + } |
70 | + if operation != "" { |
71 | + parameters.Set("op", operation) |
72 | + } |
73 | + queryUrl := client.GetURL(uri) |
74 | + queryUrl.RawQuery = parameters.Encode() |
75 | + request, err := http.NewRequest("GET", queryUrl.String(), nil) |
76 | + if err != nil { |
77 | + return nil, err |
78 | + } |
79 | + return client.dispatchRequest(request) |
80 | +} |
81 | + |
82 | +// nonIdempotentRequest is a utility method to issue a PUT or a POST request. |
83 | +func (client Client) nonIdempotentRequest(method string, uri *url.URL, parameters url.Values) ([]byte, error) { |
84 | + url := client.GetURL(uri) |
85 | + request, err := http.NewRequest(method, url.String(), strings.NewReader(string(parameters.Encode()))) |
86 | + if err != nil { |
87 | + return nil, err |
88 | + } |
89 | + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
90 | + return client.dispatchRequest(request) |
91 | +} |
92 | + |
93 | +func (client Client) Post(uri *url.URL, operation string, parameters url.Values) ([]byte, error) { |
94 | + queryParams := url.Values{"op": {operation}} |
95 | + uri.RawQuery = queryParams.Encode() |
96 | + return client.nonIdempotentRequest("POST", uri, parameters) |
97 | +} |
98 | + |
99 | +func (client Client) Put(uri *url.URL, parameters url.Values) ([]byte, error) { |
100 | + return client.nonIdempotentRequest("PUT", uri, parameters) |
101 | +} |
102 | + |
103 | +func (client Client) Delete(uri *url.URL) error { |
104 | + url := client.GetURL(uri) |
105 | + request, err := http.NewRequest("DELETE", url.String(), strings.NewReader("")) |
106 | + if err != nil { |
107 | + return err |
108 | + } |
109 | + _, err = client.dispatchRequest(request) |
110 | + if err != nil { |
111 | + return err |
112 | + } |
113 | + return nil |
114 | +} |
115 | + |
116 | +type anonSigner struct{} |
117 | + |
118 | +func (signer anonSigner) OAuthSign(request *http.Request) error { |
119 | + return nil |
120 | +} |
121 | + |
122 | +// Trick to ensure *anonSigner implements the OAuthSigner interface. |
123 | +var _ OAuthSigner = anonSigner{} |
124 | + |
125 | +// NewAnonymousClient creates a client that issues anonymous requests. |
126 | +func NewAnonymousClient(BaseURL string) (*Client, error) { |
127 | + parsedBaseURL, err := url.Parse(BaseURL) |
128 | + if err != nil { |
129 | + return nil, err |
130 | + } |
131 | + return &Client{Signer: &anonSigner{}, BaseURL: parsedBaseURL}, nil |
132 | +} |
133 | + |
134 | +// NewAuthenticatedClient parses the given MAAS API key into the individual |
135 | +// OAuth tokens and creates an Client that will use these tokens to sign the |
136 | +// requests it issues. |
137 | +func NewAuthenticatedClient(BaseURL string, apiKey string) (*Client, error) { |
138 | + elements := strings.Split(apiKey, ":") |
139 | + if len(elements) != 3 { |
140 | + errString := "invalid API key %q; expected \"<consumer secret>:<token key>:<token secret>\"" |
141 | + return nil, fmt.Errorf(errString, apiKey) |
142 | + } |
143 | + token := &OAuthToken{ |
144 | + ConsumerKey: elements[0], |
145 | + // The consumer secret is the empty string in MAAS' authentication. |
146 | + ConsumerSecret: "", |
147 | + TokenKey: elements[1], |
148 | + TokenSecret: elements[2], |
149 | + } |
150 | + signer, err := NewPlainTestOAuthSigner(token, "MAAS API") |
151 | + if err != nil { |
152 | + return nil, err |
153 | + } |
154 | + parsedBaseURL, err := url.Parse(BaseURL) |
155 | + if err != nil { |
156 | + return nil, err |
157 | + } |
158 | + return &Client{Signer: signer, BaseURL: parsedBaseURL}, nil |
159 | +} |
160 | |
161 | === added file 'client_test.go' |
162 | --- client_test.go 1970-01-01 00:00:00 +0000 |
163 | +++ client_test.go 2013-02-06 17:03:19 +0000 |
164 | @@ -0,0 +1,142 @@ |
165 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
166 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
167 | + |
168 | +package gomaasapi |
169 | + |
170 | +import ( |
171 | + . "launchpad.net/gocheck" |
172 | + "net/http" |
173 | + "net/url" |
174 | + "strings" |
175 | +) |
176 | + |
177 | +type ClientSuite struct{} |
178 | + |
179 | +var _ = Suite(&ClientSuite{}) |
180 | + |
181 | +func (suite *ClientSuite) TestClientdispatchRequestReturnsError(c *C) { |
182 | + URI := "/some/url/?param1=test" |
183 | + expectedResult := "expected:result" |
184 | + server := newSingleServingServer(URI, expectedResult, http.StatusBadRequest) |
185 | + defer server.Close() |
186 | + client, _ := NewAnonymousClient(server.URL) |
187 | + request, err := http.NewRequest("GET", server.URL+URI, nil) |
188 | + |
189 | + result, err := client.dispatchRequest(request) |
190 | + |
191 | + c.Check(err, ErrorMatches, "gomaasapi: got error back from server: 400 Bad Request.*") |
192 | + c.Check(string(result), Equals, expectedResult) |
193 | +} |
194 | + |
195 | +func (suite *ClientSuite) TestClientdispatchRequestSignsRequest(c *C) { |
196 | + URI := "/some/url/?param1=test" |
197 | + expectedResult := "expected:result" |
198 | + server := newSingleServingServer(URI, expectedResult, http.StatusOK) |
199 | + defer server.Close() |
200 | + client, _ := NewAuthenticatedClient(server.URL, "the:api:key") |
201 | + request, err := http.NewRequest("GET", server.URL+URI, nil) |
202 | + |
203 | + result, err := client.dispatchRequest(request) |
204 | + |
205 | + c.Check(err, IsNil) |
206 | + c.Check(string(result), Equals, expectedResult) |
207 | + c.Check((*server.requestHeader)["Authorization"][0], Matches, "^OAuth .*") |
208 | +} |
209 | + |
210 | +func (suite *ClientSuite) TestClientGetFormatsGetParameters(c *C) { |
211 | + URI, _ := url.Parse("/some/url") |
212 | + expectedResult := "expected:result" |
213 | + params := url.Values{"test": {"123"}} |
214 | + fullURI := URI.String() + "?test=123" |
215 | + server := newSingleServingServer(fullURI, expectedResult, http.StatusOK) |
216 | + defer server.Close() |
217 | + client, _ := NewAnonymousClient(server.URL) |
218 | + |
219 | + result, err := client.Get(URI, "", params) |
220 | + |
221 | + c.Check(err, IsNil) |
222 | + c.Check(string(result), Equals, expectedResult) |
223 | +} |
224 | + |
225 | +func (suite *ClientSuite) TestClientGetFormatsOperationAsGetParameter(c *C) { |
226 | + URI, _ := url.Parse("/some/url") |
227 | + expectedResult := "expected:result" |
228 | + fullURI := URI.String() + "?op=list" |
229 | + server := newSingleServingServer(fullURI, expectedResult, http.StatusOK) |
230 | + defer server.Close() |
231 | + client, _ := NewAnonymousClient(server.URL) |
232 | + |
233 | + result, err := client.Get(URI, "list", url.Values{}) |
234 | + |
235 | + c.Check(err, IsNil) |
236 | + c.Check(string(result), Equals, expectedResult) |
237 | +} |
238 | + |
239 | +func (suite *ClientSuite) TestClientPostSendsRequest(c *C) { |
240 | + URI, _ := url.Parse("/some/url") |
241 | + expectedResult := "expected:result" |
242 | + fullURI := URI.String() + "?op=list" |
243 | + params := url.Values{"test": {"123"}} |
244 | + server := newSingleServingServer(fullURI, expectedResult, http.StatusOK) |
245 | + defer server.Close() |
246 | + client, _ := NewAnonymousClient(server.URL) |
247 | + |
248 | + result, err := client.Post(URI, "list", params) |
249 | + |
250 | + c.Check(err, IsNil) |
251 | + c.Check(string(result), Equals, expectedResult) |
252 | + c.Check(*server.requestContent, Equals, "test=123") |
253 | +} |
254 | + |
255 | +func (suite *ClientSuite) TestClientPutSendsRequest(c *C) { |
256 | + URI, _ := url.Parse("/some/url") |
257 | + expectedResult := "expected:result" |
258 | + params := url.Values{"test": {"123"}} |
259 | + server := newSingleServingServer(URI.String(), expectedResult, http.StatusOK) |
260 | + defer server.Close() |
261 | + client, _ := NewAnonymousClient(server.URL) |
262 | + |
263 | + result, err := client.Put(URI, params) |
264 | + |
265 | + c.Check(err, IsNil) |
266 | + c.Check(string(result), Equals, expectedResult) |
267 | + c.Check(*server.requestContent, Equals, "test=123") |
268 | +} |
269 | + |
270 | +func (suite *ClientSuite) TestClientDeleteSendsRequest(c *C) { |
271 | + URI, _ := url.Parse("/some/url") |
272 | + expectedResult := "expected:result" |
273 | + server := newSingleServingServer(URI.String(), expectedResult, http.StatusOK) |
274 | + defer server.Close() |
275 | + client, _ := NewAnonymousClient(server.URL) |
276 | + |
277 | + err := client.Delete(URI) |
278 | + |
279 | + c.Check(err, IsNil) |
280 | +} |
281 | + |
282 | +func (suite *ClientSuite) TestNewAuthenticatedClientParsesApiKey(c *C) { |
283 | + // NewAuthenticatedClient returns a plainTextOAuthSigneri configured |
284 | + // to use the given API key. |
285 | + consumerKey := "consumerKey" |
286 | + tokenKey := "tokenKey" |
287 | + tokenSecret := "tokenSecret" |
288 | + keyElements := []string{consumerKey, tokenKey, tokenSecret} |
289 | + apiKey := strings.Join(keyElements, ":") |
290 | + |
291 | + client, err := NewAuthenticatedClient("http://example.com/api", apiKey) |
292 | + |
293 | + c.Check(err, IsNil) |
294 | + signer := client.Signer.(*plainTextOAuthSigner) |
295 | + c.Check(signer.token.ConsumerKey, Equals, consumerKey) |
296 | + c.Check(signer.token.TokenKey, Equals, tokenKey) |
297 | + c.Check(signer.token.TokenSecret, Equals, tokenSecret) |
298 | +} |
299 | + |
300 | +func (suite *ClientSuite) TestNewAuthenticatedClientFailsIfInvalidKey(c *C) { |
301 | + client, err := NewAuthenticatedClient("", "invalid-key") |
302 | + |
303 | + c.Check(err, ErrorMatches, "invalid API key.*") |
304 | + c.Check(client, IsNil) |
305 | + |
306 | +} |
307 | |
308 | === added directory 'example' |
309 | === added file 'example/live_example.go' |
310 | --- example/live_example.go 1970-01-01 00:00:00 +0000 |
311 | +++ example/live_example.go 2013-02-06 17:03:19 +0000 |
312 | @@ -0,0 +1,106 @@ |
313 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
314 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
315 | + |
316 | +/* |
317 | +This is an example on how the Go library gomaasapi can be used to interact with |
318 | +a real MAAS server. |
319 | +Note that this is a provided only as an example and that real code should probably do something more sensible with errors than ignoring them or panicking. |
320 | +*/ |
321 | +package main |
322 | + |
323 | +import ( |
324 | + "fmt" |
325 | + "launchpad.net/gomaasapi" |
326 | + "net/url" |
327 | +) |
328 | + |
329 | +var apiKey string |
330 | +var apiURL string |
331 | + |
332 | +func getParams() { |
333 | + fmt.Println("Warning: this will create a node on the MAAS server; it should be deleted at the end of the run but if something goes wrong, that test node might be left over. You've been warned.") |
334 | + fmt.Print("Enter API key: ") |
335 | + _, err := fmt.Scanf("%s", &apiKey) |
336 | + if err != nil { |
337 | + panic(err) |
338 | + } |
339 | + fmt.Print("Enter API URL: ") |
340 | + _, err = fmt.Scanf("%s", &apiURL) |
341 | + if err != nil { |
342 | + panic(err) |
343 | + } |
344 | +} |
345 | + |
346 | +func checkError(err error) { |
347 | + if err != nil { |
348 | + panic(err) |
349 | + } |
350 | +} |
351 | + |
352 | +func main() { |
353 | + getParams() |
354 | + authClient, err := gomaasapi.NewAuthenticatedClient(apiURL, apiKey) |
355 | + checkError(err) |
356 | + |
357 | + maas := gomaasapi.NewMAAS(*authClient) |
358 | + |
359 | + nodeListing := maas.GetSubObject("nodes") |
360 | + |
361 | + // List nodes. |
362 | + fmt.Println("Fetching list of nodes...") |
363 | + listNodeObjects, err := nodeListing.CallGet("list", url.Values{}) |
364 | + checkError(err) |
365 | + listNodes, err := listNodeObjects.GetArray() |
366 | + checkError(err) |
367 | + fmt.Printf("Got list of %v nodes\n", len(listNodes)) |
368 | + for index, nodeObj := range listNodes { |
369 | + node, err := nodeObj.GetMAASObject() |
370 | + checkError(err) |
371 | + hostname, err := node.GetField("hostname") |
372 | + checkError(err) |
373 | + fmt.Printf("Node #%d is named '%v' (%v)\n", index, hostname, node.URL()) |
374 | + } |
375 | + |
376 | + // Create a node. |
377 | + fmt.Println("Creating a new node...") |
378 | + params := url.Values{"architecture": {"i386/generic"}, "mac_addresses": {"AA:BB:CC:DD:EE:FF"}} |
379 | + newNodeObj, err := nodeListing.CallPost("new", params) |
380 | + checkError(err) |
381 | + newNode, err := newNodeObj.GetMAASObject() |
382 | + checkError(err) |
383 | + newNodeName, err := newNode.GetField("hostname") |
384 | + checkError(err) |
385 | + fmt.Printf("New node created: %s (%s)\n", newNodeName, newNode.URL()) |
386 | + |
387 | + // Update the new node. |
388 | + fmt.Println("Updating the new node...") |
389 | + updateParams := url.Values{"hostname": {"mynewname"}} |
390 | + newNodeObj2, err := newNode.Update(updateParams) |
391 | + checkError(err) |
392 | + newNode2, err := newNodeObj2.GetMAASObject() |
393 | + checkError(err) |
394 | + newNodeName2, err := newNode2.GetField("hostname") |
395 | + checkError(err) |
396 | + fmt.Printf("New node updated, now named: %s\n", newNodeName2) |
397 | + |
398 | + // Count the nodes. |
399 | + listNodeObjects2, err := nodeListing.CallGet("list", url.Values{}) |
400 | + checkError(err) |
401 | + listNodes2, err := listNodeObjects2.GetArray() |
402 | + checkError(err) |
403 | + fmt.Printf("We've got %v nodes\n", len(listNodes2)) |
404 | + |
405 | + // Delete the new node. |
406 | + fmt.Println("Deleting the new node...") |
407 | + errDelete := newNode.Delete() |
408 | + checkError(errDelete) |
409 | + |
410 | + // Count the nodes. |
411 | + listNodeObjects3, err := nodeListing.CallGet("list", url.Values{}) |
412 | + checkError(err) |
413 | + listNodes3, err := listNodeObjects3.GetArray() |
414 | + checkError(err) |
415 | + fmt.Printf("We've got %v nodes\n", len(listNodes3)) |
416 | + |
417 | + fmt.Println("All done.") |
418 | +} |
419 | |
420 | === modified file 'gomaasapi.go' |
421 | --- gomaasapi.go 2013-01-22 10:29:56 +0000 |
422 | +++ gomaasapi.go 2013-02-06 17:03:19 +0000 |
423 | @@ -1,1 +1,4 @@ |
424 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
425 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
426 | + |
427 | package gomaasapi |
428 | |
429 | === modified file 'gomaasapi_test.go' |
430 | --- gomaasapi_test.go 2013-01-22 10:29:56 +0000 |
431 | +++ gomaasapi_test.go 2013-02-06 17:03:19 +0000 |
432 | @@ -1,3 +1,6 @@ |
433 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
434 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
435 | + |
436 | package gomaasapi |
437 | |
438 | import ( |
439 | |
440 | === added file 'jsonobject.go' |
441 | --- jsonobject.go 1970-01-01 00:00:00 +0000 |
442 | +++ jsonobject.go 2013-02-06 17:03:19 +0000 |
443 | @@ -0,0 +1,197 @@ |
444 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
445 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
446 | + |
447 | +package gomaasapi |
448 | + |
449 | +import ( |
450 | + "encoding/json" |
451 | + "errors" |
452 | + "fmt" |
453 | +) |
454 | + |
455 | +// JSONObject is a wrapper around a JSON structure which provides |
456 | +// methods to extract data from that structure. |
457 | +// A JSONObject provides a simple structure consisting of the data types |
458 | +// defined in JSON: string, number, object, list, and bool. To get the |
459 | +// value you want out of a JSONObject, you must know (or figure out) which |
460 | +// kind of value you have, and then call the appropriate Get*() method to |
461 | +// get at it. Reading an item as the wrong type will return an error. |
462 | +// For instance, if your JSONObject consists of a number, call GetFloat64() |
463 | +// to get the value as a float64. If it's a list, call GetArray() to get |
464 | +// a slice of JSONObjects. To read any given item from the slice, you'll |
465 | +// need to "Get" that as the right type as well. |
466 | +// There is one exception: a MAASObject is really a special kind of map, |
467 | +// so you can read it as either. |
468 | +// Reading a null item is also an error. So before you try obj.Get*(), |
469 | +// first check that obj != nil. |
470 | +type JSONObject interface { |
471 | + // Type of this value: |
472 | + // "string", "float64", "map", "maasobject", "array", or "bool". |
473 | + Type() string |
474 | + // Read as string. |
475 | + GetString() (string, error) |
476 | + // Read number as float64. |
477 | + GetFloat64() (float64, error) |
478 | + // Read object as map. |
479 | + GetMap() (map[string]JSONObject, error) |
480 | + // Read object as MAAS object. |
481 | + GetMAASObject() (MAASObject, error) |
482 | + // Read list as array. |
483 | + GetArray() ([]JSONObject, error) |
484 | + // Read as bool. |
485 | + GetBool() (bool, error) |
486 | +} |
487 | + |
488 | +// Internally, each JSONObject already knows what type it is. It just |
489 | +// can't tell the caller yet because the caller may not have the right |
490 | +// hard-coded variable type. |
491 | +// So for each JSON type, there is a separate implementation of JSONObject |
492 | +// that converts only to that type. Any other conversion is an error. |
493 | +// One type is special: jsonMAASObject is an object in the MAAS sense. It |
494 | +// behaves just like a jsonMap if you want it to, but it also implements |
495 | +// MAASObject. |
496 | +type jsonString string |
497 | +type jsonFloat64 float64 |
498 | +type jsonMap map[string]JSONObject |
499 | +type jsonArray []JSONObject |
500 | +type jsonBool bool |
501 | + |
502 | +// Our JSON processor distinguishes a MAASObject from a jsonMap by the fact |
503 | +// that it contains a key "resource_uri". (A regular map might contain the |
504 | +// same key through sheer coincide, but never mind: you can still treat it |
505 | +// as a jsonMap and never notice the difference.) |
506 | +const resourceURI = "resource_uri" |
507 | + |
508 | +// maasify is internal. It turns a completely untyped json.Unmarshal result |
509 | +// into a JSONObject (with the appropriate implementation of course). |
510 | +// This function is recursive. Maps and arrays are deep-copied, with each |
511 | +// individual value being converted to a JSONObject type. |
512 | +func maasify(client Client, value interface{}) JSONObject { |
513 | + if value == nil { |
514 | + return nil |
515 | + } |
516 | + switch value.(type) { |
517 | + case string: |
518 | + return jsonString(value.(string)) |
519 | + case float64: |
520 | + return jsonFloat64(value.(float64)) |
521 | + case map[string]interface{}: |
522 | + original := value.(map[string]interface{}) |
523 | + result := make(map[string]JSONObject, len(original)) |
524 | + for key, value := range original { |
525 | + result[key] = maasify(client, value) |
526 | + } |
527 | + if _, ok := result[resourceURI]; ok { |
528 | + // If the map contains "resource-uri", we can treat |
529 | + // it as a MAAS object. |
530 | + return newJSONMAASObject(result, client) |
531 | + } |
532 | + return jsonMap(result) |
533 | + case []interface{}: |
534 | + original := value.([]interface{}) |
535 | + result := make([]JSONObject, len(original)) |
536 | + for index, value := range original { |
537 | + result[index] = maasify(client, value) |
538 | + } |
539 | + return jsonArray(result) |
540 | + case bool: |
541 | + return jsonBool(value.(bool)) |
542 | + } |
543 | + msg := fmt.Sprintf("Unknown JSON type, can't be converted to JSONObject: %v", value) |
544 | + panic(msg) |
545 | +} |
546 | + |
547 | +// Parse a JSON blob into a JSONObject. |
548 | +func Parse(client Client, input []byte) (JSONObject, error) { |
549 | + var obj interface{} |
550 | + err := json.Unmarshal(input, &obj) |
551 | + if err != nil { |
552 | + return nil, err |
553 | + } |
554 | + return maasify(client, obj), nil |
555 | +} |
556 | + |
557 | +// Return error value for failed type conversion. |
558 | +func failConversion(wantedType string, obj JSONObject) error { |
559 | + msg := fmt.Sprintf("Requested %v, got %v.", wantedType, obj.Type()) |
560 | + return errors.New(msg) |
561 | +} |
562 | + |
563 | +// Error return values for failure to convert to string. |
564 | +func failString(obj JSONObject) (string, error) { |
565 | + return "", failConversion("string", obj) |
566 | +} |
567 | + |
568 | +// Error return values for failure to convert to float64. |
569 | +func failFloat64(obj JSONObject) (float64, error) { |
570 | + return 0.0, failConversion("float64", obj) |
571 | +} |
572 | + |
573 | +// Error return values for failure to convert to map. |
574 | +func failMap(obj JSONObject) (map[string]JSONObject, error) { |
575 | + return make(map[string]JSONObject, 0), failConversion("map", obj) |
576 | +} |
577 | + |
578 | +// Error return values for failure to convert to MAAS object. |
579 | +func failMAASObject(obj JSONObject) (MAASObject, error) { |
580 | + return jsonMAASObject{}, failConversion("maasobject", obj) |
581 | +} |
582 | + |
583 | +// Error return values for failure to convert to array. |
584 | +func failArray(obj JSONObject) ([]JSONObject, error) { |
585 | + return make([]JSONObject, 0), failConversion("array", obj) |
586 | +} |
587 | + |
588 | +// Error return values for failure to convert to bool. |
589 | +func failBool(obj JSONObject) (bool, error) { |
590 | + return false, failConversion("bool", obj) |
591 | +} |
592 | + |
593 | +// JSONObject implementation for jsonString. |
594 | +func (jsonString) Type() string { return "string" } |
595 | +func (obj jsonString) GetString() (string, error) { return string(obj), nil } |
596 | +func (obj jsonString) GetFloat64() (float64, error) { return failFloat64(obj) } |
597 | +func (obj jsonString) GetMap() (map[string]JSONObject, error) { return failMap(obj) } |
598 | +func (obj jsonString) GetMAASObject() (MAASObject, error) { return failMAASObject(obj) } |
599 | +func (obj jsonString) GetArray() ([]JSONObject, error) { return failArray(obj) } |
600 | +func (obj jsonString) GetBool() (bool, error) { return failBool(obj) } |
601 | + |
602 | +// JSONObject implementation for jsonFloat64. |
603 | +func (jsonFloat64) Type() string { return "float64" } |
604 | +func (obj jsonFloat64) GetString() (string, error) { return failString(obj) } |
605 | +func (obj jsonFloat64) GetFloat64() (float64, error) { return float64(obj), nil } |
606 | +func (obj jsonFloat64) GetMap() (map[string]JSONObject, error) { return failMap(obj) } |
607 | +func (obj jsonFloat64) GetMAASObject() (MAASObject, error) { return failMAASObject(obj) } |
608 | +func (obj jsonFloat64) GetArray() ([]JSONObject, error) { return failArray(obj) } |
609 | +func (obj jsonFloat64) GetBool() (bool, error) { return failBool(obj) } |
610 | + |
611 | +// JSONObject implementation for jsonMap. |
612 | +func (jsonMap) Type() string { return "map" } |
613 | +func (obj jsonMap) GetString() (string, error) { return failString(obj) } |
614 | +func (obj jsonMap) GetFloat64() (float64, error) { return failFloat64(obj) } |
615 | +func (obj jsonMap) GetMap() (map[string]JSONObject, error) { |
616 | + return (map[string]JSONObject)(obj), nil |
617 | +} |
618 | +func (obj jsonMap) GetMAASObject() (MAASObject, error) { return failMAASObject(obj) } |
619 | +func (obj jsonMap) GetArray() ([]JSONObject, error) { return failArray(obj) } |
620 | +func (obj jsonMap) GetBool() (bool, error) { return failBool(obj) } |
621 | + |
622 | +// JSONObject implementation for jsonArray. |
623 | +func (jsonArray) Type() string { return "array" } |
624 | +func (obj jsonArray) GetString() (string, error) { return failString(obj) } |
625 | +func (obj jsonArray) GetFloat64() (float64, error) { return failFloat64(obj) } |
626 | +func (obj jsonArray) GetMap() (map[string]JSONObject, error) { return failMap(obj) } |
627 | +func (obj jsonArray) GetMAASObject() (MAASObject, error) { return failMAASObject(obj) } |
628 | +func (obj jsonArray) GetArray() ([]JSONObject, error) { |
629 | + return ([]JSONObject)(obj), nil |
630 | +} |
631 | +func (obj jsonArray) GetBool() (bool, error) { return failBool(obj) } |
632 | + |
633 | +// JSONObject implementation for jsonBool. |
634 | +func (jsonBool) Type() string { return "bool" } |
635 | +func (obj jsonBool) GetString() (string, error) { return failString(obj) } |
636 | +func (obj jsonBool) GetFloat64() (float64, error) { return failFloat64(obj) } |
637 | +func (obj jsonBool) GetMap() (map[string]JSONObject, error) { return failMap(obj) } |
638 | +func (obj jsonBool) GetMAASObject() (MAASObject, error) { return failMAASObject(obj) } |
639 | +func (obj jsonBool) GetArray() ([]JSONObject, error) { return failArray(obj) } |
640 | +func (obj jsonBool) GetBool() (bool, error) { return bool(obj), nil } |
641 | |
642 | === added file 'jsonobject_test.go' |
643 | --- jsonobject_test.go 1970-01-01 00:00:00 +0000 |
644 | +++ jsonobject_test.go 2013-02-06 17:03:19 +0000 |
645 | @@ -0,0 +1,228 @@ |
646 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
647 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
648 | + |
649 | +package gomaasapi |
650 | + |
651 | +import ( |
652 | + . "launchpad.net/gocheck" |
653 | +) |
654 | + |
655 | +type JSONObjectSuite struct { |
656 | +} |
657 | + |
658 | +var _ = Suite(&JSONObjectSuite{}) |
659 | + |
660 | +// maasify() converts nil. |
661 | +func (suite *JSONObjectSuite) TestMaasifyConvertsNil(c *C) { |
662 | + c.Check(maasify(Client{}, nil), Equals, nil) |
663 | +} |
664 | + |
665 | +// maasify() converts strings. |
666 | +func (suite *JSONObjectSuite) TestMaasifyConvertsString(c *C) { |
667 | + const text = "Hello" |
668 | + c.Check(string(maasify(Client{}, text).(jsonString)), Equals, text) |
669 | +} |
670 | + |
671 | +// maasify() converts float64 numbers. |
672 | +func (suite *JSONObjectSuite) TestMaasifyConvertsNumber(c *C) { |
673 | + const number = 3.1415926535 |
674 | + c.Check(float64(maasify(Client{}, number).(jsonFloat64)), Equals, number) |
675 | +} |
676 | + |
677 | +// maasify() converts array slices. |
678 | +func (suite *JSONObjectSuite) TestMaasifyConvertsArray(c *C) { |
679 | + original := []interface{}{3.0, 2.0, 1.0} |
680 | + output := maasify(Client{}, original).(jsonArray) |
681 | + c.Check(len(output), Equals, len(original)) |
682 | +} |
683 | + |
684 | +// When maasify() converts an array slice, the result contains JSONObjects. |
685 | +func (suite *JSONObjectSuite) TestMaasifyArrayContainsJSONObjects(c *C) { |
686 | + arr := maasify(Client{}, []interface{}{9.9}).(jsonArray) |
687 | + var entry JSONObject |
688 | + entry = arr[0] |
689 | + c.Check((float64)(entry.(jsonFloat64)), Equals, 9.9) |
690 | +} |
691 | + |
692 | +// maasify() converts maps. |
693 | +func (suite *JSONObjectSuite) TestMaasifyConvertsMap(c *C) { |
694 | + original := map[string]interface{}{"1": "one", "2": "two", "3": "three"} |
695 | + output := maasify(Client{}, original).(jsonMap) |
696 | + c.Check(len(output), Equals, len(original)) |
697 | +} |
698 | + |
699 | +// When maasify() converts a map, the result contains JSONObjects. |
700 | +func (suite *JSONObjectSuite) TestMaasifyMapContainsJSONObjects(c *C) { |
701 | + mp := maasify(Client{}, map[string]interface{}{"key": "value"}).(jsonMap) |
702 | + var entry JSONObject |
703 | + entry = mp["key"] |
704 | + c.Check((string)(entry.(jsonString)), Equals, "value") |
705 | +} |
706 | + |
707 | +// maasify() converts MAAS objects. |
708 | +func (suite *JSONObjectSuite) TestMaasifyConvertsMAASObject(c *C) { |
709 | + original := map[string]interface{}{ |
710 | + "resource_uri": "http://example.com/foo", |
711 | + "size": "3", |
712 | + } |
713 | + output := maasify(Client{}, original).(jsonMAASObject) |
714 | + c.Check(len(output.jsonMap), Equals, len(original)) |
715 | + c.Check((string)(output.jsonMap["size"].(jsonString)), Equals, "3") |
716 | +} |
717 | + |
718 | +// maasify() passes its client to a MAASObject it creates. |
719 | +func (suite *JSONObjectSuite) TestMaasifyPassesInfoToMAASObject(c *C) { |
720 | + client := Client{} |
721 | + original := map[string]interface{}{"resource_uri": "/foo"} |
722 | + output := maasify(client, original).(jsonMAASObject) |
723 | + c.Check(output.client, Equals, client) |
724 | +} |
725 | + |
726 | +// maasify() passes its client into an array of MAASObjects it creates. |
727 | +func (suite *JSONObjectSuite) TestMaasifyPassesInfoIntoArray(c *C) { |
728 | + client := Client{} |
729 | + obj := map[string]interface{}{"resource_uri": "/foo"} |
730 | + list := []interface{}{obj} |
731 | + output := maasify(client, list).(jsonArray) |
732 | + c.Check(output[0].(jsonMAASObject).client, Equals, client) |
733 | +} |
734 | + |
735 | +// maasify() passes its client into a map of MAASObjects it creates. |
736 | +func (suite *JSONObjectSuite) TestMaasifyPassesInfoIntoMap(c *C) { |
737 | + client := Client{} |
738 | + obj := map[string]interface{}{"resource_uri": "/foo"} |
739 | + mp := map[string]interface{}{"key": obj} |
740 | + output := maasify(client, mp).(jsonMap) |
741 | + c.Check(output["key"].(jsonMAASObject).client, Equals, client) |
742 | +} |
743 | + |
744 | +// maasify() passes its client all the way down into any MAASObjects in the |
745 | +// object structure it creates. |
746 | +func (suite *JSONObjectSuite) TestMaasifyPassesInfoAllTheWay(c *C) { |
747 | + client := Client{} |
748 | + obj := map[string]interface{}{"resource_uri": "/foo"} |
749 | + mp := map[string]interface{}{"key": obj} |
750 | + list := []interface{}{mp} |
751 | + output := maasify(client, list).(jsonArray) |
752 | + maasobj := output[0].(jsonMap)["key"] |
753 | + c.Check(maasobj.(jsonMAASObject).client, Equals, client) |
754 | +} |
755 | + |
756 | +// maasify() converts Booleans. |
757 | +func (suite *JSONObjectSuite) TestMaasifyConvertsBool(c *C) { |
758 | + c.Check(bool(maasify(Client{}, true).(jsonBool)), Equals, true) |
759 | + c.Check(bool(maasify(Client{}, false).(jsonBool)), Equals, false) |
760 | +} |
761 | + |
762 | +// Parse takes you from a JSON blob to a JSONObject. |
763 | +func (suite *JSONObjectSuite) TestParseMaasifiesJSONBlob(c *C) { |
764 | + blob := []byte("[12]") |
765 | + obj, err := Parse(Client{}, blob) |
766 | + c.Check(err, IsNil) |
767 | + c.Check(float64(obj.(jsonArray)[0].(jsonFloat64)), Equals, 12.0) |
768 | +} |
769 | + |
770 | +// String-type JSONObjects convert only to string. |
771 | +func (suite *JSONObjectSuite) TestConversionsString(c *C) { |
772 | + obj := jsonString("Test string") |
773 | + |
774 | + value, err := obj.GetString() |
775 | + c.Check(err, IsNil) |
776 | + c.Check(value, Equals, "Test string") |
777 | + |
778 | + _, err = obj.GetFloat64() |
779 | + c.Check(err, NotNil) |
780 | + _, err = obj.GetMap() |
781 | + c.Check(err, NotNil) |
782 | + _, err = obj.GetMAASObject() |
783 | + c.Check(err, NotNil) |
784 | + _, err = obj.GetArray() |
785 | + c.Check(err, NotNil) |
786 | + _, err = obj.GetBool() |
787 | + c.Check(err, NotNil) |
788 | +} |
789 | + |
790 | +// Number-type JSONObjects convert only to float64. |
791 | +func (suite *JSONObjectSuite) TestConversionsFloat64(c *C) { |
792 | + obj := jsonFloat64(1.1) |
793 | + |
794 | + value, err := obj.GetFloat64() |
795 | + c.Check(err, IsNil) |
796 | + c.Check(value, Equals, 1.1) |
797 | + |
798 | + _, err = obj.GetString() |
799 | + c.Check(err, NotNil) |
800 | + _, err = obj.GetMap() |
801 | + c.Check(err, NotNil) |
802 | + _, err = obj.GetMAASObject() |
803 | + c.Check(err, NotNil) |
804 | + _, err = obj.GetArray() |
805 | + c.Check(err, NotNil) |
806 | + _, err = obj.GetBool() |
807 | + c.Check(err, NotNil) |
808 | +} |
809 | + |
810 | +// Map-type JSONObjects convert only to map. |
811 | +func (suite *JSONObjectSuite) TestConversionsMap(c *C) { |
812 | + input := map[string]JSONObject{"x": jsonString("y")} |
813 | + obj := jsonMap(input) |
814 | + |
815 | + value, err := obj.GetMap() |
816 | + c.Check(err, IsNil) |
817 | + text, err := value["x"].GetString() |
818 | + c.Check(err, IsNil) |
819 | + c.Check(text, Equals, "y") |
820 | + |
821 | + _, err = obj.GetString() |
822 | + c.Check(err, NotNil) |
823 | + _, err = obj.GetFloat64() |
824 | + c.Check(err, NotNil) |
825 | + _, err = obj.GetMAASObject() |
826 | + c.Check(err, NotNil) |
827 | + _, err = obj.GetArray() |
828 | + c.Check(err, NotNil) |
829 | + _, err = obj.GetBool() |
830 | + c.Check(err, NotNil) |
831 | +} |
832 | + |
833 | +// Array-type JSONObjects convert only to array. |
834 | +func (suite *JSONObjectSuite) TestConversionsArray(c *C) { |
835 | + obj := jsonArray([]JSONObject{jsonString("item")}) |
836 | + |
837 | + value, err := obj.GetArray() |
838 | + c.Check(err, IsNil) |
839 | + text, err := value[0].GetString() |
840 | + c.Check(err, IsNil) |
841 | + c.Check(text, Equals, "item") |
842 | + |
843 | + _, err = obj.GetString() |
844 | + c.Check(err, NotNil) |
845 | + _, err = obj.GetFloat64() |
846 | + c.Check(err, NotNil) |
847 | + _, err = obj.GetMap() |
848 | + c.Check(err, NotNil) |
849 | + _, err = obj.GetMAASObject() |
850 | + c.Check(err, NotNil) |
851 | + _, err = obj.GetBool() |
852 | + c.Check(err, NotNil) |
853 | +} |
854 | + |
855 | +// Boolean-type JSONObjects convert only to bool. |
856 | +func (suite *JSONObjectSuite) TestConversionsBool(c *C) { |
857 | + obj := jsonBool(false) |
858 | + |
859 | + value, err := obj.GetBool() |
860 | + c.Check(err, IsNil) |
861 | + c.Check(value, Equals, false) |
862 | + |
863 | + _, err = obj.GetString() |
864 | + c.Check(err, NotNil) |
865 | + _, err = obj.GetFloat64() |
866 | + c.Check(err, NotNil) |
867 | + _, err = obj.GetMap() |
868 | + c.Check(err, NotNil) |
869 | + _, err = obj.GetMAASObject() |
870 | + c.Check(err, NotNil) |
871 | + _, err = obj.GetArray() |
872 | + c.Check(err, NotNil) |
873 | +} |
874 | |
875 | === added file 'maas.go' |
876 | --- maas.go 1970-01-01 00:00:00 +0000 |
877 | +++ maas.go 2013-02-06 17:03:19 +0000 |
878 | @@ -0,0 +1,10 @@ |
879 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
880 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
881 | + |
882 | +package gomaasapi |
883 | + |
884 | +// NewMAAS returns an interface to the MAAS API as a MAASObject. |
885 | +func NewMAAS(client Client) MAASObject { |
886 | + input := map[string]JSONObject{resourceURI: jsonString(client.BaseURL.String())} |
887 | + return newJSONMAASObject(jsonMap(input), client) |
888 | +} |
889 | |
890 | === added file 'maas_test.go' |
891 | --- maas_test.go 1970-01-01 00:00:00 +0000 |
892 | +++ maas_test.go 2013-02-06 17:03:19 +0000 |
893 | @@ -0,0 +1,22 @@ |
894 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
895 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
896 | + |
897 | +package gomaasapi |
898 | + |
899 | +import ( |
900 | + . "launchpad.net/gocheck" |
901 | + "net/url" |
902 | +) |
903 | + |
904 | +type MAASSuite struct{} |
905 | + |
906 | +var _ = Suite(&MAASSuite{}) |
907 | + |
908 | +func (suite *MAASSuite) TestNewMAASUsesBaseURLFromClient(c *C) { |
909 | + baseURLString := "https://server.com:888/path/to/api" |
910 | + baseURL, _ := url.Parse(baseURLString) |
911 | + client := Client{BaseURL: baseURL} |
912 | + maas := NewMAAS(client) |
913 | + URL := maas.URL() |
914 | + c.Check(URL, DeepEquals, baseURL) |
915 | +} |
916 | |
917 | === added file 'maasobject.go' |
918 | --- maasobject.go 1970-01-01 00:00:00 +0000 |
919 | +++ maasobject.go 2013-02-06 17:03:19 +0000 |
920 | @@ -0,0 +1,167 @@ |
921 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
922 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
923 | + |
924 | +package gomaasapi |
925 | + |
926 | +import ( |
927 | + "errors" |
928 | + "net/url" |
929 | +) |
930 | + |
931 | +// MAASObject represents a MAAS object as returned by the MAAS API, such as a |
932 | +// Node or a Tag. |
933 | +// This is a special kind of JSONObject. A MAAS API call will usually return |
934 | +// either a MAASObject or a list of MAASObjects. (The list itself will be |
935 | +// wrapped in a JSONObject). |
936 | +type MAASObject interface { |
937 | + JSONObject |
938 | + |
939 | + // Utility method to extract a string field from this MAAS object. |
940 | + GetField(name string) (string, error) |
941 | + // URL for this MAAS object. |
942 | + URL() *url.URL |
943 | + // Resource URI for this MAAS object. |
944 | + URI() *url.URL |
945 | + // Retrieve the MAAS object located at thisObject.URI()+name. |
946 | + GetSubObject(name string) MAASObject |
947 | + // Retrieve this MAAS object. |
948 | + Get() (MAASObject, error) |
949 | + // Write this MAAS object. |
950 | + Post(params url.Values) (JSONObject, error) |
951 | + // Update this MAAS object with the given values. |
952 | + Update(params url.Values) (MAASObject, error) |
953 | + // Delete this MAAS object. |
954 | + Delete() error |
955 | + // Invoke a GET-based method on this MAAS object. |
956 | + CallGet(operation string, params url.Values) (JSONObject, error) |
957 | + // Invoke a POST-based method on this MAAS object. |
958 | + CallPost(operation string, params url.Values) (JSONObject, error) |
959 | +} |
960 | + |
961 | +// JSONObject implementation for a MAAS object. From a decoding perspective, |
962 | +// a jsonMAASObject is just like a jsonMap except it contains a key |
963 | +// "resource_uri", and it keeps track of the Client you got it from so that |
964 | +// you can invoke API methods directly on their MAAS objects. |
965 | +// jsonMAASObject implements both JSONObject and MAASObject. |
966 | +type jsonMAASObject struct { |
967 | + jsonMap |
968 | + client Client |
969 | + uri *url.URL |
970 | +} |
971 | + |
972 | +// newJSONMAASObject creates a new MAAS object. It will panic if the given map |
973 | +// does not contain a valid URL for the 'resource_uri' key. |
974 | +func newJSONMAASObject(jmap jsonMap, client Client) jsonMAASObject { |
975 | + const panicPrefix = "Error processing MAAS object: " |
976 | + uriObj, ok := jmap[resourceURI] |
977 | + if !ok { |
978 | + panic(errors.New(panicPrefix + "no 'resource_uri' key present in the given jsonMap.")) |
979 | + } |
980 | + uriString, err := uriObj.GetString() |
981 | + if err != nil { |
982 | + panic(errors.New(panicPrefix + "the value of 'resource_uri' is not a string.")) |
983 | + } |
984 | + uri, err := url.Parse(uriString) |
985 | + if err != nil { |
986 | + panic(errors.New(panicPrefix + "the value of 'resource_uri' is not a valid URL.")) |
987 | + } |
988 | + return jsonMAASObject{jmap, client, uri} |
989 | +} |
990 | + |
991 | +var _ JSONObject = (*jsonMAASObject)(nil) |
992 | +var _ MAASObject = (*jsonMAASObject)(nil) |
993 | + |
994 | +// JSONObject implementation for jsonMAASObject. |
995 | +func (jsonMAASObject) Type() string { return "maasobject" } |
996 | +func (obj jsonMAASObject) GetString() (string, error) { return failString(obj) } |
997 | +func (obj jsonMAASObject) GetFloat64() (float64, error) { return failFloat64(obj) } |
998 | +func (obj jsonMAASObject) GetMap() (map[string]JSONObject, error) { return obj.jsonMap.GetMap() } |
999 | +func (obj jsonMAASObject) GetMAASObject() (MAASObject, error) { return obj, nil } |
1000 | +func (obj jsonMAASObject) GetArray() ([]JSONObject, error) { return failArray(obj) } |
1001 | +func (obj jsonMAASObject) GetBool() (bool, error) { return failBool(obj) } |
1002 | + |
1003 | +// MAASObject implementation for jsonMAASObject. |
1004 | + |
1005 | +func (obj jsonMAASObject) GetField(name string) (string, error) { |
1006 | + return obj.jsonMap[name].GetString() |
1007 | +} |
1008 | + |
1009 | +func (obj jsonMAASObject) URI() *url.URL { |
1010 | + // Duplicate the URL. |
1011 | + uri, err := url.Parse(obj.uri.String()) |
1012 | + if err != nil { |
1013 | + panic(err) |
1014 | + } |
1015 | + return uri |
1016 | +} |
1017 | + |
1018 | +func (obj jsonMAASObject) URL() *url.URL { |
1019 | + return obj.client.GetURL(obj.URI()) |
1020 | +} |
1021 | + |
1022 | +func (obj jsonMAASObject) GetSubObject(name string) MAASObject { |
1023 | + uri := obj.URI() |
1024 | + uri.Path = EnsureTrailingSlash(JoinURLs(uri.Path, name)) |
1025 | + input := map[string]JSONObject{resourceURI: jsonString(uri.String())} |
1026 | + return newJSONMAASObject(jsonMap(input), obj.client) |
1027 | +} |
1028 | + |
1029 | +var NotImplemented = errors.New("Not implemented") |
1030 | + |
1031 | +func (obj jsonMAASObject) Get() (MAASObject, error) { |
1032 | + uri := obj.URI() |
1033 | + result, err := obj.client.Get(uri, "", url.Values{}) |
1034 | + if err != nil { |
1035 | + return nil, err |
1036 | + } |
1037 | + jsonObj, err := Parse(obj.client, result) |
1038 | + if err != nil { |
1039 | + return nil, err |
1040 | + } |
1041 | + return jsonObj.GetMAASObject() |
1042 | +} |
1043 | + |
1044 | +func (obj jsonMAASObject) Post(params url.Values) (JSONObject, error) { |
1045 | + uri := obj.URI() |
1046 | + result, err := obj.client.Post(uri, "", params) |
1047 | + if err != nil { |
1048 | + return nil, err |
1049 | + } |
1050 | + return Parse(obj.client, result) |
1051 | +} |
1052 | + |
1053 | +func (obj jsonMAASObject) Update(params url.Values) (MAASObject, error) { |
1054 | + uri := obj.URI() |
1055 | + result, err := obj.client.Put(uri, params) |
1056 | + if err != nil { |
1057 | + return nil, err |
1058 | + } |
1059 | + jsonObj, err := Parse(obj.client, result) |
1060 | + if err != nil { |
1061 | + return nil, err |
1062 | + } |
1063 | + return jsonObj.GetMAASObject() |
1064 | +} |
1065 | + |
1066 | +func (obj jsonMAASObject) Delete() error { |
1067 | + uri := obj.URI() |
1068 | + return obj.client.Delete(uri) |
1069 | +} |
1070 | + |
1071 | +func (obj jsonMAASObject) CallGet(operation string, params url.Values) (JSONObject, error) { |
1072 | + uri := obj.URI() |
1073 | + result, err := obj.client.Get(uri, operation, params) |
1074 | + if err != nil { |
1075 | + return nil, err |
1076 | + } |
1077 | + return Parse(obj.client, result) |
1078 | +} |
1079 | + |
1080 | +func (obj jsonMAASObject) CallPost(operation string, params url.Values) (JSONObject, error) { |
1081 | + uri := obj.URI() |
1082 | + result, err := obj.client.Post(uri, operation, params) |
1083 | + if err != nil { |
1084 | + return nil, err |
1085 | + } |
1086 | + return Parse(obj.client, result) |
1087 | +} |
1088 | |
1089 | === added file 'maasobject_test.go' |
1090 | --- maasobject_test.go 1970-01-01 00:00:00 +0000 |
1091 | +++ maasobject_test.go 2013-02-06 17:03:19 +0000 |
1092 | @@ -0,0 +1,133 @@ |
1093 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
1094 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
1095 | + |
1096 | +package gomaasapi |
1097 | + |
1098 | +import ( |
1099 | + "fmt" |
1100 | + . "launchpad.net/gocheck" |
1101 | + "math/rand" |
1102 | + "net/url" |
1103 | +) |
1104 | + |
1105 | +type MAASObjectSuite struct{} |
1106 | + |
1107 | +var _ = Suite(&MAASObjectSuite{}) |
1108 | + |
1109 | +func makeFakeResourceURI() string { |
1110 | + return "http://example.com/" + fmt.Sprint(rand.Int31()) |
1111 | +} |
1112 | + |
1113 | +func makeFakeMAASObject() jsonMAASObject { |
1114 | + attrs := make(map[string]JSONObject) |
1115 | + attrs[resourceURI] = jsonString(makeFakeResourceURI()) |
1116 | + return jsonMAASObject{jsonMap: jsonMap(attrs)} |
1117 | +} |
1118 | + |
1119 | +// jsonMAASObjects convert only to map or to MAASObject. |
1120 | +func (suite *MAASObjectSuite) TestConversionsMAASObject(c *C) { |
1121 | + input := map[string]JSONObject{resourceURI: jsonString("someplace")} |
1122 | + obj := jsonMAASObject{jsonMap: jsonMap(input)} |
1123 | + |
1124 | + mp, err := obj.GetMap() |
1125 | + c.Check(err, IsNil) |
1126 | + text, err := mp[resourceURI].GetString() |
1127 | + c.Check(err, IsNil) |
1128 | + c.Check(text, Equals, "someplace") |
1129 | + |
1130 | + maasobj, err := obj.GetMAASObject() |
1131 | + c.Check(err, IsNil) |
1132 | + _ = maasobj.(jsonMAASObject) |
1133 | + |
1134 | + _, err = obj.GetString() |
1135 | + c.Check(err, NotNil) |
1136 | + _, err = obj.GetFloat64() |
1137 | + c.Check(err, NotNil) |
1138 | + _, err = obj.GetArray() |
1139 | + c.Check(err, NotNil) |
1140 | + _, err = obj.GetBool() |
1141 | + c.Check(err, NotNil) |
1142 | +} |
1143 | + |
1144 | +func (suite *MAASObjectSuite) TestNewJSONMAASObjectPanicsIfNoResourceURI(c *C) { |
1145 | + defer func() { |
1146 | + recoveredError := recover() |
1147 | + c.Check(recoveredError, NotNil) |
1148 | + msg := recoveredError.(error).Error() |
1149 | + c.Check(msg, Matches, ".*no 'resource_uri' key.*") |
1150 | + }() |
1151 | + input := map[string]JSONObject{"test": jsonString("test")} |
1152 | + newJSONMAASObject(jsonMap(input), Client{}) |
1153 | +} |
1154 | + |
1155 | +func (suite *MAASObjectSuite) TestNewJSONMAASObjectPanicsIfResourceURINotString(c *C) { |
1156 | + defer func() { |
1157 | + recoveredError := recover() |
1158 | + c.Check(recoveredError, NotNil) |
1159 | + msg := recoveredError.(error).Error() |
1160 | + c.Check(msg, Matches, ".*the value of 'resource_uri' is not a string.*") |
1161 | + }() |
1162 | + input := map[string]JSONObject{resourceURI: jsonFloat64(77.7)} |
1163 | + newJSONMAASObject(jsonMap(input), Client{}) |
1164 | +} |
1165 | + |
1166 | +func (suite *MAASObjectSuite) TestNewJSONMAASObjectPanicsIfResourceURINotURL(c *C) { |
1167 | + defer func() { |
1168 | + recoveredError := recover() |
1169 | + c.Check(recoveredError, NotNil) |
1170 | + msg := recoveredError.(error).Error() |
1171 | + c.Check(msg, Matches, ".*the value of 'resource_uri' is not a valid URL.*") |
1172 | + }() |
1173 | + input := map[string]JSONObject{resourceURI: jsonString("")} |
1174 | + newJSONMAASObject(jsonMap(input), Client{}) |
1175 | +} |
1176 | + |
1177 | +func (suite *MAASObjectSuite) TestNewJSONMAASObjectSetsUpURI(c *C) { |
1178 | + URI, _ := url.Parse("http://example.com/a/resource") |
1179 | + input := map[string]JSONObject{resourceURI: jsonString(URI.String())} |
1180 | + obj := newJSONMAASObject(jsonMap(input), Client{}) |
1181 | + c.Check(obj.uri, DeepEquals, URI) |
1182 | +} |
1183 | + |
1184 | +func (suite *MAASObjectSuite) TestURL(c *C) { |
1185 | + baseURL, _ := url.Parse("http://example.com/") |
1186 | + uri := "http://example.com/a/resource" |
1187 | + resourceURL, _ := url.Parse(uri) |
1188 | + input := map[string]JSONObject{resourceURI: jsonString(uri)} |
1189 | + client := Client{BaseURL: baseURL} |
1190 | + obj := newJSONMAASObject(jsonMap(input), client) |
1191 | + |
1192 | + URL := obj.URL() |
1193 | + |
1194 | + c.Check(URL, DeepEquals, resourceURL) |
1195 | +} |
1196 | + |
1197 | +func (suite *MAASObjectSuite) TestGetSubObject(c *C) { |
1198 | + baseURL, _ := url.Parse("http://example.com/") |
1199 | + uri := "http://example.com/a/resource/" |
1200 | + input := map[string]JSONObject{resourceURI: jsonString(uri)} |
1201 | + client := Client{BaseURL: baseURL} |
1202 | + obj := newJSONMAASObject(jsonMap(input), client) |
1203 | + subName := "/test" |
1204 | + |
1205 | + subObj := obj.GetSubObject(subName) |
1206 | + subURL := subObj.URL() |
1207 | + |
1208 | + // uri ends with a slash and subName starts with one, but the two paths |
1209 | + // should be concatenated as "http://example.com/a/resource/test/". |
1210 | + expectedSubURL, _ := url.Parse("http://example.com/a/resource/test/") |
1211 | + c.Check(subURL, DeepEquals, expectedSubURL) |
1212 | +} |
1213 | + |
1214 | +func (suite *MAASObjectSuite) TestGetField(c *C) { |
1215 | + uri := "http://example.com/a/resource" |
1216 | + fieldName := "field name" |
1217 | + fieldValue := "a value" |
1218 | + input := map[string]JSONObject{ |
1219 | + resourceURI: jsonString(uri), fieldName: jsonString(fieldValue), |
1220 | + } |
1221 | + obj := jsonMAASObject{jsonMap: jsonMap(input)} |
1222 | + value, err := obj.GetField(fieldName) |
1223 | + c.Check(err, IsNil) |
1224 | + c.Check(value, Equals, fieldValue) |
1225 | +} |
1226 | |
1227 | === added file 'oauth.go' |
1228 | --- oauth.go 1970-01-01 00:00:00 +0000 |
1229 | +++ oauth.go 2013-02-06 17:03:19 +0000 |
1230 | @@ -0,0 +1,81 @@ |
1231 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
1232 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
1233 | + |
1234 | +package gomaasapi |
1235 | + |
1236 | +import ( |
1237 | + "crypto/rand" |
1238 | + "fmt" |
1239 | + "math/big" |
1240 | + "net/http" |
1241 | + "net/url" |
1242 | + "strconv" |
1243 | + "strings" |
1244 | + "time" |
1245 | +) |
1246 | + |
1247 | +var nonceMax = big.NewInt(100000000) |
1248 | + |
1249 | +func generateNonce() (string, error) { |
1250 | + randInt, err := rand.Int(rand.Reader, nonceMax) |
1251 | + if err != nil { |
1252 | + return "", err |
1253 | + } |
1254 | + return strconv.Itoa(int(randInt.Int64())), nil |
1255 | +} |
1256 | + |
1257 | +func generateTimestamp() string { |
1258 | + return strconv.Itoa(int(time.Now().Unix())) |
1259 | +} |
1260 | + |
1261 | +type OAuthSigner interface { |
1262 | + OAuthSign(request *http.Request) error |
1263 | +} |
1264 | + |
1265 | +type OAuthToken struct { |
1266 | + ConsumerKey string |
1267 | + ConsumerSecret string |
1268 | + TokenKey string |
1269 | + TokenSecret string |
1270 | +} |
1271 | + |
1272 | +// Trick to ensure *plainTextOAuthSigner implements the OAuthSigner interface. |
1273 | +var _ OAuthSigner = (*plainTextOAuthSigner)(nil) |
1274 | + |
1275 | +type plainTextOAuthSigner struct { |
1276 | + token *OAuthToken |
1277 | + realm string |
1278 | +} |
1279 | + |
1280 | +func NewPlainTestOAuthSigner(token *OAuthToken, realm string) (OAuthSigner, error) { |
1281 | + return &plainTextOAuthSigner{token, realm}, nil |
1282 | +} |
1283 | + |
1284 | +// OAuthSignPLAINTEXT signs the provided request using the OAuth PLAINTEXT |
1285 | +// method: http://oauth.net/core/1.0/#anchor22. |
1286 | +func (signer plainTextOAuthSigner) OAuthSign(request *http.Request) error { |
1287 | + |
1288 | + signature := signer.token.ConsumerSecret + `&` + signer.token.TokenSecret |
1289 | + nonce, err := generateNonce() |
1290 | + if err != nil { |
1291 | + return err |
1292 | + } |
1293 | + authData := map[string]string{ |
1294 | + "realm": signer.realm, |
1295 | + "oauth_consumer_key": signer.token.ConsumerKey, |
1296 | + "oauth_token": signer.token.TokenKey, |
1297 | + "oauth_signature_method": "PLAINTEXT", |
1298 | + "oauth_signature": signature, |
1299 | + "oauth_timestamp": generateTimestamp(), |
1300 | + "oauth_nonce": nonce, |
1301 | + "oauth_version": "1.0", |
1302 | + } |
1303 | + // Build OAuth header. |
1304 | + var authHeader []string |
1305 | + for key, value := range authData { |
1306 | + authHeader = append(authHeader, fmt.Sprintf(`%s="%s"`, key, url.QueryEscape(value))) |
1307 | + } |
1308 | + strHeader := "OAuth " + strings.Join(authHeader, ", ") |
1309 | + request.Header.Add("Authorization", strHeader) |
1310 | + return nil |
1311 | +} |
1312 | |
1313 | === added directory 'templates' |
1314 | === added file 'templates/source.go' |
1315 | --- templates/source.go 1970-01-01 00:00:00 +0000 |
1316 | +++ templates/source.go 2013-02-06 17:03:19 +0000 |
1317 | @@ -0,0 +1,4 @@ |
1318 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
1319 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
1320 | + |
1321 | +package gomaasapi |
1322 | |
1323 | === added file 'templates/source_test.go' |
1324 | --- templates/source_test.go 1970-01-01 00:00:00 +0000 |
1325 | +++ templates/source_test.go 2013-02-06 17:03:19 +0000 |
1326 | @@ -0,0 +1,13 @@ |
1327 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
1328 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
1329 | + |
1330 | +package gomaasapi |
1331 | + |
1332 | +import ( |
1333 | + . "launchpad.net/gocheck" |
1334 | +) |
1335 | + |
1336 | +// TODO: Replace with real test functions. Give them real names. |
1337 | +func (suite *GomaasapiTestSuite) TestXXX(c *C) { |
1338 | + c.Check(2+2, Equals, 4) |
1339 | +} |
1340 | |
1341 | === added file 'testing.go' |
1342 | --- testing.go 1970-01-01 00:00:00 +0000 |
1343 | +++ testing.go 2013-02-06 17:03:19 +0000 |
1344 | @@ -0,0 +1,46 @@ |
1345 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
1346 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
1347 | + |
1348 | +package gomaasapi |
1349 | + |
1350 | +import ( |
1351 | + "fmt" |
1352 | + "io/ioutil" |
1353 | + "net/http" |
1354 | + "net/http/httptest" |
1355 | +) |
1356 | + |
1357 | +type singleServingServer struct { |
1358 | + *httptest.Server |
1359 | + requestContent *string |
1360 | + requestHeader *http.Header |
1361 | +} |
1362 | + |
1363 | +// newSingleServingServer creates a single-serving test http server which will |
1364 | +// return only one response as defined by the passed arguments. |
1365 | +func newSingleServingServer(uri string, response string, code int) *singleServingServer { |
1366 | + var requestContent string |
1367 | + var requestHeader http.Header |
1368 | + var requested bool |
1369 | + handler := func(writer http.ResponseWriter, request *http.Request) { |
1370 | + if requested { |
1371 | + http.Error(writer, "Already requested", http.StatusServiceUnavailable) |
1372 | + } |
1373 | + res, err := ioutil.ReadAll(request.Body) |
1374 | + if err != nil { |
1375 | + panic(err) |
1376 | + } |
1377 | + requestContent = string(res) |
1378 | + requestHeader = request.Header |
1379 | + if request.URL.String() != uri { |
1380 | + errorMsg := fmt.Sprintf("Error 404: page not found (expected '%v', got '%v').", uri, request.URL.String()) |
1381 | + http.Error(writer, errorMsg, http.StatusNotFound) |
1382 | + } else { |
1383 | + writer.WriteHeader(code) |
1384 | + fmt.Fprint(writer, response) |
1385 | + } |
1386 | + requested = true |
1387 | + } |
1388 | + server := httptest.NewServer(http.HandlerFunc(handler)) |
1389 | + return &singleServingServer{server, &requestContent, &requestHeader} |
1390 | +} |
1391 | |
1392 | === added file 'testservice.go' |
1393 | --- testservice.go 1970-01-01 00:00:00 +0000 |
1394 | +++ testservice.go 2013-02-06 17:03:19 +0000 |
1395 | @@ -0,0 +1,240 @@ |
1396 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
1397 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
1398 | + |
1399 | +package gomaasapi |
1400 | + |
1401 | +import ( |
1402 | + "encoding/json" |
1403 | + "fmt" |
1404 | + "net/http" |
1405 | + "net/http/httptest" |
1406 | + "net/url" |
1407 | + "regexp" |
1408 | +) |
1409 | + |
1410 | +// TestMAASObject is a fake MAAS server MAASObject. |
1411 | +type TestMAASObject struct { |
1412 | + MAASObject |
1413 | + TestServer *TestServer |
1414 | +} |
1415 | + |
1416 | +// TestMAASObject implements the MAASObject interface. |
1417 | +var _ MAASObject = (*TestMAASObject)(nil) |
1418 | + |
1419 | +// NewTestMAAS returns a TestMAASObject that implements the MAASObject |
1420 | +// interface and thus can be used as a test object instead of the one returned |
1421 | +// by gomaasapi.NewMAAS(). |
1422 | +func NewTestMAAS(version string) *TestMAASObject { |
1423 | + server := NewTestServer(version) |
1424 | + authClient, _ := NewAnonymousClient(server.URL + fmt.Sprintf("/api/%s/", version)) |
1425 | + return &TestMAASObject{NewMAAS(*authClient), server} |
1426 | +} |
1427 | + |
1428 | +// Close shuts down the test server. |
1429 | +func (testMAASObject *TestMAASObject) Close() { |
1430 | + testMAASObject.TestServer.Close() |
1431 | +} |
1432 | + |
1433 | +// A TestServer is an HTTP server listening on a system-chosen port on the |
1434 | +// local loopback interface, which simulates the behavior of a MAAS server. |
1435 | +// It is intendend for use in end-to-end HTTP tests using the gomaasapi |
1436 | +// library. |
1437 | +type TestServer struct { |
1438 | + *httptest.Server |
1439 | + serveMux *http.ServeMux |
1440 | + nodes map[string]MAASObject |
1441 | + client Client |
1442 | + nodeOperations map[string][]string |
1443 | + version string |
1444 | +} |
1445 | + |
1446 | +func getNodeURI(version, systemId string) string { |
1447 | + return fmt.Sprintf("/api/%s/nodes/%s/", version, systemId) |
1448 | +} |
1449 | + |
1450 | +// Clear clears all the fake data stored and recorded by the test server |
1451 | +// (nodes, recorded operations, etc.). |
1452 | +func (server *TestServer) Clear() { |
1453 | + server.nodes = make(map[string]MAASObject) |
1454 | + server.nodeOperations = make(map[string][]string) |
1455 | +} |
1456 | + |
1457 | +// NodeOperations returns the map containing the list of the operations |
1458 | +// performed for each node. |
1459 | +func (server *TestServer) NodeOperations() map[string][]string { |
1460 | + return server.nodeOperations |
1461 | +} |
1462 | + |
1463 | +func (server *TestServer) addNodeOperation(systemId, operation string) { |
1464 | + operations, present := server.nodeOperations[systemId] |
1465 | + if !present { |
1466 | + operations = []string{operation} |
1467 | + } else { |
1468 | + operations = append(operations, operation) |
1469 | + } |
1470 | + server.nodeOperations[systemId] = operations |
1471 | +} |
1472 | + |
1473 | +// NewNode creates a MAAS node. The provided string should be a valid json |
1474 | +// string representing a map and contain a string value for the key |
1475 | +// 'system_id'. e.g. `{"system_id": "mysystemid"}`. |
1476 | +// If one of these conditions is not met, NewNode panics. |
1477 | +func (server *TestServer) NewNode(json string) MAASObject { |
1478 | + obj, err := Parse(server.client, []byte(json)) |
1479 | + if err != nil { |
1480 | + panic(err) |
1481 | + } |
1482 | + mapobj, err := obj.GetMap() |
1483 | + if err != nil { |
1484 | + panic(err) |
1485 | + } |
1486 | + systemId, hasSystemId := mapobj["system_id"] |
1487 | + if !hasSystemId { |
1488 | + panic("The given map json string does not contain a 'system_id' value.") |
1489 | + } |
1490 | + stringSystemId, err := systemId.GetString() |
1491 | + if err != nil { |
1492 | + panic(err) |
1493 | + } |
1494 | + resourceUri := getNodeURI(server.version, stringSystemId) |
1495 | + mapobj[resourceURI] = jsonString(resourceUri) |
1496 | + maasobj := newJSONMAASObject(mapobj, server.client) |
1497 | + server.nodes[stringSystemId] = maasobj |
1498 | + return maasobj |
1499 | +} |
1500 | + |
1501 | +// Returns a map associating all the nodes' system ids with the nodes' |
1502 | +// objects. |
1503 | +func (server *TestServer) Nodes() map[string]MAASObject { |
1504 | + return server.nodes |
1505 | +} |
1506 | + |
1507 | +// ChangeNode updates a node with the given key/value. |
1508 | +func (server *TestServer) ChangeNode(systemId, key, value string) { |
1509 | + node, found := server.nodes[systemId] |
1510 | + if !found { |
1511 | + panic("No node with such 'system_id'.") |
1512 | + } |
1513 | + mapObj, _ := node.GetMap() |
1514 | + mapObj[key] = jsonString(value) |
1515 | +} |
1516 | + |
1517 | +func getNodeListingURL(version string) string { |
1518 | + return fmt.Sprintf("/api/%s/nodes/", version) |
1519 | +} |
1520 | + |
1521 | +func getNodeURLRE(version string) *regexp.Regexp { |
1522 | + reString := fmt.Sprintf("^/api/%s/nodes/([^/]*)/$", version) |
1523 | + return regexp.MustCompile(reString) |
1524 | +} |
1525 | + |
1526 | +// NewTestServer starts and returns a new MAAS test server. The caller should call Close when finished, to shut it down. |
1527 | +func NewTestServer(version string) *TestServer { |
1528 | + server := &TestServer{version: version} |
1529 | + |
1530 | + serveMux := http.NewServeMux() |
1531 | + nodeListingURL := getNodeListingURL(server.version) |
1532 | + // Register handler for '/api/<version>/nodes/*'. |
1533 | + serveMux.HandleFunc(nodeListingURL, func(w http.ResponseWriter, r *http.Request) { |
1534 | + nodesHandler(server, w, r) |
1535 | + }) |
1536 | + |
1537 | + newServer := httptest.NewServer(serveMux) |
1538 | + client, _ := NewAnonymousClient(newServer.URL) |
1539 | + server.Server = newServer |
1540 | + server.serveMux = serveMux |
1541 | + server.client = *client |
1542 | + server.Clear() |
1543 | + return server |
1544 | +} |
1545 | + |
1546 | +// nodesHandler handles requests for '/api/<version>/nodes/*'. |
1547 | +func nodesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { |
1548 | + values, _ := url.ParseQuery(r.URL.RawQuery) |
1549 | + op := values.Get("op") |
1550 | + nodeURLRE := getNodeURLRE(server.version) |
1551 | + nodeURLMatch := nodeURLRE.FindStringSubmatch(r.URL.Path) |
1552 | + nodeListingURL := getNodeListingURL(server.version) |
1553 | + switch { |
1554 | + case r.Method == "GET" && op == "list" && r.URL.Path == nodeListingURL: |
1555 | + // Node listing operation. |
1556 | + nodeListingHandler(server, w, r) |
1557 | + case nodeURLMatch != nil: |
1558 | + // Request for a single node. |
1559 | + nodeHandler(server, w, r, nodeURLMatch[1], op) |
1560 | + default: |
1561 | + // Default handler: not found. |
1562 | + http.NotFoundHandler().ServeHTTP(w, r) |
1563 | + } |
1564 | +} |
1565 | + |
1566 | +func marshalNode(node MAASObject) string { |
1567 | + mapObj, _ := node.GetMap() |
1568 | + res, _ := json.Marshal(mapObj) |
1569 | + return string(res) |
1570 | + |
1571 | +} |
1572 | + |
1573 | +// nodeHandler handles requests for '/api/<version>/nodes/<system_id>/'. |
1574 | +func nodeHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string, operation string) { |
1575 | + node, ok := server.nodes[systemId] |
1576 | + if !ok { |
1577 | + http.NotFoundHandler().ServeHTTP(w, r) |
1578 | + return |
1579 | + } |
1580 | + if r.Method == "GET" { |
1581 | + if operation == "" { |
1582 | + w.WriteHeader(http.StatusOK) |
1583 | + fmt.Fprint(w, marshalNode(node)) |
1584 | + return |
1585 | + } else { |
1586 | + w.WriteHeader(http.StatusBadRequest) |
1587 | + return |
1588 | + } |
1589 | + } |
1590 | + if r.Method == "POST" { |
1591 | + // The only operations supported are "start", "stop" and "release". |
1592 | + if operation == "start" || operation == "stop" || operation == "release" { |
1593 | + // Record operation on node. |
1594 | + server.addNodeOperation(systemId, operation) |
1595 | + |
1596 | + w.WriteHeader(http.StatusOK) |
1597 | + fmt.Fprint(w, marshalNode(node)) |
1598 | + return |
1599 | + } else { |
1600 | + w.WriteHeader(http.StatusBadRequest) |
1601 | + return |
1602 | + } |
1603 | + } |
1604 | + if r.Method == "DELETE" { |
1605 | + delete(server.nodes, systemId) |
1606 | + w.WriteHeader(http.StatusOK) |
1607 | + return |
1608 | + } |
1609 | + http.NotFoundHandler().ServeHTTP(w, r) |
1610 | +} |
1611 | + |
1612 | +func contains(slice []string, val string) bool { |
1613 | + for _, item := range slice { |
1614 | + if item == val { |
1615 | + return true |
1616 | + } |
1617 | + } |
1618 | + return false |
1619 | +} |
1620 | + |
1621 | +// nodeListingHandler handles requests for '/nodes/'. |
1622 | +func nodeListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { |
1623 | + values, _ := url.ParseQuery(r.URL.RawQuery) |
1624 | + ids, hasId := values["id"] |
1625 | + var convertedNodes = []map[string]JSONObject{} |
1626 | + for systemId, node := range server.nodes { |
1627 | + if !hasId || contains(ids, systemId) { |
1628 | + mapp, _ := node.GetMap() |
1629 | + convertedNodes = append(convertedNodes, mapp) |
1630 | + } |
1631 | + } |
1632 | + res, _ := json.Marshal(convertedNodes) |
1633 | + w.WriteHeader(http.StatusOK) |
1634 | + fmt.Fprint(w, string(res)) |
1635 | +} |
1636 | |
1637 | === added file 'testservice_test.go' |
1638 | --- testservice_test.go 1970-01-01 00:00:00 +0000 |
1639 | +++ testservice_test.go 2013-02-06 17:03:19 +0000 |
1640 | @@ -0,0 +1,255 @@ |
1641 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
1642 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
1643 | + |
1644 | +package gomaasapi |
1645 | + |
1646 | +import ( |
1647 | + "encoding/json" |
1648 | + "fmt" |
1649 | + . "launchpad.net/gocheck" |
1650 | + "net/http" |
1651 | + "net/url" |
1652 | +) |
1653 | + |
1654 | +type TestServerSuite struct { |
1655 | + server *TestServer |
1656 | +} |
1657 | + |
1658 | +var _ = Suite(&TestServerSuite{}) |
1659 | + |
1660 | +func (suite *TestServerSuite) SetUpTest(c *C) { |
1661 | + server := NewTestServer("1.0") |
1662 | + suite.server = server |
1663 | +} |
1664 | + |
1665 | +func (suite *TestServerSuite) TearDownTest(c *C) { |
1666 | + suite.server.Close() |
1667 | +} |
1668 | + |
1669 | +func (suite *TestServerSuite) TestNewTestServerReturnsTestServer(c *C) { |
1670 | + handler := func(w http.ResponseWriter, r *http.Request) { |
1671 | + w.WriteHeader(http.StatusAccepted) |
1672 | + } |
1673 | + suite.server.serveMux.HandleFunc("/test/", handler) |
1674 | + resp, err := http.Get(suite.server.Server.URL + "/test/") |
1675 | + |
1676 | + c.Check(err, IsNil) |
1677 | + c.Check(resp.StatusCode, Equals, http.StatusAccepted) |
1678 | +} |
1679 | + |
1680 | +func (suite *TestServerSuite) TestGetResourceURI(c *C) { |
1681 | + c.Check(getNodeURI("version", "test"), Equals, "/api/version/nodes/test/") |
1682 | +} |
1683 | + |
1684 | +func (suite *TestServerSuite) TestHandlesNodeListingUnknownPath(c *C) { |
1685 | + invalidPath := fmt.Sprintf("/api/%s/nodes/invalid/path/", suite.server.version) |
1686 | + resp, err := http.Get(suite.server.Server.URL + invalidPath) |
1687 | + |
1688 | + c.Check(err, IsNil) |
1689 | + c.Check(resp.StatusCode, Equals, http.StatusNotFound) |
1690 | +} |
1691 | + |
1692 | +func (suite *TestServerSuite) TestNewNode(c *C) { |
1693 | + input := `{"system_id": "mysystemid"}` |
1694 | + |
1695 | + newNode := suite.server.NewNode(input) |
1696 | + |
1697 | + c.Check(len(suite.server.nodes), Equals, 1) |
1698 | + c.Check(suite.server.nodes["mysystemid"], DeepEquals, newNode) |
1699 | +} |
1700 | + |
1701 | +func (suite *TestServerSuite) TestNodesReturnsNodes(c *C) { |
1702 | + input := `{"system_id": "mysystemid"}` |
1703 | + newNode := suite.server.NewNode(input) |
1704 | + |
1705 | + nodesMap := suite.server.Nodes() |
1706 | + |
1707 | + c.Check(len(nodesMap), Equals, 1) |
1708 | + c.Check(nodesMap["mysystemid"], DeepEquals, newNode) |
1709 | +} |
1710 | + |
1711 | +func (suite *TestServerSuite) TestChangeNode(c *C) { |
1712 | + input := `{"system_id": "mysystemid"}` |
1713 | + suite.server.NewNode(input) |
1714 | + suite.server.ChangeNode("mysystemid", "newfield", "newvalue") |
1715 | + |
1716 | + node, _ := suite.server.nodes["mysystemid"] |
1717 | + mapObj, _ := node.GetMap() |
1718 | + field, _ := mapObj["newfield"].GetString() |
1719 | + c.Check(field, Equals, "newvalue") |
1720 | +} |
1721 | + |
1722 | +func (suite *TestServerSuite) TestClearClearsData(c *C) { |
1723 | + input := `{"system_id": "mysystemid"}` |
1724 | + suite.server.NewNode(input) |
1725 | + suite.server.addNodeOperation("mysystemid", "start") |
1726 | + |
1727 | + suite.server.Clear() |
1728 | + |
1729 | + c.Check(len(suite.server.nodes), Equals, 0) |
1730 | + c.Check(len(suite.server.nodeOperations), Equals, 0) |
1731 | +} |
1732 | + |
1733 | +func (suite *TestServerSuite) TestAddNodeOperationPopulatesOperations(c *C) { |
1734 | + input := `{"system_id": "mysystemid"}` |
1735 | + suite.server.NewNode(input) |
1736 | + |
1737 | + suite.server.addNodeOperation("mysystemid", "start") |
1738 | + suite.server.addNodeOperation("mysystemid", "stop") |
1739 | + |
1740 | + nodeOperations := suite.server.NodeOperations() |
1741 | + operations := nodeOperations["mysystemid"] |
1742 | + c.Check(operations, DeepEquals, []string{"start", "stop"}) |
1743 | +} |
1744 | + |
1745 | +func (suite *TestServerSuite) TestNewNodeRequiresJSONString(c *C) { |
1746 | + input := `invalid:json` |
1747 | + defer func() { |
1748 | + recoveredError := recover().(*json.SyntaxError) |
1749 | + c.Check(recoveredError, NotNil) |
1750 | + c.Check(recoveredError.Error(), Matches, ".*invalid character.*") |
1751 | + }() |
1752 | + suite.server.NewNode(input) |
1753 | +} |
1754 | + |
1755 | +func (suite *TestServerSuite) TestNewNodeRequiresSystemIdKey(c *C) { |
1756 | + input := `{"test": "test"}` |
1757 | + defer func() { |
1758 | + recoveredError := recover() |
1759 | + c.Check(recoveredError, NotNil) |
1760 | + c.Check(recoveredError, Matches, ".*does not contain a 'system_id' value.") |
1761 | + }() |
1762 | + suite.server.NewNode(input) |
1763 | +} |
1764 | + |
1765 | +func (suite *TestServerSuite) TestHandlesNodeRequestNotFound(c *C) { |
1766 | + getURI := fmt.Sprintf("/api/%s/nodes/test/", suite.server.version) |
1767 | + resp, err := http.Get(suite.server.Server.URL + getURI) |
1768 | + |
1769 | + c.Check(err, IsNil) |
1770 | + c.Check(resp.StatusCode, Equals, http.StatusNotFound) |
1771 | +} |
1772 | + |
1773 | +func (suite *TestServerSuite) TestHandlesNodeUnknownOperation(c *C) { |
1774 | + input := `{"system_id": "mysystemid"}` |
1775 | + suite.server.NewNode(input) |
1776 | + postURI := fmt.Sprintf("/api/%s/nodes/mysystemid/?op=unknown/", suite.server.version) |
1777 | + respStart, err := http.Post(suite.server.Server.URL+postURI, "", nil) |
1778 | + |
1779 | + c.Check(err, IsNil) |
1780 | + c.Check(respStart.StatusCode, Equals, http.StatusBadRequest) |
1781 | +} |
1782 | + |
1783 | +func (suite *TestServerSuite) TestHandlesNodeDelete(c *C) { |
1784 | + input := `{"system_id": "mysystemid"}` |
1785 | + suite.server.NewNode(input) |
1786 | + deleteURI := fmt.Sprintf("/api/%s/nodes/mysystemid/?op=mysystemid", suite.server.version) |
1787 | + req, err := http.NewRequest("DELETE", suite.server.Server.URL+deleteURI, nil) |
1788 | + client := &http.Client{} |
1789 | + resp, err := client.Do(req) |
1790 | + |
1791 | + c.Check(err, IsNil) |
1792 | + c.Check(resp.StatusCode, Equals, http.StatusOK) |
1793 | + c.Check(len(suite.server.nodes), Equals, 0) |
1794 | +} |
1795 | + |
1796 | +// TestMAASObjectSuite validates that the object created by |
1797 | +// TestMAASObject can be used by the gomaasapi library as if it were a real |
1798 | +// MAAS server. |
1799 | +type TestMAASObjectSuite struct { |
1800 | + TestMAASObject *TestMAASObject |
1801 | +} |
1802 | + |
1803 | +var _ = Suite(&TestMAASObjectSuite{}) |
1804 | + |
1805 | +func (s *TestMAASObjectSuite) SetUpSuite(c *C) { |
1806 | + s.TestMAASObject = NewTestMAAS("1.0") |
1807 | +} |
1808 | + |
1809 | +func (s *TestMAASObjectSuite) TearDownSuite(c *C) { |
1810 | + s.TestMAASObject.Close() |
1811 | +} |
1812 | + |
1813 | +func (s *TestMAASObjectSuite) TearDownTest(c *C) { |
1814 | + s.TestMAASObject.TestServer.Clear() |
1815 | +} |
1816 | + |
1817 | +func (suite *TestMAASObjectSuite) TestListNodes(c *C) { |
1818 | + input := `{"system_id": "mysystemid"}` |
1819 | + suite.TestMAASObject.TestServer.NewNode(input) |
1820 | + nodeListing := suite.TestMAASObject.GetSubObject("nodes") |
1821 | + |
1822 | + listNodeObjects, err := nodeListing.CallGet("list", url.Values{}) |
1823 | + |
1824 | + c.Check(err, IsNil) |
1825 | + listNodes, err := listNodeObjects.GetArray() |
1826 | + c.Check(err, IsNil) |
1827 | + c.Check(len(listNodes), Equals, 1) |
1828 | + node, _ := listNodes[0].GetMAASObject() |
1829 | + systemId, _ := node.GetField("system_id") |
1830 | + c.Check(systemId, Equals, "mysystemid") |
1831 | + resourceURI, _ := node.GetField(resourceURI) |
1832 | + expectedResourceURI := fmt.Sprintf("/api/%s/nodes/mysystemid/", suite.TestMAASObject.TestServer.version) |
1833 | + c.Check(resourceURI, Equals, expectedResourceURI) |
1834 | +} |
1835 | + |
1836 | +func (suite *TestMAASObjectSuite) TestListNodesNoNodes(c *C) { |
1837 | + nodeListing := suite.TestMAASObject.GetSubObject("nodes") |
1838 | + listNodeObjects, err := nodeListing.CallGet("list", url.Values{}) |
1839 | + c.Check(err, IsNil) |
1840 | + |
1841 | + listNodes, err := listNodeObjects.GetArray() |
1842 | + |
1843 | + c.Check(err, IsNil) |
1844 | + c.Check(listNodes, DeepEquals, []JSONObject{}) |
1845 | +} |
1846 | + |
1847 | +func (suite *TestMAASObjectSuite) TestListNodesSelectedNodes(c *C) { |
1848 | + input := `{"system_id": "mysystemid"}` |
1849 | + suite.TestMAASObject.TestServer.NewNode(input) |
1850 | + input2 := `{"system_id": "mysystemid2"}` |
1851 | + suite.TestMAASObject.TestServer.NewNode(input2) |
1852 | + nodeListing := suite.TestMAASObject.GetSubObject("nodes") |
1853 | + |
1854 | + listNodeObjects, err := nodeListing.CallGet("list", url.Values{"id": {"mysystemid2"}}) |
1855 | + |
1856 | + c.Check(err, IsNil) |
1857 | + listNodes, err := listNodeObjects.GetArray() |
1858 | + c.Check(err, IsNil) |
1859 | + c.Check(len(listNodes), Equals, 1) |
1860 | + node, _ := listNodes[0].GetMAASObject() |
1861 | + systemId, _ := node.GetField("system_id") |
1862 | + c.Check(systemId, Equals, "mysystemid2") |
1863 | +} |
1864 | + |
1865 | +func (suite *TestMAASObjectSuite) TestDeleteNode(c *C) { |
1866 | + input := `{"system_id": "mysystemid"}` |
1867 | + node := suite.TestMAASObject.TestServer.NewNode(input) |
1868 | + |
1869 | + err := node.Delete() |
1870 | + |
1871 | + c.Check(err, IsNil) |
1872 | + c.Check(suite.TestMAASObject.TestServer.Nodes(), DeepEquals, map[string]MAASObject{}) |
1873 | +} |
1874 | + |
1875 | +func (suite *TestMAASObjectSuite) TestOperationsOnNode(c *C) { |
1876 | + input := `{"system_id": "mysystemid"}` |
1877 | + node := suite.TestMAASObject.TestServer.NewNode(input) |
1878 | + operations := []string{"start", "stop", "release"} |
1879 | + for _, operation := range operations { |
1880 | + _, err := node.CallPost(operation, url.Values{}) |
1881 | + c.Check(err, IsNil) |
1882 | + } |
1883 | +} |
1884 | + |
1885 | +func (suite *TestMAASObjectSuite) TestOperationsOnNodeGetRecorded(c *C) { |
1886 | + input := `{"system_id": "mysystemid"}` |
1887 | + node := suite.TestMAASObject.TestServer.NewNode(input) |
1888 | + |
1889 | + _, err := node.CallPost("start", url.Values{}) |
1890 | + |
1891 | + c.Check(err, IsNil) |
1892 | + nodeOperations := suite.TestMAASObject.TestServer.NodeOperations() |
1893 | + operations := nodeOperations["mysystemid"] |
1894 | + c.Check(operations, DeepEquals, []string{"start"}) |
1895 | +} |
1896 | |
1897 | === added file 'util.go' |
1898 | --- util.go 1970-01-01 00:00:00 +0000 |
1899 | +++ util.go 2013-02-06 17:03:19 +0000 |
1900 | @@ -0,0 +1,27 @@ |
1901 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
1902 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
1903 | + |
1904 | +package gomaasapi |
1905 | + |
1906 | +import ( |
1907 | + "strings" |
1908 | +) |
1909 | + |
1910 | +// JoinURLs joins a base URL and a subpath together. |
1911 | +// Regardless of whether baseURL ends in a trailing slash (or even multiple |
1912 | +// trailing slashes), or whether there are any leading slashes at the begining |
1913 | +// of path, the two will always be joined together by a single slash. |
1914 | +func JoinURLs(baseURL, path string) string { |
1915 | + return strings.TrimRight(baseURL, "/") + "/" + strings.TrimLeft(path, "/") |
1916 | +} |
1917 | + |
1918 | +// EnsureTrailingSlash appends a slash at the end of the given string unless |
1919 | +// there already is one. |
1920 | +// This is used to create the kind of normalized URLs that Django expects. |
1921 | +// (to avoid Django's redirection when an URL does not ends with a slash.) |
1922 | +func EnsureTrailingSlash(URL string) string { |
1923 | + if strings.HasSuffix(URL, "/") { |
1924 | + return URL |
1925 | + } |
1926 | + return URL + "/" |
1927 | +} |
1928 | |
1929 | === added file 'util_test.go' |
1930 | --- util_test.go 1970-01-01 00:00:00 +0000 |
1931 | +++ util_test.go 2013-02-06 17:03:19 +0000 |
1932 | @@ -0,0 +1,32 @@ |
1933 | +// Copyright 2013 Canonical Ltd. This software is licensed under the |
1934 | +// GNU Lesser General Public License version 3 (see the file COPYING). |
1935 | + |
1936 | +package gomaasapi |
1937 | + |
1938 | +import ( |
1939 | + . "launchpad.net/gocheck" |
1940 | +) |
1941 | + |
1942 | +func (suite *GomaasapiTestSuite) TestJoinURLsAppendsPathToBaseURL(c *C) { |
1943 | + c.Check(JoinURLs("http://example.com/", "foo"), Equals, "http://example.com/foo") |
1944 | +} |
1945 | + |
1946 | +func (suite *GomaasapiTestSuite) TestJoinURLsAddsSlashIfNeeded(c *C) { |
1947 | + c.Check(JoinURLs("http://example.com/foo", "bar"), Equals, "http://example.com/foo/bar") |
1948 | +} |
1949 | + |
1950 | +func (suite *GomaasapiTestSuite) TestJoinURLsNormalizesDoubleSlash(c *C) { |
1951 | + c.Check(JoinURLs("http://example.com/base/", "/szot"), Equals, "http://example.com/base/szot") |
1952 | +} |
1953 | + |
1954 | +func (suite *GomaasapiTestSuite) TestEnsureTrailingSlashAppendsSlashIfMissing(c *C) { |
1955 | + c.Check(EnsureTrailingSlash("test"), Equals, "test/") |
1956 | +} |
1957 | + |
1958 | +func (suite *GomaasapiTestSuite) TestEnsureTrailingSlashDoesNotAppendIfPresent(c *C) { |
1959 | + c.Check(EnsureTrailingSlash("test/"), Equals, "test/") |
1960 | +} |
1961 | + |
1962 | +func (suite *GomaasapiTestSuite) TestEnsureTrailingSlashReturnsSlashIfEmpty(c *C) { |
1963 | + c.Check(EnsureTrailingSlash(""), Equals, "/") |
1964 | +} |
Reviewers: mp+145811_ code.launchpad. net,
Message:
Please take a look.
Description:
Rollup of all relevent changes on lp:gomaasapi
This is for review only, showing the current trunk of the project in
the diff (from the initial commit of the licence and a couple of stub
go files). Note it's not possible for the Red squad to update this
proposal directly, so don't expect code changes in response to comments
to appear here, please watch for other merge proposals. :)
https:/ /code.launchpad .net/~gz/ gomaasapi/ gomaasapi/ +merge/ 145811
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/7228069/
Affected files: live_example. go source_ test.go
A README
A [revision details]
A client.go
A client_test.go
A example/
M gomaasapi.go
M gomaasapi_test.go
A jsonobject.go
A jsonobject_test.go
A maas.go
A maas_test.go
A maasobject.go
A maasobject_test.go
A oauth.go
A templates/source.go
A templates/
A testing.go
A util.go
A util_test.go