summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/goamz/goamz/exp/sdb/sdb.go
blob: 0d13f4a9e1928d73bf336d9c1966e31f39780cb5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
//
// goamz - Go packages to interact with the Amazon Web Services.
//
//   https://wiki.ubuntu.com/goamz
//
// Copyright (c) 2011 AppsAttic Ltd.
//
// sdb package written by:
//
//      Andrew Chilton <chilts@appsattic.com>
//      Brad Rydzewski <brad.rydzewski@gmail.com>

// This package is in an experimental state, and does not currently
// follow conventions and style of the rest of goamz or common
// Go conventions. It must be polished before it's considered a
// first-class package in goamz.
package sdb

// BUG: SelectResp isn't properly organized. It must change.

//

import (
	"encoding/xml"
	"github.com/goamz/goamz/aws"
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strconv"
	"time"
)

const debug = false

// The SDB type encapsulates operations with a specific SimpleDB region.
type SDB struct {
	aws.Auth
	aws.Region
	private byte // Reserve the right of using private data.
}

// New creates a new SDB.
func New(auth aws.Auth, region aws.Region) *SDB {
	return &SDB{auth, region, 0}
}

// The Domain type represents a collection of items that are described
// by name-value attributes.
type Domain struct {
	*SDB
	Name string
}

// Domain returns a Domain with the given name.
func (sdb *SDB) Domain(name string) *Domain {
	return &Domain{sdb, name}
}

// The Item type represent individual objects that contain one or more
// name-value attributes stored within a SDB Domain as rows.
type Item struct {
	*SDB
	*Domain
	Name string
}

// Item returns an Item with the given name.
func (domain *Domain) Item(name string) *Item {
	return &Item{domain.SDB, domain, name}
}

// The Attr type represent categories of data that can be assigned to items.
type Attr struct {
	Name  string
	Value string
}

// ----------------------------------------------------------------------------
// Service-level operations.

// --- ListDomains

// Response to a ListDomains request.
//
// See http://goo.gl/3u0Cf for more details.
type ListDomainsResp struct {
	Domains          []string `xml:"ListDomainsResult>DomainName"`
	NextToken        string   `xml:"ListDomainsResult>NextToken"`
	ResponseMetadata ResponseMetadata
}

// ListDomains lists all domains in sdb.
//
// See http://goo.gl/Dsw15 for more details.
func (sdb *SDB) ListDomains() (resp *ListDomainsResp, err error) {
	return sdb.ListDomainsN(0, "")
}

// ListDomainsN lists domains in sdb up to maxDomains.
// If nextToken is not empty, domains listed will start at the given token.
//
// See http://goo.gl/Dsw15 for more details.
func (sdb *SDB) ListDomainsN(maxDomains int, nextToken string) (resp *ListDomainsResp, err error) {
	params := makeParams("ListDomains")
	if maxDomains != 0 {
		params["MaxNumberOfDomains"] = []string{strconv.Itoa(maxDomains)}
	}
	if nextToken != "" {
		params["NextToken"] = []string{nextToken}
	}
	resp = &ListDomainsResp{}
	err = sdb.query(nil, nil, params, nil, resp)
	return
}

// --- SelectExpression

// Response to a Select request.
//
// See http://goo.gl/GTsSZ for more details.
type SelectResp struct {
	Items []struct {
		Name  string
		Attrs []Attr `xml:"Attribute"`
	} `xml:"SelectResult>Item"`
	ResponseMetadata ResponseMetadata
}

// Select returns a set of items and attributes that match expr.
// Select is similar to the standard SQL SELECT statement.
//
// See http://goo.gl/GTsSZ for more details.
func (sdb *SDB) Select(expr string, consistent bool) (resp *SelectResp, err error) {
	resp = &SelectResp{}
	params := makeParams("Select")
	params["SelectExpression"] = []string{expr}
	if consistent {
		params["ConsistentRead"] = []string{"true"}
	}
	err = sdb.query(nil, nil, params, nil, resp)
	return
}

