amazon_sdb 0.5.5
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +11 -0
- data/Manifest.txt +15 -0
- data/README.txt +84 -0
- data/Rakefile +23 -0
- data/lib/amazon_sdb.rb +39 -0
- data/lib/amazon_sdb/base.rb +156 -0
- data/lib/amazon_sdb/domain.rb +145 -0
- data/lib/amazon_sdb/exceptions.rb +18 -0
- data/lib/amazon_sdb/item.rb +51 -0
- data/lib/amazon_sdb/multimap.rb +267 -0
- data/lib/amazon_sdb/resultset.rb +45 -0
- data/test/test_amazon_base.rb +135 -0
- data/test/test_amazon_domain.rb +208 -0
- data/test/test_multimap.rb +177 -0
- data/test/test_sdb_harness.rb +124 -0
- metadata +90 -0
@@ -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
|