Merge lp:~jtv/gwacl/storage-urls into lp:gwacl
- storage-urls
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Jeroen T. Vermeulen |
Approved revision: | 113 |
Merged at revision: | 102 |
Proposed branch: | lp:~jtv/gwacl/storage-urls |
Merge into: | lp:gwacl |
Diff against target: |
576 lines (+187/-190) 6 files modified
storage_base.go (+44/-53) storage_base_test.go (+54/-82) storage_test.go (+5/-4) utils.go (+21/-20) utils_test.go (+60/-26) x509session.go (+3/-5) |
To merge this branch: | bzr merge lp:~jtv/gwacl/storage-urls |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | Approve | ||
Gavin Panella | Pending | ||
Review via email: mp+161562@code.launchpad.net |
This proposal supersedes a proposal from 2013-04-29.
Commit message
Factor out some repeated composition of storage URLs.
Description of the change
This increases the testing code a bit, but simplifies run-of-the-mill code nicely and eliminates some error-prone repetition. I also moved the addURLQueryParam() helper into utils.go, where it probably belongs now. Actually addURLQueryParam() is now a simple wrapper around addURLQueryParams() — I didn't bother testing them separately given how thin the wrapping is. Maybe I should drop the singular one now.
Much of the added complication in tests is an absolutely revolting property of url.URL: it does not maintain a consistent sorting of URL parameters. It's arbitrary. So the tests that happily assumed a fixed ordering, now that we're setting them "properly" through the standard library, start failing randomly. Different subset every time. The tests need to do looser text matching on URLs, and separate matching on the query.
With this change, interpolateURL() is barely used any more — just two cases, with one interpolation argument each. And I'm not even sure the one in x509Session.
Jeroen
Gavin Panella (allenap) wrote : Posted in a previous version of this proposal | # |
Jeroen T. Vermeulen (jtv) wrote : Posted in a previous version of this proposal | # |
Hypothetically, if get{Account,
Gavin Panella (allenap) wrote : Posted in a previous version of this proposal | # |
> Hypothetically, if get{Account,
> their URL from scratch, repeating part of the URL between the three
> but not between call sites, would that assuage your concerns?
Bizarrely enough, even though I like addURLQueryPara
that adds the most noise. There's the map[string]
construction and then the err != nil bits, both of which are almost
entirely noise, and distracting from the intention of the code.
There are two changes I might make:
1. Don't use a map[noise noise noise}, create a new struct to hold
query parameters and pass them using a variadic arg. This might
also help with the ordering issue you mentioned.
-- or --
Use a url.Values instead of a map[noise}. It won't help with the
ordering though, if this is where the problem lies.
2. Panic instead of returning an error? I'm not saying anything new,
but returning errors means loads of err != nil noise, and panicking
is awkward to deal with because defer/recover is about as pleasant
as rubbing fire ants into one's squishy bits. The choice is not
clear.
However, if the URL can't be parsed in the situations we have in
gwacl *at this point in time* (as you say, the URL is tightly
controlled), I would say that's a programmatic error rather than a
runtime error, and so panicking seems appropriate.
Something like:
type queryParam struct {
name string
value string
}
func addURLQueryParams(
originalURL string, params ...queryParam) string {
parsedURL, err := url.Parse(
if err != nil {
}
query := parsedURL.Query()
for param := range params {
}
parsedURL
return parsedURL.String()
}
url := "http://
addURLQueryPa
url, queryParam{"fred", "foo"}, queryParam("fred", "bar"})
c.Assert(url, Equals, "http://
Jeroen T. Vermeulen (jtv) wrote : Posted in a previous version of this proposal | # |
About interpolateURL, I think fmt.Sprintf("http://
Jeroen T. Vermeulen (jtv) wrote : Posted in a previous version of this proposal | # |
> 1. Don't use a map[noise noise noise}, create a new struct to hold
> query parameters and pass them using a variadic arg. This might
> also help with the ordering issue you mentioned.
Good one. I'll do that. It also unifies the two functions.
> 2. Panic instead of returning an error? I'm not saying anything new,
> but returning errors means loads of err != nil noise, and panicking
> is awkward to deal with because defer/recover is about as pleasant
> as rubbing fire ants into one's squishy bits. The choice is not
> clear.
>
> However, if the URL can't be parsed in the situations we have in
> gwacl *at this point in time* (as you say, the URL is tightly
> controlled), I would say that's a programmatic error rather than a
> runtime error, and so panicking seems appropriate.
I toyed with the idea, but didn't have the courage to do it. But if I have an accomplice, that makes me feel safer.
Attempts to write clean code feel so futile in Go... In any other language, if I see the same 4 or 5 lines repeated all over the place, that's intolerable so I extract a function. But in Go, calling the function is 4 or 5 lines of code. Maybe 6 if it means I have to pre-define a variable to avoid shadowing. Or I can compress it a bit if I don't mind making the code harder to read — but I do.
Jeroen
Jeroen T. Vermeulen (jtv) wrote : Posted in a previous version of this proposal | # |
Okay, I removed interpolateURL and that simplified a few things (like over-interpolation bugs we had to watch out for). I also whittled down the redundancy in the get*URL() tests. The branch now removes about as many lines as it adds. Since I'm more or less manning the fort alone this week, I'm going to land this and then we can argue over any remaining details later.
Julian Edwards (julian-edwards) wrote : Posted in a previous version of this proposal | # |
On 29/04/13 22:57, Gavin Panella wrote:
> Review: Approve
>
> I think addURLQueryParams() is good.
>
> However, I don't think get{Account,
> resulting changes to storage_base.go are very useful. Despite the lack
> of precedent, the syntax used with interpolateURL() is quite obvious
> and readable, though I am surely biased. There's more code now, it
> obfuscates what's happening, and I suspect YAGNI too.
>
> I think most of the benefit of this branch could be obtained with:
>
> const STORAGE_
>
> s/"http://
>
> I'm torn. On the one hand I could well be biased because I introduced
> interpolateURL(). On the other hand, I have little love for any Go
> code, mine or otherwise. I vote approve, but please don't land this
> unless you're really sure that it improves gwacl, regardless of any
> other factor.
>
FWIW, that URL needs to be configurable at some point. There is a
development simulator service which we may want to use if anyone dares
run up Windows.
Julian Edwards (julian-edwards) wrote : Posted in a previous version of this proposal | # |
No proposals found for merge of https:/
Julian Edwards (julian-edwards) wrote : Posted in a previous version of this proposal | # |
No proposals found for merge of https:/
Jeroen T. Vermeulen (jtv) wrote : | # |
This has already been approved in a previous incarnation of the MP, but I'm resubmitting it because Tarmac is refusing to land it. The previous merge proposal had a prerequisite branch, which has already landed.
Preview Diff
1 | === modified file 'storage_base.go' |
2 | --- storage_base.go 2013-04-30 08:58:11 +0000 |
3 | +++ storage_base.go 2013-04-30 09:36:29 +0000 |
4 | @@ -265,18 +265,23 @@ |
5 | return resp, nil |
6 | } |
7 | |
8 | -// addURLQueryParam adds a query parameter to a URL (and escapes as needed). |
9 | -func addURLQueryParam(originalURL, key, value string) (string, error) { |
10 | - parsedURL, err := url.Parse(originalURL) |
11 | - if err != nil { |
12 | - // Not much we can add beyond that the URL doesn't parse. Leave it |
13 | - // to the caller to provide context. |
14 | - return "", err |
15 | - } |
16 | - query := parsedURL.Query() |
17 | - query.Add(key, value) |
18 | - parsedURL.RawQuery = query.Encode() |
19 | - return parsedURL.String(), nil |
20 | +// getAccountURL returns the base URL for the context's storage account. |
21 | +// (The result ends in a slash.) |
22 | +func (context *StorageContext) getAccountURL() string { |
23 | + escapedAccount := url.QueryEscape(context.Account) |
24 | + return fmt.Sprintf("http://%s.blob.core.windows.net/", escapedAccount) |
25 | +} |
26 | + |
27 | +// getContainerURL returns the URL for a given storage container. |
28 | +// (The result does not end in a slash.) |
29 | +func (context *StorageContext) getContainerURL(container string) string { |
30 | + return context.getAccountURL() + url.QueryEscape(container) |
31 | +} |
32 | + |
33 | +// getFileURL returns the URL for a given file in the given container. |
34 | +// (The result does not end in a slash.) |
35 | +func (context *StorageContext) getFileURL(container, filename string) string { |
36 | + return context.getContainerURL(container) + "/" + url.QueryEscape(filename) |
37 | } |
38 | |
39 | // getListContainersBatch calls the "List Containers" operation on the storage |
40 | @@ -285,16 +290,10 @@ |
41 | // subsequent calls to get additional batches of the same result, pass the |
42 | // NextMarker from the previous call's result. |
43 | func (context *StorageContext) getListContainersBatch(marker string) (*ContainerEnumerationResults, error) { |
44 | - var err error |
45 | - uri := interpolateURL("http://___.blob.core.windows.net/?comp=list", context.Account) |
46 | + uri := context.getAccountURL() |
47 | + uri = addURLQueryParams(uri, "comp", "list") |
48 | if marker != "" { |
49 | - // Add the marker argument. Do it after interpolation, in case the |
50 | - // marker string might contain the interpolation's placeholder |
51 | - // sequence. |
52 | - uri, err = addURLQueryParam(uri, "marker", marker) |
53 | - if err != nil { |
54 | - return nil, fmt.Errorf("malformed storage account URL '%s': %v", uri, err) |
55 | - } |
56 | + uri = addURLQueryParams(uri, "marker", marker) |
57 | } |
58 | req, err := http.NewRequest("GET", uri, nil) |
59 | if err != nil { |
60 | @@ -349,18 +348,13 @@ |
61 | // subsequent calls to get additional batches of the same result, pass the |
62 | // NextMarker from the previous call's result. |
63 | func (context *StorageContext) getListBlobsBatch(container, marker string) (*BlobEnumerationResults, error) { |
64 | - var err error |
65 | - uri := interpolateURL( |
66 | - "http://___.blob.core.windows.net/___?restype=container&comp=list", |
67 | - context.Account, container) |
68 | + uri := context.getContainerURL(container) |
69 | + uri = addURLQueryParams( |
70 | + uri, |
71 | + "restype", "container", |
72 | + "comp", "list") |
73 | if marker != "" { |
74 | - // Add the marker argument. Do it after interpolation, in case the |
75 | - // marker string might contain the interpolation's placeholder |
76 | - // sequence. |
77 | - uri, err = addURLQueryParam(uri, "marker", marker) |
78 | - if err != nil { |
79 | - return nil, fmt.Errorf("malformed storage account URL '%s': %v", uri, err) |
80 | - } |
81 | + uri = addURLQueryParams(uri, "marker", marker) |
82 | } |
83 | req, err := http.NewRequest("GET", uri, nil) |
84 | if err != nil { |
85 | @@ -408,9 +402,8 @@ |
86 | // Send a request to the storage service to create a new container. If the |
87 | // request fails, error is non-nil. |
88 | func (context *StorageContext) CreateContainer(container string) error { |
89 | - uri := interpolateURL( |
90 | - "http://___.blob.core.windows.net/___?restype=container", |
91 | - context.Account, container) |
92 | + uri := context.getContainerURL(container) |
93 | + uri = addURLQueryParams(uri, "restype", "container") |
94 | req, err := http.NewRequest("PUT", uri, nil) |
95 | if err != nil { |
96 | return err |
97 | @@ -427,9 +420,7 @@ |
98 | // do the uploading, it just makes an empty file. |
99 | func (context *StorageContext) PutBlob(container, filename string) error { |
100 | // TODO: Add blobtype header. |
101 | - uri := interpolateURL( |
102 | - "http://___.blob.core.windows.net/___/___", |
103 | - context.Account, container, filename) |
104 | + uri := context.getFileURL(container, filename) |
105 | req, err := http.NewRequest("PUT", uri, nil) |
106 | if err != nil { |
107 | return err |
108 | @@ -445,9 +436,12 @@ |
109 | // Send a request to fetch the list of blocks that have been uploaded as part |
110 | // of a block blob. |
111 | func (context *StorageContext) GetBlockList(container, filename string) (*GetBlockList, error) { |
112 | - uri := interpolateURL( |
113 | - "http://___.blob.core.windows.net/___/___?comp=blocklist&blocklisttype=all", |
114 | - context.Account, container, filename) |
115 | + uri := context.getFileURL(container, filename) |
116 | + uri = addURLQueryParams( |
117 | + uri, |
118 | + "comp", "blocklist", |
119 | + "blocklisttype", "all") |
120 | + |
121 | req, err := http.NewRequest("GET", uri, nil) |
122 | if err != nil { |
123 | return nil, err |
124 | @@ -466,9 +460,11 @@ |
125 | // data block to upload. |
126 | func (context *StorageContext) PutBlock(container, filename, id string, data io.Reader) error { |
127 | base64_id := base64.StdEncoding.EncodeToString([]byte(id)) |
128 | - uri := interpolateURL( |
129 | - "http://___.blob.core.windows.net/___/___?comp=block&blockid=___", |
130 | - context.Account, container, filename, base64_id) |
131 | + uri := context.getFileURL(container, filename) |
132 | + uri = addURLQueryParams( |
133 | + uri, |
134 | + "comp", "block", |
135 | + "blockid", base64_id) |
136 | req, err := http.NewRequest("PUT", uri, data) |
137 | if err != nil { |
138 | return err |
139 | @@ -486,9 +482,8 @@ |
140 | |
141 | // Send a request to piece together blocks into a list that specifies a blob. |
142 | func (context *StorageContext) PutBlockList(container, filename string, blocklist *BlockList) error { |
143 | - uri := interpolateURL( |
144 | - "http://___.blob.core.windows.net/___/___?comp=blocklist", |
145 | - context.Account, container, filename) |
146 | + uri := context.getFileURL(container, filename) |
147 | + uri = addURLQueryParams(uri, "comp", "blocklist") |
148 | data, err := blocklist.Serialize() |
149 | if err != nil { |
150 | return err |
151 | @@ -511,9 +506,7 @@ |
152 | |
153 | // Delete the specified blob from the given container. |
154 | func (context *StorageContext) DeleteBlob(container, filename string) error { |
155 | - uri := interpolateURL( |
156 | - "http://___.blob.core.windows.net/___/___", |
157 | - context.Account, container, filename) |
158 | + uri := context.getFileURL(container, filename) |
159 | req, err := http.NewRequest("DELETE", uri, nil) |
160 | if err != nil { |
161 | return err |
162 | @@ -532,9 +525,7 @@ |
163 | |
164 | // Get the specified blob from the given container. |
165 | func (context *StorageContext) GetBlob(container, filename string) (io.ReadCloser, error) { |
166 | - uri := interpolateURL( |
167 | - "http://___.blob.core.windows.net/___/___", |
168 | - context.Account, container, filename) |
169 | + uri := context.getFileURL(container, filename) |
170 | req, err := http.NewRequest("GET", uri, nil) |
171 | if err != nil { |
172 | return nil, err |
173 | |
174 | === modified file 'storage_base_test.go' |
175 | --- storage_base_test.go 2013-04-29 08:06:24 +0000 |
176 | +++ storage_base_test.go 2013-04-30 09:36:29 +0000 |
177 | @@ -246,6 +246,42 @@ |
178 | |
179 | var _ = Suite(&TestStorageContext{}) |
180 | |
181 | +// makeNastyURLUnfriendlyString returns a string that really needs escaping |
182 | +// before it can be included in a URL. |
183 | +func makeNastyURLUnfriendlyString() string { |
184 | + return MakeRandomString(3) + "?&" + MakeRandomString(3) + "$%" |
185 | +} |
186 | + |
187 | +func (suite *TestStorageContext) TestGetAccountURL(c *C) { |
188 | + account := makeNastyURLUnfriendlyString() |
189 | + context := StorageContext{Account: account} |
190 | + c.Check( |
191 | + context.getAccountURL(), |
192 | + Equals, |
193 | + "http://"+url.QueryEscape(account)+".blob.core.windows.net/") |
194 | +} |
195 | + |
196 | +func (suite *TestStorageContext) TestGetContainerURL(c *C) { |
197 | + account := makeNastyURLUnfriendlyString() |
198 | + container := makeNastyURLUnfriendlyString() |
199 | + context := StorageContext{Account: account} |
200 | + c.Check( |
201 | + context.getContainerURL(container), |
202 | + Equals, |
203 | + "http://"+url.QueryEscape(account)+".blob.core.windows.net/"+url.QueryEscape(container)) |
204 | +} |
205 | + |
206 | +func (suite *TestStorageContext) TestGetFileURL(c *C) { |
207 | + account := makeNastyURLUnfriendlyString() |
208 | + container := makeNastyURLUnfriendlyString() |
209 | + file := makeNastyURLUnfriendlyString() |
210 | + context := StorageContext{Account: account} |
211 | + c.Check( |
212 | + context.getFileURL(container, file), |
213 | + Equals, |
214 | + "http://"+url.QueryEscape(account)+".blob.core.windows.net/"+url.QueryEscape(container)+"/"+url.QueryEscape(file)) |
215 | +} |
216 | + |
217 | func (suite *TestStorageContext) TestGetClientReturnsDefaultClient(c *C) { |
218 | context := &StorageContext{client: nil} |
219 | c.Assert(context.getClient(), Equals, http.DefaultClient) |
220 | @@ -266,48 +302,6 @@ |
221 | } |
222 | } |
223 | |
224 | -type TestAddURLQueryParam struct{} |
225 | - |
226 | -var _ = Suite(&TestAddURLQueryParam{}) |
227 | - |
228 | -func (*TestAddURLQueryParam) TestAddURLQueryParamUsesBaseURL(c *C) { |
229 | - baseURL := "http://example.com" |
230 | - |
231 | - extendedURL, err := addURLQueryParam(baseURL, "key", "value") |
232 | - c.Assert(err, IsNil) |
233 | - |
234 | - parsedURL, err := url.Parse(extendedURL) |
235 | - c.Assert(err, IsNil) |
236 | - c.Check(parsedURL.Scheme, Equals, "http") |
237 | - c.Check(parsedURL.Host, Equals, "example.com") |
238 | -} |
239 | - |
240 | -func (suite *TestAddURLQueryParam) TestEscapesParam(c *C) { |
241 | - key := "key&key" |
242 | - value := "value%value" |
243 | - |
244 | - uri, err := addURLQueryParam("http://example.com", key, value) |
245 | - c.Assert(err, IsNil) |
246 | - |
247 | - parsedURL, err := url.Parse(uri) |
248 | - c.Assert(err, IsNil) |
249 | - c.Check(parsedURL.Query()[key], DeepEquals, []string{value}) |
250 | -} |
251 | - |
252 | -// The parameter may safely contain the placeholder that we happen to use |
253 | -// for URL interpolation. |
254 | -func (suite *TestAddURLQueryParam) TestDoesNotInterpolateParam(c *C) { |
255 | - key := "key" + interpolationPlaceholder + "key" |
256 | - value := "value" + interpolationPlaceholder + "value" |
257 | - |
258 | - uri, err := addURLQueryParam("http://example.com", key, value) |
259 | - c.Assert(err, IsNil) |
260 | - |
261 | - parsedURL, err := url.Parse(uri) |
262 | - c.Assert(err, IsNil) |
263 | - c.Check(parsedURL.Query()[key], DeepEquals, []string{value}) |
264 | -} |
265 | - |
266 | type TestListContainers struct{} |
267 | |
268 | var _ = Suite(&TestListContainers{}) |
269 | @@ -479,27 +473,6 @@ |
270 | c.Check(values["marker"], DeepEquals, []string{"x&y"}) |
271 | } |
272 | |
273 | -// The marker string may safely contain the placeholder that we happen to use |
274 | -// for URL interpolation. |
275 | -func (suite *TestListContainers) TestGetListContainersBatchDoesNotInterpolateMarker(c *C) { |
276 | - transport := TestTransport2{} |
277 | - transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: Empty}, nil) |
278 | - context := makeStorageContext() |
279 | - context.client = &http.Client{Transport: &transport} |
280 | - marker := "x" + interpolationPlaceholder + "y" |
281 | - |
282 | - // An error about the http response is fine. What matters is that (1) we |
283 | - // don't fail while putting together the URL, and (2) we get the right URL. |
284 | - _, err := context.getListContainersBatch(marker) |
285 | - c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") |
286 | - c.Assert(transport.ExchangeCount, Equals, 1) |
287 | - |
288 | - query := transport.Exchanges[0].Request.URL.RawQuery |
289 | - values, err := url.ParseQuery(query) |
290 | - c.Assert(err, IsNil) |
291 | - c.Check(values["marker"], DeepEquals, []string{marker}) |
292 | -} |
293 | - |
294 | type TestListBlobs struct{} |
295 | |
296 | var _ = Suite(&TestListBlobs{}) |
297 | @@ -561,9 +534,11 @@ |
298 | context.client = &http.Client{Transport: transport} |
299 | results, err := context.ListBlobs("container") |
300 | c.Assert(err, IsNil) |
301 | - c.Check(transport.Request.URL.String(), Equals, fmt.Sprintf( |
302 | - "http://%s.blob.core.windows.net/container?restype=container&comp=list", |
303 | - context.Account)) |
304 | + c.Check(transport.Request.URL.String(), Matches, context.getContainerURL("container")+"?.*") |
305 | + c.Check(transport.Request.URL.Query(), DeepEquals, url.Values{ |
306 | + "restype": {"container"}, |
307 | + "comp": {"list"}, |
308 | + }) |
309 | c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "") |
310 | c.Assert(results, NotNil) |
311 | } |
312 | @@ -746,9 +721,7 @@ |
313 | context.client = &http.Client{Transport: transport} |
314 | err := context.PutBlob("container", "blobname") |
315 | c.Assert(err, IsNil) |
316 | - c.Check(transport.Request.URL.String(), Equals, fmt.Sprintf( |
317 | - "http://%s.blob.core.windows.net/container/blobname", |
318 | - context.Account)) |
319 | + c.Check(transport.Request.URL.String(), Equals, context.getFileURL("container", "blobname")) |
320 | c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "") |
321 | } |
322 | |
323 | @@ -799,10 +772,11 @@ |
324 | |
325 | // The blockid should have been base64 encoded and url escaped. |
326 | base64_id := base64.StdEncoding.EncodeToString([]byte(blockid)) |
327 | - escaped_id := url.QueryEscape(base64_id) |
328 | - c.Check(transport.Request.URL.String(), Equals, fmt.Sprintf( |
329 | - "http://%s.blob.core.windows.net/container/blobname?comp=block&blockid=%s", |
330 | - context.Account, escaped_id)) |
331 | + c.Check(transport.Request.URL.String(), Matches, context.getFileURL("container", "blobname")+"?.*") |
332 | + c.Check(transport.Request.URL.Query(), DeepEquals, url.Values{ |
333 | + "comp": {"block"}, |
334 | + "blockid": {base64_id}, |
335 | + }) |
336 | c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "") |
337 | |
338 | data, err := ioutil.ReadAll(transport.Request.Body) |
339 | @@ -936,9 +910,11 @@ |
340 | context.client = &http.Client{Transport: transport} |
341 | results, err := context.GetBlockList("container", "myfilename") |
342 | c.Assert(err, IsNil) |
343 | - c.Check(transport.Request.URL.String(), Equals, fmt.Sprintf( |
344 | - "http://%s.blob.core.windows.net/container/myfilename?comp=blocklist&blocklisttype=all", |
345 | - context.Account)) |
346 | + c.Check(transport.Request.URL.String(), Matches, context.getFileURL("container", "myfilename")+"?.*") |
347 | + c.Check(transport.Request.URL.Query(), DeepEquals, url.Values{ |
348 | + "comp": {"blocklist"}, |
349 | + "blocklisttype": {"all"}, |
350 | + }) |
351 | c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "") |
352 | c.Assert(results, NotNil) |
353 | } |
354 | @@ -982,9 +958,7 @@ |
355 | c.Assert(err, IsNil) |
356 | |
357 | c.Check(transport.Request.Method, Equals, "DELETE") |
358 | - c.Check(transport.Request.URL.String(), Equals, fmt.Sprintf( |
359 | - "http://%s.blob.core.windows.net/container/blobname", |
360 | - context.Account)) |
361 | + c.Check(transport.Request.URL.String(), Equals, context.getFileURL("container", "blobname")) |
362 | c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "") |
363 | c.Check(transport.Request.Body, IsNil) |
364 | } |
365 | @@ -1036,9 +1010,7 @@ |
366 | defer reader.Close() |
367 | |
368 | c.Check(transport.Request.Method, Equals, "GET") |
369 | - c.Check(transport.Request.URL.String(), Equals, fmt.Sprintf( |
370 | - "http://%s.blob.core.windows.net/container/blobname", |
371 | - context.Account)) |
372 | + c.Check(transport.Request.URL.String(), Equals, context.getFileURL("container", "blobname")) |
373 | c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "") |
374 | |
375 | data, err := ioutil.ReadAll(reader) |
376 | |
377 | === modified file 'storage_test.go' |
378 | --- storage_test.go 2013-04-24 10:08:54 +0000 |
379 | +++ storage_test.go 2013-04-30 09:36:29 +0000 |
380 | @@ -67,10 +67,11 @@ |
381 | |
382 | func assertBlockSent( |
383 | c *C, context *StorageContext, data []byte, blockID string, exchange *TestTransport2Exchange) { |
384 | - c.Check(exchange.Request.URL.String(), Equals, fmt.Sprintf( |
385 | - "http://%s.blob.core.windows.net/MyContainer/MyFile"+ |
386 | - "?comp=block&blockid=%s", |
387 | - context.Account, url.QueryEscape(blockID))) |
388 | + c.Check(exchange.Request.URL.String(), Matches, context.getFileURL("MyContainer", "MyFile")+"?.*") |
389 | + c.Check(exchange.Request.URL.Query(), DeepEquals, url.Values{ |
390 | + "comp": {"block"}, |
391 | + "blockid": {blockID}, |
392 | + }) |
393 | body, err := ioutil.ReadAll(exchange.Request.Body) |
394 | c.Check(err, IsNil) |
395 | c.Check(body, DeepEquals, data) |
396 | |
397 | === modified file 'utils.go' |
398 | --- utils.go 2013-04-26 03:53:37 +0000 |
399 | +++ utils.go 2013-04-30 09:36:29 +0000 |
400 | @@ -6,7 +6,6 @@ |
401 | import ( |
402 | "fmt" |
403 | "net/url" |
404 | - "strings" |
405 | ) |
406 | |
407 | // checkPathComponents checks that none of the passed components contains any |
408 | @@ -23,23 +22,25 @@ |
409 | } |
410 | } |
411 | |
412 | -// We use ___ (three underscores) as a placeholder for variables that are to be |
413 | -// interpolated into a URL. |
414 | -const interpolationPlaceholder = "___" |
415 | - |
416 | -// interpolateURL replaces occurrences of the interpolation placeolder with the |
417 | -// components specified. Additionally, the components are percent-encoded |
418 | -// (using net.url.QueryEscape) before being interpolated. |
419 | -func interpolateURL(urlstring string, components ...string) string { |
420 | - components_count := len(components) |
421 | - replacement_count := strings.Count(urlstring, interpolationPlaceholder) |
422 | - if components_count != replacement_count { |
423 | - panic(fmt.Errorf("%d slot(s) but given %d replacement(s)", |
424 | - replacement_count, components_count)) |
425 | - } |
426 | - for _, component := range components { |
427 | - component = url.QueryEscape(component) |
428 | - urlstring = strings.Replace(urlstring, interpolationPlaceholder, component, 1) |
429 | - } |
430 | - return urlstring |
431 | +// addURLQueryParams adds query parameters to a URL (and escapes as needed). |
432 | +// Parameters are URL, [key, value, [key, value, [...]]]. |
433 | +// The original URL must be correct, i.e. it should parse without error. |
434 | +func addURLQueryParams(originalURL string, params ...string) string { |
435 | + if len(params)%2 != 0 { |
436 | + panic(fmt.Errorf("got %d parameter argument(s), instead of matched key/value pairs", len(params))) |
437 | + } |
438 | + parsedURL, err := url.Parse(originalURL) |
439 | + if err != nil { |
440 | + panic(err) |
441 | + } |
442 | + query := parsedURL.Query() |
443 | + |
444 | + for index := 0; index < len(params); index += 2 { |
445 | + key := params[index] |
446 | + value := params[index+1] |
447 | + query.Add(key, value) |
448 | + } |
449 | + |
450 | + parsedURL.RawQuery = query.Encode() |
451 | + return parsedURL.String() |
452 | } |
453 | |
454 | === modified file 'utils_test.go' |
455 | --- utils_test.go 2013-04-12 15:03:26 +0000 |
456 | +++ utils_test.go 2013-04-30 09:36:29 +0000 |
457 | @@ -5,7 +5,7 @@ |
458 | |
459 | import ( |
460 | . "launchpad.net/gocheck" |
461 | - "regexp" |
462 | + "net/url" |
463 | ) |
464 | |
465 | type UtilsSuite struct{} |
466 | @@ -25,29 +25,63 @@ |
467 | PanicMatches, "'[.][.]' is not allowed") |
468 | } |
469 | |
470 | -func (suite *UtilsSuite) TestInterpolateURL(c *C) { |
471 | - // Interpolation with nothing is fine. |
472 | - c.Check( |
473 | - interpolateURL("http://example.com"), |
474 | - Equals, "http://example.com") |
475 | - // Interpolation with left-over arguments causes a panic. |
476 | - c.Check( |
477 | - func() { interpolateURL("http://example.com", "fred") }, PanicMatches, |
478 | - regexp.QuoteMeta("0 slot(s) but given 1 replacement(s)")) |
479 | - // Three underscores denotes where replacement should happen. |
480 | - c.Check( |
481 | - interpolateURL("http://___.example.com", "fred"), |
482 | - Equals, "http://fred.example.com") |
483 | - // Multiple replacements are fine. |
484 | - c.Check( |
485 | - interpolateURL("http://___.example.com/___", "fred", "bob"), |
486 | - Equals, "http://fred.example.com/bob") |
487 | - // If there are too few replacements it panics. |
488 | - c.Check( |
489 | - func() { interpolateURL("http://example.com/___") }, PanicMatches, |
490 | - regexp.QuoteMeta("1 slot(s) but given 0 replacement(s)")) |
491 | - // Replacements are percent-encoded. |
492 | - c.Check( |
493 | - interpolateURL("http://example.com/___", "foo^bar/baz"), |
494 | - Equals, "http://example.com/foo%5Ebar%2Fbaz") |
495 | +type TestAddURLQueryParams struct{} |
496 | + |
497 | +var _ = Suite(&TestAddURLQueryParams{}) |
498 | + |
499 | +func (*TestAddURLQueryParams) TestUsesBaseURL(c *C) { |
500 | + baseURL := "http://example.com" |
501 | + |
502 | + extendedURL := addURLQueryParams(baseURL, "key", "value") |
503 | + |
504 | + parsedURL, err := url.Parse(extendedURL) |
505 | + c.Assert(err, IsNil) |
506 | + c.Check(parsedURL.Scheme, Equals, "http") |
507 | + c.Check(parsedURL.Host, Equals, "example.com") |
508 | +} |
509 | + |
510 | +func (suite *TestAddURLQueryParams) TestEscapesParams(c *C) { |
511 | + key := "key&key" |
512 | + value := "value%value" |
513 | + |
514 | + uri := addURLQueryParams("http://example.com", key, value) |
515 | + |
516 | + parsedURL, err := url.Parse(uri) |
517 | + c.Assert(err, IsNil) |
518 | + c.Check(parsedURL.Query()[key], DeepEquals, []string{value}) |
519 | +} |
520 | + |
521 | +func (suite *TestAddURLQueryParams) TestAddsToExistingParams(c *C) { |
522 | + uri := addURLQueryParams("http://example.com?a=one", "b", "two") |
523 | + |
524 | + parsedURL, err := url.Parse(uri) |
525 | + c.Assert(err, IsNil) |
526 | + c.Check(parsedURL.Query(), DeepEquals, url.Values{ |
527 | + "a": {"one"}, |
528 | + "b": {"two"}, |
529 | + }) |
530 | +} |
531 | + |
532 | +func (suite *TestAddURLQueryParams) TestAppendsRepeatedParams(c *C) { |
533 | + uri := addURLQueryParams("http://example.com?foo=bar", "foo", "bar") |
534 | + c.Check(uri, Equals, "http://example.com?foo=bar&foo=bar") |
535 | +} |
536 | + |
537 | +func (suite *TestAddURLQueryParams) TestAddsMultipleParams(c *C) { |
538 | + uri := addURLQueryParams("http://example.com", "one", "un", "two", "deux") |
539 | + parsedURL, err := url.Parse(uri) |
540 | + c.Assert(err, IsNil) |
541 | + c.Check(parsedURL.Query(), DeepEquals, url.Values{ |
542 | + "one": {"un"}, |
543 | + "two": {"deux"}, |
544 | + }) |
545 | +} |
546 | + |
547 | +func (suite *TestAddURLQueryParams) TestRejectsOddNumberOfParams(c *C) { |
548 | + defer func() { |
549 | + err := recover() |
550 | + c.Check(err, ErrorMatches, ".*got 1 parameter.*") |
551 | + }() |
552 | + addURLQueryParams("http://example.com", "key") |
553 | + c.Assert("This should have panicked", Equals, "But it didn't.") |
554 | } |
555 | |
556 | === modified file 'x509session.go' |
557 | --- x509session.go 2013-04-12 09:26:52 +0000 |
558 | +++ x509session.go 2013-04-30 09:36:29 +0000 |
559 | @@ -34,14 +34,12 @@ |
560 | if err != nil { |
561 | panic(err) |
562 | } |
563 | - path = fmt.Sprintf("___/%s", path) |
564 | - path_url, err := url.Parse(path) |
565 | + escapedID := url.QueryEscape(session.subscriptionId) |
566 | + path_url, err := url.Parse(escapedID + "/" + path) |
567 | if err != nil { |
568 | panic(err) |
569 | } |
570 | - return interpolateURL( |
571 | - azure_url.ResolveReference(path_url).String(), |
572 | - session.subscriptionId) |
573 | + return azure_url.ResolveReference(path_url).String() |
574 | } |
575 | |
576 | // _X509Dispatcher is the function used to dispatch requests. We call the |
I think addURLQueryParams() is good.
However, I don't think get{Account, Container, File}URL and the
resulting changes to storage_base.go are very useful. Despite the lack
of precedent, the syntax used with interpolateURL() is quite obvious
and readable, though I am surely biased. There's more code now, it
obfuscates what's happening, and I suspect YAGNI too.
I think most of the benefit of this branch could be obtained with:
const STORAGE_ URL_TEMPLATE = "http:// ___.blob. core.windows. net"
s/"http:// ___.blob. core.windows. net"/STORAGE_ URL_TEMPLATE + "/g
I'm torn. On the one hand I could well be biased because I introduced
interpolateURL(). On the other hand, I have little love for any Go
code, mine or otherwise. I vote approve, but please don't land this
unless you're really sure that it improves gwacl, regardless of any
other factor.