// ----------------------------------------------------------------------------
// Domain-level operations.

// --- CreateDomain

// CreateDomain creates a new domain.
//
// See http://goo.gl/jDjGH for more details.
func (domain *Domain) CreateDomain() (resp *SimpleResp, err error) {
	params := makeParams("CreateDomain")
	resp = &SimpleResp{}
	err = domain.SDB.query(domain, nil, params, nil, resp)
	return
}

// DeleteDomain deletes an existing domain.
//
// See http://goo.gl/S0dCL for more details.
func (domain *Domain) DeleteDomain() (resp *SimpleResp, err error) {
	params := makeParams("DeleteDomain")
	resp = &SimpleResp{}
	err = domain.SDB.query(domain, nil, params, nil, resp)
	return
}

// ----------------------------------------------------------------------------
// Item-level operations.

type PutAttrs struct {
	attrs    []Attr
	expected []Attr
	replace  map[string]bool
	missing  map[string]bool
}

func (pa *PutAttrs) Add(name, value string) {
	pa.attrs = append(pa.attrs, Attr{name, value})
}

func (pa *PutAttrs) Replace(name, value string) {
	pa.Add(name, value)
	if pa.replace == nil {
		pa.replace = make(map[string]bool)
	}
	pa.replace[name] = true
}

// The PutAttrs request will only succeed if the existing
// item in SimpleDB contains a matching name / value pair.
func (pa *PutAttrs) IfValue(name, value string) {
	pa.expected = append(pa.expected, Attr{name, value})
}

// Flag to test the existence of an attribute while performing
// conditional updates. X can be any positive integer or 0.
//
// This should set Expected.N.Name=name and Expected.N.Exists=false
func (pa *PutAttrs) IfMissing(name string) {
	if pa.missing == nil {
		pa.missing = make(map[string]bool)
	}
	pa.missing[name] = true
}

// PutAttrs adds attrs to item.
//
// See http://goo.gl/yTAV4 for more details.
func (item *Item) PutAttrs(attrs *PutAttrs) (resp *SimpleResp, err error) {
	params := makeParams("PutAttributes")
	resp = &SimpleResp{}

	// copy these attrs over to the parameters
	itemNum := 1
	for _, attr := range attrs.attrs {
		itemNumStr := strconv.Itoa(itemNum)

		// do the name, value and replace
		params["Attribute."+itemNumStr+".Name"] = []string{attr.Name}
		params["Attribute."+itemNumStr+".Value"] = []string{attr.Value}

		if _, ok := attrs.replace[attr.Name]; ok {
			params["Attribute."+itemNumStr+".Replace"] = []string{"true"}
		}

		itemNum++
	}

	//append expected values to params
	expectedNum := 1
	for _, attr := range attrs.expected {
		expectedNumStr := strconv.Itoa(expectedNum)
		params["Expected."+expectedNumStr+".Name"] = []string{attr.Name}
		params["Expected."+expectedNumStr+".Value"] = []string{attr.Value}

		if attrs.missing[attr.Name] {
			params["Expected."+expectedNumStr+".Exists"] = []string{"false"}
		}
		expectedNum++
	}

	err = item.query(params, nil, resp)
	if err != nil {
		return nil, err
	}
	return
}

// Response to an Attrs request.
//
// See http://goo.gl/45X1M for more details.
type AttrsResp struct {
	Attrs            []Attr `xml:"GetAttributesResult>Attribute"`
	ResponseMetadata ResponseMetadata
}

// Attrs returns one or more of the named attributes, or
// all of item's attributes if names is nil.
// If consistent is true, previous writes will necessarily
// be observed.
//
// See http://goo.gl/45X1M for more details.
func (item *Item) Attrs(names []string, consistent bool) (resp *AttrsResp, err error) {
	params := makeParams("GetAttributes")
	params["ItemName"] = []string{item.Name}
	if consistent {
		params["ConsistentRead"] = []string{"true"}
	}

	// Copy these attributes over to the parameters
	for i, name := range names {
		params["AttributeName."+strconv.Itoa(i+1)] = []string{name}
	}

	resp = &AttrsResp{}
	err = item.query(params, nil, resp)
	if err != nil {
		return nil, err
	}
	return
}

