bare-ruby-aws 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +340 -0
- data/INSTALL +260 -0
- data/NEWS +808 -0
- data/README +580 -0
- data/lib/amazon.rb +144 -0
- data/lib/amazon/aws.rb +963 -0
- data/lib/amazon/aws/cache.rb +141 -0
- data/lib/amazon/aws/search.rb +458 -0
- data/test/setup.rb +56 -0
- data/test/tc_amazon.rb +20 -0
- data/test/tc_aws.rb +160 -0
- data/test/tc_item_search.rb +105 -0
- data/test/tc_operation_request.rb +64 -0
- data/test/tc_serialisation.rb +107 -0
- data/test/ts_aws.rb +24 -0
- metadata +91 -0
data/lib/amazon.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
# $Id: amazon.rb,v 1.33 2010/03/19 17:20:46 ianmacd Exp $
|
2
|
+
#
|
3
|
+
|
4
|
+
module Amazon
|
5
|
+
|
6
|
+
# A top-level exception container class.
|
7
|
+
#
|
8
|
+
class AmazonError < StandardError; end
|
9
|
+
|
10
|
+
NAME = 'Ruby/Amazon'
|
11
|
+
|
12
|
+
@@config = {}
|
13
|
+
|
14
|
+
# We're going to have to use String#size if String#bytesize isn't available.
|
15
|
+
# This is for Ruby pre-1.8.7.
|
16
|
+
#
|
17
|
+
unless String.instance_methods.include? 'bytesize'
|
18
|
+
String.module_eval( 'alias :bytesize :size' )
|
19
|
+
end
|
20
|
+
|
21
|
+
# Prints debugging messages and works like printf, except that it prints
|
22
|
+
# only when Ruby is run with the -d switch.
|
23
|
+
#
|
24
|
+
def Amazon.dprintf(format='', *args)
|
25
|
+
$stderr.printf( format + "\n", *args ) if $DEBUG
|
26
|
+
end
|
27
|
+
|
28
|
+
# Encode a string, such that it is suitable for HTTP transmission.
|
29
|
+
#
|
30
|
+
def Amazon.url_encode(string)
|
31
|
+
|
32
|
+
# Shamelessly plagiarised from Wakou Aoyama's cgi.rb, but then altered
|
33
|
+
# slightly to please AWS.
|
34
|
+
#
|
35
|
+
string.gsub( /([^a-zA-Z0-9_.~-]+)/ ) do
|
36
|
+
'%' + $1.unpack( 'H2' * $1.bytesize ).join( '%' ).upcase
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Convert a string from CamelCase to ruby_case.
|
41
|
+
#
|
42
|
+
def Amazon.uncamelise(string)
|
43
|
+
# Avoid modifying by reference.
|
44
|
+
#
|
45
|
+
string = string.dup
|
46
|
+
|
47
|
+
# Don't mess with string if all caps.
|
48
|
+
#
|
49
|
+
if string =~ /[a-z]/
|
50
|
+
string.gsub!( /(.+?)(([A-Z][a-z]|[A-Z]+$))/, "\\1_\\2" )
|
51
|
+
end
|
52
|
+
|
53
|
+
# Convert to lower case.
|
54
|
+
#
|
55
|
+
string.downcase
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# A Class for dealing with configuration files, such as
|
60
|
+
# <tt>/etc/amazonrc</tt> and <tt>~/.amazonrc</tt>.
|
61
|
+
#
|
62
|
+
class Config < Hash
|
63
|
+
|
64
|
+
require 'stringio'
|
65
|
+
|
66
|
+
# Exception class for configuration file errors.
|
67
|
+
#
|
68
|
+
class ConfigError < AmazonError; end
|
69
|
+
|
70
|
+
# A configuration may be passed in as a string. Otherwise, the files
|
71
|
+
# <tt>/etc/amazonrc</tt> and <tt>~/.amazonrc</tt> are read if they exist
|
72
|
+
# and are readable.
|
73
|
+
#
|
74
|
+
def initialize(config_str)
|
75
|
+
locale = nil
|
76
|
+
|
77
|
+
if config_str
|
78
|
+
|
79
|
+
# We have been passed a config file as a string.
|
80
|
+
#
|
81
|
+
config_files = [ config_str ]
|
82
|
+
config_class = File
|
83
|
+
|
84
|
+
else
|
85
|
+
puts 'No config file specified'
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
config_files.each do |cf|
|
90
|
+
|
91
|
+
# We must determine whether the file is readable.
|
92
|
+
#
|
93
|
+
readable = File.exists?( cf ) && File.readable?( cf )
|
94
|
+
|
95
|
+
if readable
|
96
|
+
|
97
|
+
Amazon.dprintf( 'Opening %s ...', cf ) if config_class == File
|
98
|
+
|
99
|
+
config_class.open( cf ) { |f| lines = f.readlines }.each do |line|
|
100
|
+
line.chomp!
|
101
|
+
|
102
|
+
# Skip comments and blank lines.
|
103
|
+
#
|
104
|
+
next if line =~ /^(#|$)/
|
105
|
+
|
106
|
+
Amazon.dprintf( 'Read: %s', line )
|
107
|
+
|
108
|
+
# Determine whether we're entering the subsection of a new locale.
|
109
|
+
#
|
110
|
+
if match = line.match( /^\[(\w+)\]$/ )
|
111
|
+
locale = match[1]
|
112
|
+
Amazon.dprintf( "Config locale is now '%s'.", locale )
|
113
|
+
next
|
114
|
+
end
|
115
|
+
|
116
|
+
# Store these, because we'll probably find a use for these later.
|
117
|
+
#
|
118
|
+
begin
|
119
|
+
match = line.match( /^\s*(\S+)\s*=\s*(['"]?)([^'"]+)(['"]?)/ )
|
120
|
+
key, begin_quote, val, end_quote = match[1, 4]
|
121
|
+
raise ConfigError if begin_quote != end_quote
|
122
|
+
|
123
|
+
rescue NoMethodError, ConfigError
|
124
|
+
raise ConfigError, "bad config line: #{line}"
|
125
|
+
end
|
126
|
+
|
127
|
+
if locale && locale != 'global'
|
128
|
+
self[locale] ||= {}
|
129
|
+
self[locale][key] = val
|
130
|
+
else
|
131
|
+
self[key] = val
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
else
|
136
|
+
puts "could not open file"
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
data/lib/amazon/aws.rb
ADDED
@@ -0,0 +1,963 @@
|
|
1
|
+
# $Id: aws.rb,v 1.130 2010/03/20 11:58:50 ianmacd Exp $
|
2
|
+
#
|
3
|
+
#:include: ../../README.rdoc
|
4
|
+
|
5
|
+
module Amazon
|
6
|
+
|
7
|
+
module AWS
|
8
|
+
|
9
|
+
require 'amazon'
|
10
|
+
require 'amazon/aws/cache'
|
11
|
+
require 'enumerator'
|
12
|
+
require 'iconv'
|
13
|
+
require 'rexml/document'
|
14
|
+
require 'uri'
|
15
|
+
|
16
|
+
NAME = '%s/%s' % [ Amazon::NAME, 'AWS' ]
|
17
|
+
VERSION = '0.8.1'
|
18
|
+
USER_AGENT = '%s %s' % [ NAME, VERSION ]
|
19
|
+
|
20
|
+
# Default Associate tags to use per locale.
|
21
|
+
#
|
22
|
+
DEF_ASSOC = {
|
23
|
+
'ca' => 'caliban-20',
|
24
|
+
'de' => 'calibanorg0a-21',
|
25
|
+
'fr' => 'caliban08-21',
|
26
|
+
'jp' => 'calibanorg-20',
|
27
|
+
'uk' => 'caliban-21',
|
28
|
+
'us' => 'calibanorg-20'
|
29
|
+
}
|
30
|
+
|
31
|
+
# Service name and API version for AWS. The version of the API used can be
|
32
|
+
# changed via the user configuration file.
|
33
|
+
#
|
34
|
+
SERVICE = { 'Service' => 'AWSECommerceService',
|
35
|
+
'Version' => '2009-11-01'
|
36
|
+
}
|
37
|
+
|
38
|
+
# Maximum number of 301 and 302 HTTP responses to follow, should Amazon
|
39
|
+
# later decide to change the location of the service.
|
40
|
+
#
|
41
|
+
MAX_REDIRECTS = 3
|
42
|
+
|
43
|
+
# Maximum number of results pages that can be retrieved for a given
|
44
|
+
# search operation, using whichever pagination parameter is appropriate
|
45
|
+
# for that kind of operation.
|
46
|
+
#
|
47
|
+
PAGINATION = {
|
48
|
+
'ItemSearch' => { 'parameter' => 'ItemPage',
|
49
|
+
'max_page' => 400 },
|
50
|
+
'ItemLookup' => { 'parameter' => 'OfferPage',
|
51
|
+
'max_page' => 100 },
|
52
|
+
'ListLookup' => { 'parameter' => 'ProductPage',
|
53
|
+
'max_page' => 30 },
|
54
|
+
'ListSearch' => { 'parameter' => 'ListPage',
|
55
|
+
'max_page' => 20 },
|
56
|
+
'CustomerContentLookup' => { 'parameter' => 'ReviewPage',
|
57
|
+
'max_page' => 10 },
|
58
|
+
'CustomerContentSearch' => { 'parameter' => 'CustomerPage',
|
59
|
+
'max_page' => 20 },
|
60
|
+
'VehiclePartLookup' => { 'parameter' => 'FitmentPage',
|
61
|
+
'max_page' => 10 }
|
62
|
+
}
|
63
|
+
# N.B. ItemLookup can also use the following two pagination parameters
|
64
|
+
#
|
65
|
+
# max. page
|
66
|
+
# ---------
|
67
|
+
# VariationPage 150
|
68
|
+
# ReviewPage 20
|
69
|
+
|
70
|
+
|
71
|
+
# A hash to store character encoding converters.
|
72
|
+
#
|
73
|
+
@@encodings = {}
|
74
|
+
|
75
|
+
|
76
|
+
# Exception class for HTTP errors.
|
77
|
+
#
|
78
|
+
class HTTPError < AmazonError; end
|
79
|
+
|
80
|
+
|
81
|
+
# Exception class for faulty batch operations.
|
82
|
+
#
|
83
|
+
class BatchError < AmazonError; end
|
84
|
+
|
85
|
+
|
86
|
+
# Exception class for obsolete features.
|
87
|
+
#
|
88
|
+
class ObsolescenceError < AmazonError; end
|
89
|
+
|
90
|
+
|
91
|
+
class Endpoint
|
92
|
+
|
93
|
+
attr_reader :host, :path
|
94
|
+
|
95
|
+
def initialize(endpoint)
|
96
|
+
uri = URI.parse( endpoint )
|
97
|
+
@host = uri.host
|
98
|
+
@path = uri.path
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
ENDPOINT = {
|
103
|
+
'ca' => Endpoint.new( 'http://ecs.amazonaws.ca/onca/xml' ),
|
104
|
+
'de' => Endpoint.new( 'http://ecs.amazonaws.de/onca/xml' ),
|
105
|
+
'fr' => Endpoint.new( 'http://ecs.amazonaws.fr/onca/xml' ),
|
106
|
+
'jp' => Endpoint.new( 'http://ecs.amazonaws.jp/onca/xml' ),
|
107
|
+
'uk' => Endpoint.new( 'http://ecs.amazonaws.co.uk/onca/xml' ),
|
108
|
+
'us' => Endpoint.new( 'http://ecs.amazonaws.com/onca/xml' )
|
109
|
+
}
|
110
|
+
|
111
|
+
|
112
|
+
# Fetch a page, either from the cache or by HTTP. This is used internally.
|
113
|
+
#
|
114
|
+
def AWS.get_page(request) # :nodoc:
|
115
|
+
|
116
|
+
url = ENDPOINT[request.locale].path + request.query
|
117
|
+
cache_url = ENDPOINT[request.locale].host + url
|
118
|
+
|
119
|
+
# Check for cached page and return that if it's there.
|
120
|
+
#
|
121
|
+
if request.cache && request.cache.cached?( cache_url )
|
122
|
+
body = request.cache.fetch( cache_url )
|
123
|
+
return body if body
|
124
|
+
end
|
125
|
+
|
126
|
+
# Check whether we have a secret key available for signing the request.
|
127
|
+
# If so, sign the request for authentication.
|
128
|
+
#
|
129
|
+
if request.config['secret_key_id']
|
130
|
+
unless request.sign
|
131
|
+
Amazon.dprintf( 'Warning! Failed to sign request. No OpenSSL support for SHA256 digest.' )
|
132
|
+
end
|
133
|
+
|
134
|
+
url = ENDPOINT[request.locale].path + request.query
|
135
|
+
end
|
136
|
+
|
137
|
+
# Get the existing connection. If there isn't one, force a new one.
|
138
|
+
#
|
139
|
+
conn = request.conn || request.reconnect.conn
|
140
|
+
user_agent = request.user_agent
|
141
|
+
|
142
|
+
Amazon.dprintf( 'Fetching http://%s%s ...', conn.address, url )
|
143
|
+
|
144
|
+
begin
|
145
|
+
response = conn.get( url, { 'user-agent' => user_agent } )
|
146
|
+
|
147
|
+
# If we've pulled and processed a lot of pages from the cache (or
|
148
|
+
# just not passed by here recently), the HTTP connection to the server
|
149
|
+
# will probably have timed out.
|
150
|
+
#
|
151
|
+
rescue EOFError, Errno::ECONNABORTED, Errno::ECONNREFUSED,
|
152
|
+
Errno::ECONNRESET, Errno::EPIPE, Errno::ETIMEDOUT,
|
153
|
+
Timeout::Error => error
|
154
|
+
Amazon.dprintf( 'Connection to server lost: %s. Retrying...', error )
|
155
|
+
conn = request.reconnect.conn
|
156
|
+
retry
|
157
|
+
end
|
158
|
+
|
159
|
+
redirects = 0
|
160
|
+
while response.key? 'location'
|
161
|
+
if ( redirects += 1 ) > MAX_REDIRECTS
|
162
|
+
raise HTTPError, "More than #{MAX_REDIRECTS} redirections"
|
163
|
+
end
|
164
|
+
|
165
|
+
old_url = url
|
166
|
+
url = URI.parse( response['location'] )
|
167
|
+
url.scheme = old_url.scheme unless url.scheme
|
168
|
+
url.host = old_url.host unless url.host
|
169
|
+
Amazon.dprintf( 'Following HTTP %s to %s ...', response.code, url )
|
170
|
+
response = Net::HTTP::start( url.host ).
|
171
|
+
get( url.path, { 'user-agent' => user_agent } )
|
172
|
+
end
|
173
|
+
|
174
|
+
if response.code != '200'
|
175
|
+
raise HTTPError, "HTTP response code #{response.code}"
|
176
|
+
end
|
177
|
+
|
178
|
+
# Cache the page if we're using a cache.
|
179
|
+
#
|
180
|
+
if request.cache
|
181
|
+
request.cache.store( cache_url, response.body )
|
182
|
+
end
|
183
|
+
|
184
|
+
response.body
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
def AWS.assemble_query(items, encoding=nil) # :nodoc:
|
189
|
+
|
190
|
+
query = ''
|
191
|
+
@@encodings[encoding] ||= Iconv.new( 'utf-8', encoding ) if encoding
|
192
|
+
|
193
|
+
# We must sort the items into an array to get reproducible ordering
|
194
|
+
# of the query parameters. Otherwise, URL caching would not work. We
|
195
|
+
# must also convert the parameter values to strings, in case Symbols
|
196
|
+
# have been used as the values.
|
197
|
+
#
|
198
|
+
items.sort { |a,b| a.to_s <=> b.to_s }.each do |k, v|
|
199
|
+
if encoding
|
200
|
+
query << '&%s=%s' %
|
201
|
+
[ k, Amazon.url_encode( @@encodings[encoding].iconv( v.to_s ) ) ]
|
202
|
+
else
|
203
|
+
query << '&%s=%s' % [ k, Amazon.url_encode( v.to_s ) ]
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Replace initial ampersand with question-mark.
|
208
|
+
#
|
209
|
+
query[0] = '?'
|
210
|
+
|
211
|
+
query
|
212
|
+
end
|
213
|
+
|
214
|
+
|
215
|
+
# Everything returned by AWS is an AWSObject.
|
216
|
+
#
|
217
|
+
class AWSObject
|
218
|
+
|
219
|
+
include REXML
|
220
|
+
|
221
|
+
# This method can be used to load AWSObject data previously serialised
|
222
|
+
# by Marshal.dump.
|
223
|
+
#
|
224
|
+
# Example:
|
225
|
+
#
|
226
|
+
# File.open( 'aws.dat' ) { |f| Amazon::AWS::AWSObject.load( f ) }
|
227
|
+
#
|
228
|
+
# Marshal.load cannot be used directly, because subclasses of AWSObject
|
229
|
+
# are dynamically defined as needed when AWS XML responses are parsed.
|
230
|
+
#
|
231
|
+
# Later attempts to load objects instantiated from these classes cause a
|
232
|
+
# problem for Marshal, because it knows nothing of classes that were
|
233
|
+
# dynamically defined by a separate process.
|
234
|
+
#
|
235
|
+
def AWSObject.load(io)
|
236
|
+
begin
|
237
|
+
Marshal.load( io )
|
238
|
+
rescue ArgumentError => ex
|
239
|
+
m = ex.to_s.match( /Amazon::AWS::AWSObject::([^ ]+)/ )
|
240
|
+
const_set( m[1], Class.new( AWSObject ) )
|
241
|
+
|
242
|
+
io.rewind
|
243
|
+
retry
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
# This method can be used to load AWSObject data previously serialised
|
249
|
+
# by YAML.dump.
|
250
|
+
#
|
251
|
+
# Example:
|
252
|
+
#
|
253
|
+
# File.open( 'aws.yaml' ) { |f| Amazon::AWS::AWSObject.yaml_load( f ) }
|
254
|
+
#
|
255
|
+
# The standard YAML.load cannot be used directly, because subclasses of
|
256
|
+
# AWSObject are dynamically defined as needed when AWS XML responses are
|
257
|
+
# parsed.
|
258
|
+
#
|
259
|
+
# Later attempts to load objects instantiated from these classes cause a
|
260
|
+
# problem for YAML, because it knows nothing of classes that were
|
261
|
+
# dynamically defined by a separate process.
|
262
|
+
#
|
263
|
+
def AWSObject.yaml_load(io)
|
264
|
+
io.each do |line|
|
265
|
+
|
266
|
+
# File data is external, so it's deemed unsafe when $SAFE > 0, which
|
267
|
+
# is the case with mod_ruby, for example, where $SAFE == 1.
|
268
|
+
#
|
269
|
+
# YAML data isn't eval'ed or anything dangerous like that, so we
|
270
|
+
# consider it safe to untaint it. If we don't, mod_ruby will complain
|
271
|
+
# when Module#const_defined? is invoked a few lines down from here.
|
272
|
+
#
|
273
|
+
line.untaint
|
274
|
+
|
275
|
+
m = line.match( /Amazon::AWS::AWSObject::([^ ]+)/ )
|
276
|
+
if m
|
277
|
+
cl_name = [ m[1] ]
|
278
|
+
|
279
|
+
# Module#const_defined? takes 2 parameters in Ruby 1.9.
|
280
|
+
#
|
281
|
+
cl_name << false if RUBY_VERSION >= '1.9.0'
|
282
|
+
|
283
|
+
unless AWSObject.const_defined?( *cl_name )
|
284
|
+
AWSObject.const_set( m[1], Class.new( AWSObject ) )
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
io.rewind
|
291
|
+
YAML.load( io )
|
292
|
+
end
|
293
|
+
|
294
|
+
|
295
|
+
def initialize(op=nil)
|
296
|
+
# The name of this instance variable must never clash with the
|
297
|
+
# uncamelised name of an Amazon tag.
|
298
|
+
#
|
299
|
+
# This is used to store the REXML::Text value of an element, which
|
300
|
+
# exists only when the element contains no children.
|
301
|
+
#
|
302
|
+
@__val__ = nil
|
303
|
+
@__op__ = op if op
|
304
|
+
end
|
305
|
+
|
306
|
+
|
307
|
+
def method_missing(method, *params)
|
308
|
+
iv = '@' + method.id2name
|
309
|
+
|
310
|
+
if instance_variables.include?( iv )
|
311
|
+
|
312
|
+
# Return the instance variable that matches the method called.
|
313
|
+
#
|
314
|
+
instance_variable_get( iv )
|
315
|
+
elsif instance_variables.include?( iv.to_sym )
|
316
|
+
|
317
|
+
# Ruby 1.9 Object#instance_variables method returns Array of Symbol,
|
318
|
+
# not String.
|
319
|
+
#
|
320
|
+
instance_variable_get( iv.to_sym )
|
321
|
+
elsif @__val__.respond_to?( method.id2name )
|
322
|
+
|
323
|
+
# If our value responds to the method in question, call the method
|
324
|
+
# on that.
|
325
|
+
#
|
326
|
+
@__val__.send( method.id2name )
|
327
|
+
else
|
328
|
+
nil
|
329
|
+
end
|
330
|
+
end
|
331
|
+
private :method_missing
|
332
|
+
|
333
|
+
|
334
|
+
def remove_val
|
335
|
+
remove_instance_variable( :@__val__ )
|
336
|
+
end
|
337
|
+
private :remove_val
|
338
|
+
|
339
|
+
|
340
|
+
# Iterator method for cycling through an object's properties and values.
|
341
|
+
#
|
342
|
+
def each # :yields: property, value
|
343
|
+
self.properties.each do |iv|
|
344
|
+
yield iv, instance_variable_get( "@#{iv}" )
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
alias :each_property :each
|
349
|
+
|
350
|
+
|
351
|
+
def inspect # :nodoc:
|
352
|
+
remove_val if instance_variable_defined?( :@__val__ ) && @__val__.nil?
|
353
|
+
str = super
|
354
|
+
str.sub( /@__val__=/, 'value=' ) if str
|
355
|
+
end
|
356
|
+
|
357
|
+
|
358
|
+
def to_s # :nodoc:
|
359
|
+
if instance_variable_defined?( :@__val__ )
|
360
|
+
return @__val__ if @__val__.is_a?( String )
|
361
|
+
remove_val
|
362
|
+
end
|
363
|
+
|
364
|
+
string = ''
|
365
|
+
|
366
|
+
# Assemble the object's details.
|
367
|
+
#
|
368
|
+
each { |iv, value| string << "%s = %s\n" % [ iv, value ] }
|
369
|
+
|
370
|
+
string
|
371
|
+
end
|
372
|
+
|
373
|
+
alias :to_str :to_s
|
374
|
+
|
375
|
+
|
376
|
+
def ==(other) # :nodoc:
|
377
|
+
@__val__.to_s == other
|
378
|
+
end
|
379
|
+
|
380
|
+
|
381
|
+
def =~(other) # :nodoc:
|
382
|
+
@__val__.to_s =~ other
|
383
|
+
end
|
384
|
+
|
385
|
+
|
386
|
+
# This alias makes the ability to determine an AWSObject's properties a
|
387
|
+
# little more intuitive. It's pretty much just an alias for the
|
388
|
+
# inherited <em>Object#instance_variables</em> method, with a little
|
389
|
+
# tidying.
|
390
|
+
#
|
391
|
+
def properties
|
392
|
+
# Make sure we remove the leading @.
|
393
|
+
#
|
394
|
+
iv = instance_variables.collect { |v| v = v[1..-1] }
|
395
|
+
iv.delete( '__val__' )
|
396
|
+
iv
|
397
|
+
end
|
398
|
+
|
399
|
+
|
400
|
+
# Provide a shortcut down to the data likely to be of most interest.
|
401
|
+
# This method is experimental and may be removed.
|
402
|
+
#
|
403
|
+
def kernel # :nodoc:
|
404
|
+
# E.g. Amazon::AWS::SellerListingLookup -> seller_listing_lookup
|
405
|
+
#
|
406
|
+
stub = Amazon.uncamelise( @__op__.class.to_s.sub( /^.+::/, '' ) )
|
407
|
+
|
408
|
+
# E.g. seller_listing_response
|
409
|
+
#
|
410
|
+
level1 = stub + '_response'
|
411
|
+
|
412
|
+
# E.g. seller_listing
|
413
|
+
#
|
414
|
+
level3 = stub.sub( /_[^_]+$/, '' )
|
415
|
+
|
416
|
+
# E.g. seller_listings
|
417
|
+
#
|
418
|
+
level2 = level3 + 's'
|
419
|
+
|
420
|
+
# E.g.
|
421
|
+
# seller_listing_search_response[0].seller_listings[0].seller_listing
|
422
|
+
#
|
423
|
+
self.instance_variable_get( "@#{level1}" )[0].
|
424
|
+
instance_variable_get( "@#{level2}" )[0].
|
425
|
+
instance_variable_get( "@#{level3}" )
|
426
|
+
end
|
427
|
+
|
428
|
+
|
429
|
+
# Convert an AWSObject to a Hash.
|
430
|
+
#
|
431
|
+
def to_h
|
432
|
+
hash = {}
|
433
|
+
|
434
|
+
each do |iv, value|
|
435
|
+
if value.is_a? AWSObject
|
436
|
+
hash[iv] = value.to_h
|
437
|
+
elsif value.is_a?( AWSArray ) && value.size == 1
|
438
|
+
hash[iv] = value[0]
|
439
|
+
else
|
440
|
+
hash[iv] = value
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
hash
|
445
|
+
end
|
446
|
+
|
447
|
+
|
448
|
+
# Fake the appearance of an AWSObject as a hash. _key_ should be any
|
449
|
+
# attribute of the object and can be a String, Symbol or anything else
|
450
|
+
# that can be converted to a String with to_s.
|
451
|
+
#
|
452
|
+
def [](key)
|
453
|
+
instance_variable_get( "@#{key}" )
|
454
|
+
end
|
455
|
+
|
456
|
+
|
457
|
+
# Recursively walk through an XML tree, starting from _node_. This is
|
458
|
+
# called internally and is not intended for user code.
|
459
|
+
#
|
460
|
+
def walk(node) # :nodoc:
|
461
|
+
|
462
|
+
if node.instance_of?( REXML::Document )
|
463
|
+
walk( node.root )
|
464
|
+
|
465
|
+
elsif node.instance_of?( REXML::Element )
|
466
|
+
name = Amazon.uncamelise( node.name )
|
467
|
+
|
468
|
+
cl_name = [ node.name ]
|
469
|
+
|
470
|
+
# Module#const_defined? takes 2 parameters in Ruby 1.9.
|
471
|
+
#
|
472
|
+
cl_name << false if RUBY_VERSION >= '1.9.0'
|
473
|
+
|
474
|
+
# Create a class for the new element type unless it already exists.
|
475
|
+
#
|
476
|
+
unless AWS::AWSObject.const_defined?( *cl_name )
|
477
|
+
cl = AWS::AWSObject.const_set( node.name, Class.new( AWSObject ) )
|
478
|
+
|
479
|
+
# Give it an accessor for @attrib.
|
480
|
+
#
|
481
|
+
cl.send( :attr_accessor, :attrib )
|
482
|
+
end
|
483
|
+
|
484
|
+
# Instantiate an object in the newly created class.
|
485
|
+
#
|
486
|
+
obj = AWS::AWSObject.const_get( node.name ).new
|
487
|
+
|
488
|
+
sym_name = "@#{name}".to_sym
|
489
|
+
|
490
|
+
if instance_variable_defined?( sym_name)
|
491
|
+
instance_variable_set( sym_name,
|
492
|
+
instance_variable_get( sym_name ) << obj )
|
493
|
+
else
|
494
|
+
instance_variable_set( sym_name, AWSArray.new( [ obj ] ) )
|
495
|
+
end
|
496
|
+
|
497
|
+
if node.has_attributes?
|
498
|
+
obj.attrib = {}
|
499
|
+
node.attributes.each_pair do |a_name, a_value|
|
500
|
+
obj.attrib[a_name.downcase] =
|
501
|
+
a_value.to_s.sub( /^#{a_name}=/, '' )
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
node.children.each { |child| obj.walk( child ) }
|
506
|
+
|
507
|
+
else # REXML::Text
|
508
|
+
@__val__ = node.to_s
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
|
513
|
+
# For objects of class AWSObject::.*Image, fetch the image in question,
|
514
|
+
# optionally overlaying a discount icon for the percentage amount of
|
515
|
+
# _discount_ to the image.
|
516
|
+
#
|
517
|
+
def get(discount=nil)
|
518
|
+
if self.class.to_s =~ /Image$/ && @url
|
519
|
+
url = URI.parse( @url[0] )
|
520
|
+
url.path.sub!( /(\.\d\d\._)/, "\\1PE#{discount}" ) if discount
|
521
|
+
|
522
|
+
# FIXME: All HTTP in Ruby/AWS should go through the same method.
|
523
|
+
#
|
524
|
+
Net::HTTP.start( url.host, url.port ) do |http|
|
525
|
+
http.get( url.path )
|
526
|
+
end.body
|
527
|
+
|
528
|
+
else
|
529
|
+
nil
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
end
|
534
|
+
|
535
|
+
|
536
|
+
# Everything we get back from AWS is transformed into an array. Many of
|
537
|
+
# these, however, have only one element, because the corresponding XML
|
538
|
+
# consists of a parent element containing only a single child element.
|
539
|
+
#
|
540
|
+
# This class consists solely to allow single element arrays to pass a
|
541
|
+
# method call down to their one element, thus obviating the need for lots
|
542
|
+
# of references to <tt>foo[0]</tt> in user code.
|
543
|
+
#
|
544
|
+
# For example, the following:
|
545
|
+
#
|
546
|
+
# items = resp.item_search_response[0].items[0].item
|
547
|
+
#
|
548
|
+
# can be reduced to:
|
549
|
+
#
|
550
|
+
# items = resp.item_search_response.items.item
|
551
|
+
#
|
552
|
+
class AWSArray < Array
|
553
|
+
|
554
|
+
def method_missing(method, *params)
|
555
|
+
self.size == 1 ? self[0].send( method, *params ) : super
|
556
|
+
end
|
557
|
+
private :method_missing
|
558
|
+
|
559
|
+
|
560
|
+
# In the case of a single-element array, return the first element,
|
561
|
+
# converted to a String.
|
562
|
+
#
|
563
|
+
def to_s # :nodoc:
|
564
|
+
self.size == 1 ? self[0].to_s : super
|
565
|
+
end
|
566
|
+
|
567
|
+
alias :to_str :to_s
|
568
|
+
|
569
|
+
|
570
|
+
# In the case of a single-element array, return the first element,
|
571
|
+
# converted to an Integer.
|
572
|
+
#
|
573
|
+
def to_i # :nodoc:
|
574
|
+
self.size == 1 ? self[0].to_i : super
|
575
|
+
end
|
576
|
+
|
577
|
+
|
578
|
+
# In the case of a single-element array, compare the first element with
|
579
|
+
# _other_.
|
580
|
+
#
|
581
|
+
def ==(other) # :nodoc:
|
582
|
+
self.size == 1 ? self[0].to_s == other : super
|
583
|
+
end
|
584
|
+
|
585
|
+
|
586
|
+
# In the case of a single-element array, perform a pattern match on the
|
587
|
+
# first element against _other_.
|
588
|
+
#
|
589
|
+
def =~(other) # :nodoc:
|
590
|
+
self.size == 1 ? self[0].to_s =~ other : super
|
591
|
+
end
|
592
|
+
|
593
|
+
end
|
594
|
+
|
595
|
+
|
596
|
+
# This is the base class of all AWS operations.
|
597
|
+
#
|
598
|
+
class Operation
|
599
|
+
|
600
|
+
# These are the types of AWS operation currently implemented by Ruby/AWS.
|
601
|
+
#
|
602
|
+
OPERATIONS = %w[
|
603
|
+
ItemSearch
|
604
|
+
|
605
|
+
|
606
|
+
]
|
607
|
+
|
608
|
+
attr_reader :kind
|
609
|
+
attr_accessor :params, :response_group
|
610
|
+
|
611
|
+
def initialize(parameters)
|
612
|
+
|
613
|
+
op_kind = self.class.to_s.sub( /^.*::/, '' )
|
614
|
+
|
615
|
+
raise "Bad operation: #{op_kind}" unless OPERATIONS.include?( op_kind )
|
616
|
+
|
617
|
+
if ResponseGroup::DEFAULT.key?( op_kind )
|
618
|
+
response_group =
|
619
|
+
ResponseGroup.new( ResponseGroup::DEFAULT[op_kind] )
|
620
|
+
else
|
621
|
+
response_group = nil
|
622
|
+
end
|
623
|
+
|
624
|
+
if op_kind =~ /^Cart/
|
625
|
+
@params = parameters
|
626
|
+
else
|
627
|
+
@params = Hash.new { |hash, key| hash[key] = [] }
|
628
|
+
@response_group = Hash.new { |hash, key| hash[key] = [] }
|
629
|
+
|
630
|
+
unless op_kind == 'MultipleOperation'
|
631
|
+
@params[op_kind] = [ parameters ]
|
632
|
+
@response_group[op_kind] = [ response_group ]
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
@kind = op_kind
|
637
|
+
end
|
638
|
+
|
639
|
+
|
640
|
+
# Make sure we can still get to the old @response_group= writer method.
|
641
|
+
#
|
642
|
+
alias :response_group_orig= :response_group=
|
643
|
+
|
644
|
+
# If the user assigns to @response_group, we need to set this response
|
645
|
+
# group for any and all operations that may have been batched.
|
646
|
+
#
|
647
|
+
def response_group=(rg) # :nodoc:
|
648
|
+
@params.each_value do |op_arr|
|
649
|
+
op_arr.each do |op|
|
650
|
+
op['ResponseGroup'] = rg
|
651
|
+
end
|
652
|
+
end
|
653
|
+
end
|
654
|
+
|
655
|
+
|
656
|
+
# Group together operations of the same class in a batch request.
|
657
|
+
# _operations_ should be either an operation of the same class as *self*
|
658
|
+
# or an array of such operations.
|
659
|
+
#
|
660
|
+
# If you need to batch operations of different classes, use a
|
661
|
+
# MultipleOperation instead.
|
662
|
+
#
|
663
|
+
# Example:
|
664
|
+
#
|
665
|
+
# is = ItemSearch.new( 'Books', { 'Title' => 'ruby programming' } )
|
666
|
+
# is2 = ItemSearch.new( 'Music', { 'Artist' => 'stranglers' } )
|
667
|
+
# is.response_group = ResponseGroup.new( :Small )
|
668
|
+
# is2.response_group = ResponseGroup.new( :Tracks )
|
669
|
+
# is.batch( is2 )
|
670
|
+
#
|
671
|
+
# Please see MultipleOperation.new for implementation details that also
|
672
|
+
# apply to batched operations.
|
673
|
+
#
|
674
|
+
def batch(*operations)
|
675
|
+
|
676
|
+
operations.flatten.each do |op|
|
677
|
+
|
678
|
+
unless self.class == op.class
|
679
|
+
raise BatchError, "You can't batch operations of different classes. Use class MultipleOperation."
|
680
|
+
end
|
681
|
+
|
682
|
+
# Add the operation's single element array containing the parameter
|
683
|
+
# hash to the array.
|
684
|
+
#
|
685
|
+
@params[op.kind].concat( op.params[op.kind] )
|
686
|
+
|
687
|
+
# Add the operation's response group array to the array.
|
688
|
+
#
|
689
|
+
@response_group[op.kind].concat( op.response_group[op.kind] )
|
690
|
+
end
|
691
|
+
|
692
|
+
end
|
693
|
+
|
694
|
+
|
695
|
+
# Return a hash of operation parameters and values, possibly converted to
|
696
|
+
# batch syntax, suitable for encoding in a query.
|
697
|
+
#
|
698
|
+
def query_parameters # :nodoc:
|
699
|
+
query = {}
|
700
|
+
|
701
|
+
@params.each do |op_kind, ops|
|
702
|
+
|
703
|
+
# If we have only one type of operation and only one operation of
|
704
|
+
# that type, return that one in non-batched syntax.
|
705
|
+
#
|
706
|
+
if @params.size == 1 && @params[op_kind].size == 1
|
707
|
+
return { 'Operation' => op_kind,
|
708
|
+
'ResponseGroup' => @response_group[op_kind][0] }.
|
709
|
+
merge( @params[op_kind][0] )
|
710
|
+
end
|
711
|
+
|
712
|
+
# Otherwise, use batch syntax.
|
713
|
+
#
|
714
|
+
ops.each_with_index do |op, op_index|
|
715
|
+
|
716
|
+
# Make sure we use a response group of some kind.
|
717
|
+
#
|
718
|
+
shared = '%s.%d.ResponseGroup' % [ op_kind, op_index + 1 ]
|
719
|
+
query[shared] = op['ResponseGroup'] ||
|
720
|
+
ResponseGroup::DEFAULT[op_kind]
|
721
|
+
|
722
|
+
# Add all of the parameters to the query hash.
|
723
|
+
#
|
724
|
+
op.each do |k, v|
|
725
|
+
shared = '%s.%d.%s' % [ op_kind, op_index + 1, k ]
|
726
|
+
query[shared] = v
|
727
|
+
end
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
# Add the operation list.
|
732
|
+
#
|
733
|
+
{ 'Operation' => @params.keys.join( ',' ) }.merge( query )
|
734
|
+
end
|
735
|
+
|
736
|
+
end
|
737
|
+
|
738
|
+
|
739
|
+
# This is the class for the most common type of AWS look-up, an
|
740
|
+
# ItemSearch. This allows you to search for items that match a set of
|
741
|
+
# broad criteria. It returns items for sale by Amazon merchants and most
|
742
|
+
# types of seller.
|
743
|
+
#
|
744
|
+
class ItemSearch < Operation
|
745
|
+
|
746
|
+
# Not all search indices work in all locales. It is the user's
|
747
|
+
# responsibility to ensure that a given index is valid within a given
|
748
|
+
# locale.
|
749
|
+
#
|
750
|
+
# According to the AWS documentation:
|
751
|
+
#
|
752
|
+
# - *All* searches through all indices.
|
753
|
+
# - *Blended* combines Apparel, Automotive, Books, DVD, Electronics,
|
754
|
+
# GourmetFood, Kitchen, Music, PCHardware, PetSupplies, Software,
|
755
|
+
# SoftwareVideoGames, SportingGoods, Tools, Toys, VHS and VideoGames.
|
756
|
+
# - *Merchants* combines all search indices for a merchant given with
|
757
|
+
# MerchantId.
|
758
|
+
# - *Music* combines the Classical, DigitalMusic, and MusicTracks
|
759
|
+
# indices.
|
760
|
+
# - *Video* combines the DVD and VHS search indices.
|
761
|
+
#
|
762
|
+
SEARCH_INDICES = %w[
|
763
|
+
All
|
764
|
+
Apparel
|
765
|
+
Automotive
|
766
|
+
Baby
|
767
|
+
Beauty
|
768
|
+
Blended
|
769
|
+
Books
|
770
|
+
Classical
|
771
|
+
DigitalMusic
|
772
|
+
DVD
|
773
|
+
Electronics
|
774
|
+
ForeignBooks
|
775
|
+
GourmetFood
|
776
|
+
Grocery
|
777
|
+
HealthPersonalCare
|
778
|
+
Hobbies
|
779
|
+
HomeGarden
|
780
|
+
HomeImprovement
|
781
|
+
Industrial
|
782
|
+
Jewelry
|
783
|
+
KindleStore
|
784
|
+
Kitchen
|
785
|
+
Lighting
|
786
|
+
Magazines
|
787
|
+
Merchants
|
788
|
+
Miscellaneous
|
789
|
+
MP3Downloads
|
790
|
+
Music
|
791
|
+
MusicalInstruments
|
792
|
+
MusicTracks
|
793
|
+
OfficeProducts
|
794
|
+
OutdoorLiving
|
795
|
+
Outlet
|
796
|
+
PCHardware
|
797
|
+
PetSupplies
|
798
|
+
Photo
|
799
|
+
Shoes
|
800
|
+
SilverMerchants
|
801
|
+
Software
|
802
|
+
SoftwareVideoGames
|
803
|
+
SportingGoods
|
804
|
+
Tools
|
805
|
+
Toys
|
806
|
+
UnboxVideo
|
807
|
+
VHS
|
808
|
+
Video
|
809
|
+
VideoGames
|
810
|
+
Watches
|
811
|
+
Wireless
|
812
|
+
WirelessAccessories
|
813
|
+
]
|
814
|
+
|
815
|
+
|
816
|
+
# Search AWS for items. _search_index_ must be one of _SEARCH_INDICES_
|
817
|
+
# and _parameters_ is an optional hash of parameters that further refine
|
818
|
+
# the scope of the search.
|
819
|
+
#
|
820
|
+
# Example:
|
821
|
+
#
|
822
|
+
# is = ItemSearch.new( 'Books', { 'Title' => 'ruby programming' } )
|
823
|
+
#
|
824
|
+
# In the above example, we search for books with <b>Ruby Programming</b>
|
825
|
+
# in the title.
|
826
|
+
#
|
827
|
+
def initialize(search_index, parameters)
|
828
|
+
unless SEARCH_INDICES.include? search_index.to_s
|
829
|
+
raise "Invalid search index: #{search_index}"
|
830
|
+
end
|
831
|
+
|
832
|
+
super( { 'SearchIndex' => search_index }.merge( parameters ) )
|
833
|
+
end
|
834
|
+
|
835
|
+
end
|
836
|
+
|
837
|
+
|
838
|
+
|
839
|
+
|
840
|
+
# Response groups determine which data pertaining to the item(s) being
|
841
|
+
# sought is returned. They strongly influence the amount of data returned,
|
842
|
+
# so you should always use the smallest response group(s) containing the
|
843
|
+
# data of interest to you, to avoid masses of unnecessary data being
|
844
|
+
# returned.
|
845
|
+
#
|
846
|
+
class ResponseGroup
|
847
|
+
|
848
|
+
# The default type of response group to use with each type of operation.
|
849
|
+
#
|
850
|
+
DEFAULT = {
|
851
|
+
'ItemSearch' => :Large
|
852
|
+
}
|
853
|
+
|
854
|
+
# Define a set of one or more response groups to be applied to items
|
855
|
+
# retrieved by an AWS operation.
|
856
|
+
#
|
857
|
+
# Example:
|
858
|
+
#
|
859
|
+
# rg = ResponseGroup.new( 'Medium', 'Offers', 'Reviews' )
|
860
|
+
#
|
861
|
+
def initialize(*rg)
|
862
|
+
@list = rg.join( ',' )
|
863
|
+
end
|
864
|
+
|
865
|
+
|
866
|
+
# We need a form we can interpolate into query strings.
|
867
|
+
#
|
868
|
+
def to_s # :nodoc:
|
869
|
+
@list
|
870
|
+
end
|
871
|
+
|
872
|
+
end
|
873
|
+
|
874
|
+
|
875
|
+
# All dynamically generated exceptions occur within this namespace.
|
876
|
+
#
|
877
|
+
module Error
|
878
|
+
|
879
|
+
# The base exception class for errors that result from AWS operations.
|
880
|
+
# Classes for these are dynamically generated as subclasses of this one.
|
881
|
+
#
|
882
|
+
class AWSError < AmazonError; end
|
883
|
+
|
884
|
+
def Error.exception(xml)
|
885
|
+
err_class = xml.elements['Code'].text.sub( /^AWS.*\./, '' )
|
886
|
+
err_msg = xml.elements['Message'].text
|
887
|
+
|
888
|
+
# Dynamically define a new exception class for this class of error,
|
889
|
+
# unless it already exists.
|
890
|
+
#
|
891
|
+
# Note that Ruby 1.9's Module.const_defined? needs a second parameter
|
892
|
+
# of *false*, or it will also search AWSError's ancestors.
|
893
|
+
#
|
894
|
+
cd_params = [ err_class ]
|
895
|
+
cd_params << false if RUBY_VERSION >= '1.9.0'
|
896
|
+
|
897
|
+
unless Amazon::AWS::Error.const_defined?( *cd_params )
|
898
|
+
Amazon::AWS::Error.const_set( err_class, Class.new( AWSError ) )
|
899
|
+
end
|
900
|
+
|
901
|
+
# Generate and return a new exception from the relevant class.
|
902
|
+
#
|
903
|
+
Amazon::AWS::Error.const_get( err_class ).new( err_msg )
|
904
|
+
end
|
905
|
+
|
906
|
+
end
|
907
|
+
|
908
|
+
|
909
|
+
# Create a shorthand module method for each of the AWS operations. These
|
910
|
+
# can be used to create less verbose code at the expense of flexibility.
|
911
|
+
#
|
912
|
+
# For example, we might normally write the following code:
|
913
|
+
#
|
914
|
+
# is = ItemSearch.new( 'Books', { 'Title' => 'Ruby' } )
|
915
|
+
# rg = ResponseGroup.new( 'Large' )
|
916
|
+
# req = Request.new
|
917
|
+
# response = req.search( is, rg )
|
918
|
+
#
|
919
|
+
# but we could instead use ItemSearch's associated module method as
|
920
|
+
# follows:
|
921
|
+
#
|
922
|
+
# response = Amazon::AWS.item_search( 'Books', { 'Title' => 'Ruby' } )
|
923
|
+
#
|
924
|
+
# Note that these equivalent module methods all attempt to use the *Large*
|
925
|
+
# response group, which may or may not work. If an
|
926
|
+
# Amazon::AWS::Error::InvalidResponseGroup is raised, we will scan the
|
927
|
+
# text of the error message returned by AWS to try to glean a valid
|
928
|
+
# response group and then retry the operation using that instead.
|
929
|
+
|
930
|
+
|
931
|
+
# Obtain a list of all subclasses of the Operation class.
|
932
|
+
#
|
933
|
+
classes =
|
934
|
+
ObjectSpace.enum_for( :each_object, class << Operation; self; end ).to_a
|
935
|
+
|
936
|
+
classes.each do |cl|
|
937
|
+
# Convert class name to Ruby case, e.g. ItemSearch => item_search.
|
938
|
+
#
|
939
|
+
class_name = cl.to_s.sub( /^.+::/, '' )
|
940
|
+
uncamelised_name = Amazon.uncamelise( class_name )
|
941
|
+
|
942
|
+
# Define the module method counterpart of each operation.
|
943
|
+
#
|
944
|
+
module_eval %Q(
|
945
|
+
def AWS.#{uncamelised_name}(*params)
|
946
|
+
# Instantiate an object of the desired operational class.
|
947
|
+
#
|
948
|
+
op = #{cl.to_s}.new( *params )
|
949
|
+
|
950
|
+
# Attempt a search for the given operation using its default
|
951
|
+
# response group types.
|
952
|
+
#
|
953
|
+
results = Search::Request.new.search( op )
|
954
|
+
yield results if block_given?
|
955
|
+
return results
|
956
|
+
|
957
|
+
end
|
958
|
+
)
|
959
|
+
end
|
960
|
+
|
961
|
+
end
|
962
|
+
|
963
|
+
end
|