amazon_sdb 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ module Amazon
2
+ module SDB
3
+ class InvalidParameterError < ArgumentError
4
+ end
5
+
6
+ class LimitError < Exception
7
+ end
8
+
9
+ class DomainLimitError < LimitError
10
+ end
11
+
12
+ class UnknownError < Exception
13
+ end
14
+
15
+ class RecordNotFoundError < Exception
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ module Amazon
2
+ module SDB
3
+ ##
4
+ # An item from sdb. This basically is a key for the item in the domain and a Multimap of the attributes. You should never
5
+ # call Item#new, instead it is returned by various methods in Domain and ResultSet
6
+ class Item
7
+ include Enumerable
8
+ attr_accessor :key, :attributes
9
+
10
+ def initialize(domain, key, multimap=nil)
11
+ @domain = domain
12
+ @key = key
13
+ @attributes = multimap
14
+ end
15
+
16
+ def reload!
17
+ item = @domain.get_attributes(@key)
18
+ @attributes = item.attributes
19
+ end
20
+
21
+ def empty?
22
+ @attributes.size == 0
23
+ end
24
+
25
+ def destroy!
26
+ @domain.delete_attributes(@key)
27
+ end
28
+
29
+ def save
30
+ @domain.put_attributes(@key, @attributes)
31
+ end
32
+
33
+ def get(key)
34
+ reload! if @attributes.nil?
35
+ @attributes.get(key)
36
+ end
37
+
38
+ def [](key)
39
+ get(key)
40
+ end
41
+
42
+ def each
43
+ @attributes.each
44
+ end
45
+
46
+ def each_pair
47
+ @attributes.each_pair
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,267 @@
1
+ module Amazon
2
+ module SDB
3
+ ##
4
+ # A multimap is like a hash or set, but it only requires that key/value pair is unique (the same key may have multiple values).
5
+ # Multimaps may be created by the user to send into Amazon sdb or they may be read back from sdb as the attributes for an object.
6
+ #
7
+ # For your convenience, multimap's initializer can take several types of input parameters:
8
+ # - A hash of key/value pairs (for when you want keys to be unique)
9
+ # - An array of key/value pairs represented as 2-member arrays
10
+ # - Another multimap
11
+ # - Or nothing at all (an empty set)
12
+ class Multimap
13
+ include Enumerable
14
+
15
+ ##
16
+ # To be honest, floats are difficult for sdb. In order to work with lexical comparisons, you need to save floats as strings padded to the same size.
17
+ # The problem is, automatic conversion can run afoul of rounding errors if it has a larger precision than the original float,
18
+ # so for the short term I've provided the numeric helper method for saving floats as strings into the multimap (when read back from
19
+ # sdb they will still be converted from floats). To use, specify the precision you want to represent as well as the total size (pick something large like 32 to be safe)
20
+ def self.numeric(float, size, precision)
21
+ sprintf "%0#{size}.#{precision}f", float
22
+ end
23
+
24
+ def initialize(init=nil)
25
+ @mset = {}
26
+
27
+ clear_size!
28
+
29
+ if init.nil?
30
+ # do nothing
31
+ elsif init.is_a? Hash
32
+ init.each {|k, v| put(k, v) }
33
+ elsif init.is_a? Array
34
+ # load from array
35
+ if init.any? {|v| ! v.is_a? Array || v.size != 2 }
36
+ raise ArgumentError, "Array must be of key/value pairs only"
37
+ end
38
+
39
+ init.each {|v| self.put(v[0], v[1])}
40
+ elsif init.is_a? Multimap
41
+ @mset = init.mset.dup
42
+ else
43
+ raise ArgumentError, "Wrong type passed as initializer"
44
+ end
45
+ end
46
+
47
+ def clear_size!
48
+ @size = nil
49
+ end
50
+
51
+ ##
52
+ # Returns the size of the multimap. This is the total number of key/value pairs in it.
53
+ def size
54
+ if @size.nil?
55
+ @size = @mset.inject(0) do |total, pair|
56
+ value = pair[1]
57
+ if value.is_a? Array
58
+ total + value.size
59
+ else
60
+ total + 1
61
+ end
62
+ end
63
+ end
64
+
65
+ @size
66
+ end
67
+
68
+ ##
69
+ # Save a key/value attribute into the multimap. Takes additional options
70
+ # - <tt>:replace => true</tt> remove any attributes with the same key before insert (otherwise, appends)
71
+ def put(key_arg, value, options = {})
72
+ key = key_arg.to_s
73
+
74
+ if options[:before_cast]
75
+ key = "#{key}_before_cast"
76
+ end
77
+
78
+ k = @mset[key]
79
+ clear_size!
80
+
81
+ if k.nil? || options[:replace]
82
+ @mset[key] = value
83
+ else
84
+ if @mset[key].is_a? Array
85
+ @mset[key] << value
86
+ else
87
+ @mset[key] = [@mset[key], value]
88
+ end
89
+ end
90
+ end
91
+
92
+ ##
93
+ # Returns all the values that match a key. Normally, if there is only a single value entry
94
+ # returns just the value, with an array for multiple values, and nil for no match. If you want
95
+ # to always return an array, pass in <tt>:force_array => true</tt> in the options
96
+ def get(key_arg, options = {})
97
+ key = key_arg.to_s
98
+
99
+ if options[:before_cast]
100
+ key = "#{key}_before_cast"
101
+ end
102
+
103
+ k = @mset[key]
104
+
105
+ if options[:force_array]
106
+ return [] if k.nil?
107
+ k.to_a
108
+ else
109
+ k
110
+ end
111
+ end
112
+
113
+ ##
114
+ # Shortcut for #get
115
+ def [](key)
116
+ get(key)
117
+ end
118
+
119
+ ##
120
+ # Shortcut for put(key, value, :replace => true)
121
+ def []=(key, value)
122
+ put(key, value, :replace => true)
123
+ end
124
+
125
+ ##
126
+ # Support for Enumerable. Yields each key/value pair as an array of 2 members.
127
+ def each
128
+ @mset.each_pair do |key, group|
129
+ group.to_a.each do |value|
130
+ yield [key, value]
131
+ end
132
+ end
133
+ end
134
+
135
+ ##
136
+ # Yields each key/value pair as separate parameters to the block.
137
+ def each_pair
138
+ @mset.each_pair do |key, group|
139
+ case group
140
+ when Array
141
+ group.each do |value|
142
+ yield key, value
143
+ end
144
+ else
145
+ yield key, group
146
+ end
147
+ end
148
+ end
149
+
150
+ ##
151
+ # Yields each pair as separate key/value plus an index.
152
+ def each_pair_with_index
153
+ index = 0
154
+ self.each_pair do |key, value|
155
+ yield key, value, index
156
+ index += 1
157
+ end
158
+ end
159
+
160
+ def string_escape(str)
161
+ str.gsub('\\') {|c| "#{c}#{c}"}.gsub('\'') {|c| "\\#{c}"}
162
+ end
163
+
164
+ def sdb_key_escape(key)
165
+ case key
166
+ when String
167
+ string_escape(key)
168
+ else
169
+ key.to_s
170
+ end
171
+ end
172
+
173
+ def sdb_value_escape(value)
174
+ case value
175
+ when Fixnum
176
+ sprintf("%0#{Base.number_padding}d", value)
177
+ when Float
178
+ numeric(value, Base.number_padding, Base.float_precision)
179
+ when String
180
+ string_escape(value)
181
+ when Time
182
+ value.iso8601
183
+ else
184
+ string_escape(value.to_s)
185
+ end
186
+ end
187
+
188
+ ##
189
+ # Outputs a multimap to sdb using Amazon's query-string notation (and doing auto-conversions of int and date values)
190
+ def to_sdb(options={})
191
+ out = {}
192
+
193
+ self.each_pair_with_index do |key, value, index|
194
+ out["Attribute.#{index}.Name"] = sdb_key_escape(key)
195
+ out["Attribute.#{index}.Value"] = sdb_value_escape(value)
196
+
197
+ if options.key?(:replace) && (options[:replace] == :all || [*options[:replace]].find {|x| x.to_s == key })
198
+ out["Attribute.#{index}.Replace"] = "true"
199
+ end
200
+ end
201
+
202
+ out
203
+ end
204
+
205
+ def coerce(value)
206
+ case value
207
+ when /^0+\d+$/
208
+ value.to_i
209
+ when /^0+\d*.\d+$/
210
+ value.to_f
211
+ when /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}([+\-]\d{2}:\d{2})?)?$/
212
+ Time.parse(value)
213
+ else
214
+ value
215
+ end
216
+ end
217
+
218
+ def from_sdb(values)
219
+ @mset = {}
220
+ clear_size!
221
+
222
+ if values.nil?
223
+ # do nothing
224
+ elsif values.is_a? Array
225
+ # load from array
226
+ if values.any? {|v| ! v.is_a? Array || v.size != 2 }
227
+ raise ArgumentError, "Array must be of key/value pairs only"
228
+ end
229
+
230
+ values.each do |v|
231
+ self.put(v[0], v[1], :before_cast => true)
232
+ self.put(v[0], coerce(v[1]))
233
+ end
234
+ else
235
+ raise ArgumentError, "Wrong type passed as initializer"
236
+ end
237
+ end
238
+
239
+ ##
240
+ # Returns the multimap as an array of 2-item arrays, one for each key-value pair
241
+ def to_a
242
+ out = []
243
+ each_pair {|k, v| out << [k, v] }
244
+ out
245
+ end
246
+
247
+ ##
248
+ # Returns the multimap as a hash. In cases where there are multiple values for a key, it puts all the values into an array.
249
+ def to_h
250
+ @mset.dup
251
+ end
252
+
253
+ def method_missing(method_symbol, *args)
254
+ name = method_symbol.to_s
255
+ if name =~ /^\w+$/
256
+ if @mset.key? name
257
+ get(name)
258
+ else
259
+ super
260
+ end
261
+ else
262
+ super
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,45 @@
1
+ module Amazon
2
+ module SDB
3
+
4
+ ##
5
+ # Represents a ResultSet returned from Domain#query. Currently, this is just
6
+ # a set of Items plus an operation to see if there is another set to be retrieved
7
+ # and to load it on demand. When Amazon sees fit to add total results or other metadata
8
+ # for queries that will also be included here.
9
+ class ResultSet
10
+ include Enumerable
11
+ attr_reader :items
12
+
13
+ def initialize(domain, items, more_token = nil)
14
+ @domain = domain
15
+ @items = items
16
+ @more_token = more_token
17
+ end
18
+
19
+ ##
20
+ # Returns true if there is another result set to be loaded
21
+ def more_items?
22
+ not @more_token.nil?
23
+ end
24
+
25
+ ##
26
+ # Not implemented yet
27
+ def load_next!
28
+ end
29
+
30
+ ##
31
+ # Iterator through all the keys in this resultset
32
+ def keys
33
+ @items.map {|i| i.key }
34
+ end
35
+
36
+ ##
37
+ # Support method for Enumerable. Iterates through the items in this set (NOT all the matching results for a query)
38
+ def each
39
+ @items.each do |i|
40
+ yield i
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,135 @@
1
+ require 'test_sdb_harness'
2
+
3
+ class TestAmazonBase < Test::Unit::TestCase
4
+ def setup
5
+ @sdb = Amazon::SDB::Base.new 'API_KEY', 'SECRET_KEY'
6
+ end
7
+
8
+ def test_sign
9
+ # this example is given by Amazon
10
+ options = {
11
+ "Timestamp" => '2004-02-12T15:19:21+00:00',
12
+ 'adc' => 1,
13
+ 'aab' => 2,
14
+ 'AWSAccessKeyId' => 'my_access_id',
15
+ 'SignatureVersion' => 1,
16
+ 'Action' => 'Get',
17
+ 'Version' => '2006-08-11'
18
+ }
19
+
20
+ signature = Amazon::SDB::Base.sign('secret_key', options)
21
+ assert_equal 'xlrD17jnkGk6E3nVVOV3Qon3Nwg=', signature
22
+ end
23
+
24
+ def test_cgi_encode
25
+ options = {'foo' => 'bar'}
26
+
27
+ assert_equal 'foo=bar', @sdb.cgi_encode(options)
28
+ end
29
+
30
+ def test_cgi_encode_array
31
+ options = {"foo" => ["bar", "baz"]}
32
+ assert_equal 'foo=bar&foo=baz', @sdb.cgi_encode(options)
33
+ end
34
+
35
+ def test_domains
36
+ @sdb.responses << <<-EOF
37
+ <?xml version="1.0" encoding="utf-8" ?>
38
+ <ListDomainsResponse xmlns="http://sdb.amazonaws.com/doc/2007-11-07">
39
+ <ListDomainsResult>
40
+ <DomainName>foo</DomainName>
41
+ <DomainName>bar</DomainName>
42
+ <DomainName>baz</DomainName>
43
+ </ListDomainsResult>
44
+ <ResponseMetadata>
45
+ <RequestId>eb13162f-1b95-4511-8b12-489b86acfd28</RequestId>
46
+ <BoxUsage>0.0000219907</BoxUsage>
47
+ </ResponseMetadata>
48
+ </ListDomainsResponse>
49
+ EOF
50
+
51
+ domains = @sdb.domains
52
+
53
+ assert_equal 1, @sdb.uris.length
54
+ assert_in_url_query({'Action' => 'ListDomains'}, @sdb.uris.first)
55
+
56
+ assert_equal 3, domains.size
57
+ %w(foo bar baz).each_with_index do |name, index|
58
+ assert_equal name, domains[index].name
59
+ end
60
+ end
61
+
62
+ def test_domains_more
63
+ @sdb.responses << <<-EOF
64
+ <?xml version="1.0" encoding="utf-8" ?>
65
+ <ListDomainsResponse xmlns="http://sdb.amazonaws.com/doc/2007-11-07">
66
+ <ListDomainsResult>
67
+ <DomainName>foo</DomainName>
68
+ <DomainName>bar</DomainName>
69
+ <NextToken>FOOBAR</NextToken>
70
+ </ListDomainsResult>
71
+ <ResponseMetadata>
72
+ <RequestId>eb13162f-1b95-4511-8b12-489b86acfd28</RequestId>
73
+ <BoxUsage>0.0000219907</BoxUsage>
74
+ </ResponseMetadata>
75
+ </ListDomainsResponse>
76
+ EOF
77
+
78
+ @sdb.responses << <<-EOF
79
+ <?xml version="1.0" encoding="utf-8" ?>
80
+ <ListDomainsResponse xmlns="http://sdb.amazonaws.com/doc/2007-11-07">
81
+ <ListDomainsResult>
82
+ <DomainName>baz</DomainName>
83
+ </ListDomainsResult>
84
+ <ResponseMetadata>
85
+ <RequestId>eb13162f-1b95-4511-8b12-489b86acfd28</RequestId>
86
+ <BoxUsage>0.0000219907</BoxUsage>
87
+ </ResponseMetadata>
88
+ </ListDomainsResponse>
89
+ EOF
90
+
91
+ domains = @sdb.domains
92
+
93
+ assert_equal 2, @sdb.uris.length, "Should make 2 requests to sdb"
94
+ assert_in_url_query({'NextToken' => 'FOOBAR'}, @sdb.uris.last)
95
+
96
+ assert_equal 3, domains.size, "Should return 3 domains"
97
+ %w(foo bar baz).each_with_index do |name, index|
98
+ assert_equal name, domains[index].name
99
+ end
100
+ end
101
+
102
+ def test_domains_fail
103
+ end
104
+
105
+ def test_create_domain
106
+ @sdb.responses << generic_response('CreateDomain')
107
+
108
+ domain = @sdb.create_domain('foobar')
109
+ assert_equal 1, @sdb.uris.length
110
+ assert_in_url_query({'Action' => 'CreateDomain', 'DomainName' => 'foobar'}, @sdb.uris.first)
111
+
112
+ assert_equal 'foobar', domain.name
113
+ end
114
+
115
+ def test_create_domain_invalid_param
116
+ @sdb.responses << error_response('InvalidParameterValue', 'Value (X) for parameter DomainName is invalid.')
117
+
118
+ assert_raise(Amazon::SDB::InvalidParameterError) { @sdb.create_domain('(X)') }
119
+ assert_equal 1, @sdb.uris.length
120
+ end
121
+
122
+ def test_create_domain_limit_error
123
+ @sdb.responses << error_response('NumberDomainsExceeded', 'Domain Limit reached')
124
+
125
+ assert_raise(Amazon::SDB::DomainLimitError) { @sdb.create_domain('foo') }
126
+ end
127
+
128
+ def test_delete_domain
129
+ @sdb.responses << generic_response('DeleteDomain')
130
+
131
+ @sdb.delete_domain!('foo')
132
+ assert_equal 1, @sdb.uris.length
133
+ assert_in_url_query({'Action' => 'DeleteDomain', 'DomainName' => 'foo'}, @sdb.uris.first)
134
+ end
135
+ end