// ----------------------------------------------------------------------------
// Generic data structures for all requests/responses.

// Error encapsulates an error returned by SDB.
type Error struct {
	StatusCode int     // HTTP status code (200, 403, ...)
	StatusMsg  string  // HTTP status message ("Service Unavailable", "Bad Request", ...)
	Code       string  // SimpleDB error code ("InvalidParameterValue", ...)
	Message    string  // The human-oriented error message
	RequestId  string  // A unique ID for this request
	BoxUsage   float64 // The measure of machine utilization for this request.
}

func (err *Error) Error() string {
	return err.Message
}

// SimpleResp represents a response to an SDB request which on success
// will return no other information besides ResponseMetadata.
type SimpleResp struct {
	ResponseMetadata ResponseMetadata
}

// ResponseMetadata
type ResponseMetadata struct {
	RequestId string  // A unique ID for tracking the request
	BoxUsage  float64 // The measure of machine utilization for this request.
}

func buildError(r *http.Response) error {
	err := Error{}
	err.StatusCode = r.StatusCode
	err.StatusMsg = r.Status
	xml.NewDecoder(r.Body).Decode(&err)
	return &err
}

// ----------------------------------------------------------------------------
// Request dispatching logic.

func (item *Item) query(params url.Values, headers http.Header, resp interface{}) error {
	return item.Domain.SDB.query(item.Domain, item, params, headers, resp)
}

func (domain *Domain) query(item *Item, params url.Values, headers http.Header, resp interface{}) error {
	return domain.SDB.query(domain, item, params, headers, resp)
}

func (sdb *SDB) query(domain *Domain, item *Item, params url.Values, headers http.Header, resp interface{}) error {
	// all SimpleDB operations have path="/"
	method := "GET"
	path := "/"

	// if we have been given no headers or params, create them
	if headers == nil {
		headers = map[string][]string{}
	}
	if params == nil {
		params = map[string][]string{}
	}

	// setup some default parameters
	params["Version"] = []string{"2009-04-15"}
	params["Timestamp"] = []string{time.Now().UTC().Format(time.RFC3339)}

	// set the DomainName param (every request must have one)
	if domain != nil {
		params["DomainName"] = []string{domain.Name}
	}

	// set the ItemName if we have one
	if item != nil {
		params["ItemName"] = []string{item.Name}
	}

	// check the endpoint URL
	u, err := url.Parse(sdb.Region.SDBEndpoint)
	if err != nil {
		return err
	}
	headers["Host"] = []string{u.Host}
	sign(sdb.Auth, method, path, params, headers)

	u.Path = path
	if len(params) > 0 {
		u.RawQuery = params.Encode()
	}
	req := http.Request{
		URL:        u,
		Method:     method,
		ProtoMajor: 1,
		ProtoMinor: 1,
		Close:      true,
		Header:     headers,
	}

	if v, ok := headers["Content-Length"]; ok {
		req.ContentLength, _ = strconv.ParseInt(v[0], 10, 64)
		delete(headers, "Content-Length")
	}

	r, err := http.DefaultClient.Do(&req)
	if err != nil {
		return err
	}
	defer r.Body.Close()

	if debug {
		dump, _ := httputil.DumpResponse(r, true)
		log.Printf("response:\n")
		log.Printf("%v\n}\n", string(dump))
	}

	// status code is always 200 when successful (since we're always doing a GET)
	if r.StatusCode != 200 {
		return buildError(r)
	}

	// everything was fine, so unmarshal the XML and return what it's err is (if any)
	err = xml.NewDecoder(r.Body).Decode(resp)
	return err
}

func makeParams(action string) map[string][]string {
	params := make(map[string][]string)
	params["Action"] = []string{action}
	return params
}