papercavalier-ruby-aaws 0.8.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.
Files changed (59) hide show
  1. data/.gitignore +3 -0
  2. data/COPYING +340 -0
  3. data/INSTALL +260 -0
  4. data/NEWS +808 -0
  5. data/README +679 -0
  6. data/README.rdoc +140 -0
  7. data/Rakefile +17 -0
  8. data/VERSION.yml +5 -0
  9. data/example/batch_operation +28 -0
  10. data/example/browse_node_lookup1 +46 -0
  11. data/example/customer_content_lookup1 +27 -0
  12. data/example/customer_content_search1 +21 -0
  13. data/example/example1 +78 -0
  14. data/example/help1 +24 -0
  15. data/example/item_lookup1 +56 -0
  16. data/example/item_lookup2 +56 -0
  17. data/example/item_search1 +30 -0
  18. data/example/item_search2 +37 -0
  19. data/example/item_search3 +23 -0
  20. data/example/list_lookup1 +29 -0
  21. data/example/list_search1 +30 -0
  22. data/example/multiple_operation1 +69 -0
  23. data/example/seller_listing_lookup1 +30 -0
  24. data/example/seller_listing_search1 +29 -0
  25. data/example/seller_lookup1 +45 -0
  26. data/example/shopping_cart1 +42 -0
  27. data/example/similarity_lookup1 +48 -0
  28. data/example/tag_lookup1 +34 -0
  29. data/example/transaction_lookup1 +25 -0
  30. data/example/vehicle_search +22 -0
  31. data/lib/amazon.rb +165 -0
  32. data/lib/amazon/aws.rb +1493 -0
  33. data/lib/amazon/aws/cache.rb +141 -0
  34. data/lib/amazon/aws/search.rb +464 -0
  35. data/lib/amazon/aws/shoppingcart.rb +537 -0
  36. data/lib/amazon/locale.rb +102 -0
  37. data/test/setup.rb +56 -0
  38. data/test/tc_amazon.rb +20 -0
  39. data/test/tc_aws.rb +160 -0
  40. data/test/tc_browse_node_lookup.rb +49 -0
  41. data/test/tc_customer_content_lookup.rb +49 -0
  42. data/test/tc_help.rb +44 -0
  43. data/test/tc_item_lookup.rb +47 -0
  44. data/test/tc_item_search.rb +105 -0
  45. data/test/tc_list_lookup.rb +60 -0
  46. data/test/tc_list_search.rb +44 -0
  47. data/test/tc_multiple_operation.rb +375 -0
  48. data/test/tc_operation_request.rb +64 -0
  49. data/test/tc_seller_listing_lookup.rb +47 -0
  50. data/test/tc_seller_listing_search.rb +55 -0
  51. data/test/tc_seller_lookup.rb +44 -0
  52. data/test/tc_serialisation.rb +107 -0
  53. data/test/tc_shopping_cart.rb +214 -0
  54. data/test/tc_similarity_lookup.rb +48 -0
  55. data/test/tc_tag_lookup.rb +24 -0
  56. data/test/tc_transaction_lookup.rb +24 -0
  57. data/test/tc_vehicle_operations.rb +118 -0
  58. data/test/ts_aws.rb +24 -0
  59. metadata +141 -0
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/ruby -w
2
+ #
3
+ # $Id: tag_lookup1,v 1.2 2010/02/20 16:49:14 ianmacd Exp $
4
+
5
+ require 'amazon/aws'
6
+ require 'amazon/aws/search'
7
+
8
+ include Amazon::AWS
9
+ include Amazon::AWS::Search
10
+
11
+ tag_str = 'Awful'
12
+ tl = TagLookup.new( tag_str )
13
+
14
+ # You can have multiple response groups.
15
+ #
16
+ tl.response_group = ResponseGroup.new( 'Tags', 'TagsSummary' )
17
+
18
+ req = Request.new
19
+ req.locale = 'us'
20
+
21
+ resp = req.search( tl )
22
+ tag = resp.tag_lookup_response.tags.tag
23
+
24
+ printf( "Tag name '%s' has %d distinct items.\n", tag_str, tag.distinct_items )
25
+ printf( "Tag has %d distinct items.\n", tag.distinct_users )
26
+ printf( "Tag has %d total usages.\n", tag.total_usages )
27
+ printf( "Tagged for the first time in entity %s on %s\nby %s..\n",
28
+ tag.first_tagging.entity_id,
29
+ tag.first_tagging.time,
30
+ tag.first_tagging.user_id )
31
+ printf( "Tagged for the last time in entity %s on %s\nby %s..\n",
32
+ tag.last_tagging.entity_id,
33
+ tag.last_tagging.time,
34
+ tag.last_tagging.user_id )
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/ruby -w
2
+ #
3
+ # $Id: transaction_lookup1,v 1.2 2010/02/20 16:49:14 ianmacd Exp $
4
+
5
+ require 'amazon/aws'
6
+ require 'amazon/aws/search'
7
+
8
+ include Amazon::AWS
9
+ include Amazon::AWS::Search
10
+
11
+ tl = TransactionLookup.new( '103-5663398-5028241' )
12
+ tl.response_group = ResponseGroup.new( 'TransactionDetails' )
13
+
14
+ req = Request.new
15
+ req.locale = 'us'
16
+
17
+ resp = req.search( tl )
18
+ trans = resp.transaction_lookup_response.transactions.transaction
19
+
20
+ printf( "Transaction date was %s.\n", trans.transaction_date )
21
+ printf( "It was in the amount of %s and the seller was %s.\n",
22
+ trans.totals.total.formatted_price, trans.seller_name )
23
+ printf( "The shipping charge was %s and the package was sent by %s.\n",
24
+ trans.totals.shipping_charge.formatted_price,
25
+ trans.shipments.shipment.delivery_method )
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/ruby -w
2
+ #
3
+ # $Id: vehicle_search,v 1.2 2010/02/20 16:49:14 ianmacd Exp $
4
+
5
+ require 'amazon/aws/search'
6
+
7
+ include Amazon::AWS
8
+ include Amazon::AWS::Search
9
+
10
+ vs = VehicleSearch.new( { 'Year' => 2008 } )
11
+ vs.response_group = ResponseGroup.new( 'VehicleMakes' )
12
+
13
+ req = Request.new
14
+ req.locale = 'us'
15
+
16
+ resp = req.search( vs )
17
+ makes = resp.vehicle_search_response[0].vehicle_years[0].vehicle_year[0].
18
+ vehicle_makes[0].vehicle_make
19
+
20
+ printf( "Search returned %d makes of vehicle for 2008.\n\n", makes.size )
21
+
22
+ makes.each { |make| puts make, '' }
@@ -0,0 +1,165 @@
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=nil)
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 = StringIO
83
+
84
+ else
85
+
86
+ # Perform the usual search for the system and user config files.
87
+ #
88
+ config_files = [ File.join( '', 'etc', 'amazonrc' ) ]
89
+
90
+ # Figure out where home is. The locations after HOME are for Windows.
91
+ # [ruby-core:12347]
92
+ #
93
+ hp = nil
94
+ if ENV.key?( 'HOMEDRIVE' ) && ENV.key?( 'HOMEPATH' )
95
+ hp = ENV['HOMEDRIVE'] + ENV['HOMEPATH']
96
+ end
97
+ home = ENV['AMAZONRCDIR'] || ENV['HOME'] || hp || ENV['USERPROFILE']
98
+
99
+ user_rcfile = ENV['AMAZONRCFILE'] || '.amazonrc'
100
+
101
+ if home
102
+ config_files << File.expand_path( File.join( home, user_rcfile ) )
103
+ end
104
+
105
+ config_class = File
106
+ end
107
+
108
+ config_files.each do |cf|
109
+
110
+ if config_class == StringIO
111
+ readable = true
112
+ else
113
+ # We must determine whether the file is readable.
114
+ #
115
+ readable = File.exists?( cf ) && File.readable?( cf )
116
+ end
117
+
118
+ if readable
119
+
120
+ Amazon.dprintf( 'Opening %s ...', cf ) if config_class == File
121
+
122
+ config_class.open( cf ) { |f| lines = f.readlines }.each do |line|
123
+ line.chomp!
124
+
125
+ # Skip comments and blank lines.
126
+ #
127
+ next if line =~ /^(#|$)/
128
+
129
+ Amazon.dprintf( 'Read: %s', line )
130
+
131
+ # Determine whether we're entering the subsection of a new locale.
132
+ #
133
+ if match = line.match( /^\[(\w+)\]$/ )
134
+ locale = match[1]
135
+ Amazon.dprintf( "Config locale is now '%s'.", locale )
136
+ next
137
+ end
138
+
139
+ # Store these, because we'll probably find a use for these later.
140
+ #
141
+ begin
142
+ match = line.match( /^\s*(\S+)\s*=\s*(['"]?)([^'"]+)(['"]?)/ )
143
+ key, begin_quote, val, end_quote = match[1, 4]
144
+ raise ConfigError if begin_quote != end_quote
145
+
146
+ rescue NoMethodError, ConfigError
147
+ raise ConfigError, "bad config line: #{line}"
148
+ end
149
+
150
+ if locale && locale != 'global'
151
+ self[locale] ||= {}
152
+ self[locale][key] = val
153
+ else
154
+ self[key] = val
155
+ end
156
+
157
+ end
158
+ end
159
+
160
+ end
161
+
162
+ end
163
+ end
164
+
165
+ end
@@ -0,0 +1,1493 @@
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
+ BrowseNodeLookup CustomerContentLookup CustomerContentSearch
604
+ Help ItemLookup ItemSearch
605
+ ListLookup ListSearch MultipleOperation
606
+ SellerListingLookup SellerListingSearch SellerLookup
607
+ SimilarityLookup TagLookup TransactionLookup
608
+ VehiclePartLookup VehiclePartSearch VehicleSearch
609
+
610
+ CartAdd CartClear CartCreate
611
+ CartGet CartModify
612
+ ]
613
+
614
+ attr_reader :kind
615
+ attr_accessor :params, :response_group
616
+
617
+ def initialize(parameters)
618
+
619
+ op_kind = self.class.to_s.sub( /^.*::/, '' )
620
+
621
+ raise "Bad operation: #{op_kind}" unless OPERATIONS.include?( op_kind )
622
+
623
+ if ResponseGroup::DEFAULT.key?( op_kind )
624
+ response_group =
625
+ ResponseGroup.new( ResponseGroup::DEFAULT[op_kind] )
626
+ else
627
+ response_group = nil
628
+ end
629
+
630
+ if op_kind =~ /^Cart/
631
+ @params = parameters
632
+ else
633
+ @params = Hash.new { |hash, key| hash[key] = [] }
634
+ @response_group = Hash.new { |hash, key| hash[key] = [] }
635
+
636
+ unless op_kind == 'MultipleOperation'
637
+ @params[op_kind] = [ parameters ]
638
+ @response_group[op_kind] = [ response_group ]
639
+ end
640
+ end
641
+
642
+ @kind = op_kind
643
+ end
644
+
645
+
646
+ # Make sure we can still get to the old @response_group= writer method.
647
+ #
648
+ alias :response_group_orig= :response_group=
649
+
650
+ # If the user assigns to @response_group, we need to set this response
651
+ # group for any and all operations that may have been batched.
652
+ #
653
+ def response_group=(rg) # :nodoc:
654
+ @params.each_value do |op_arr|
655
+ op_arr.each do |op|
656
+ op['ResponseGroup'] = rg
657
+ end
658
+ end
659
+ end
660
+
661
+
662
+ # Group together operations of the same class in a batch request.
663
+ # _operations_ should be either an operation of the same class as *self*
664
+ # or an array of such operations.
665
+ #
666
+ # If you need to batch operations of different classes, use a
667
+ # MultipleOperation instead.
668
+ #
669
+ # Example:
670
+ #
671
+ # is = ItemSearch.new( 'Books', { 'Title' => 'ruby programming' } )
672
+ # is2 = ItemSearch.new( 'Music', { 'Artist' => 'stranglers' } )
673
+ # is.response_group = ResponseGroup.new( :Small )
674
+ # is2.response_group = ResponseGroup.new( :Tracks )
675
+ # is.batch( is2 )
676
+ #
677
+ # Please see MultipleOperation.new for implementation details that also
678
+ # apply to batched operations.
679
+ #
680
+ def batch(*operations)
681
+
682
+ operations.flatten.each do |op|
683
+
684
+ unless self.class == op.class
685
+ raise BatchError, "You can't batch operations of different classes. Use class MultipleOperation."
686
+ end
687
+
688
+ # Add the operation's single element array containing the parameter
689
+ # hash to the array.
690
+ #
691
+ @params[op.kind].concat( op.params[op.kind] )
692
+
693
+ # Add the operation's response group array to the array.
694
+ #
695
+ @response_group[op.kind].concat( op.response_group[op.kind] )
696
+ end
697
+
698
+ end
699
+
700
+
701
+ # Return a hash of operation parameters and values, possibly converted to
702
+ # batch syntax, suitable for encoding in a query.
703
+ #
704
+ def query_parameters # :nodoc:
705
+ query = {}
706
+
707
+ @params.each do |op_kind, ops|
708
+
709
+ # If we have only one type of operation and only one operation of
710
+ # that type, return that one in non-batched syntax.
711
+ #
712
+ if @params.size == 1 && @params[op_kind].size == 1
713
+ return { 'Operation' => op_kind,
714
+ 'ResponseGroup' => @response_group[op_kind][0] }.
715
+ merge( @params[op_kind][0] )
716
+ end
717
+
718
+ # Otherwise, use batch syntax.
719
+ #
720
+ ops.each_with_index do |op, op_index|
721
+
722
+ # Make sure we use a response group of some kind.
723
+ #
724
+ shared = '%s.%d.ResponseGroup' % [ op_kind, op_index + 1 ]
725
+ query[shared] = op['ResponseGroup'] ||
726
+ ResponseGroup::DEFAULT[op_kind]
727
+
728
+ # Add all of the parameters to the query hash.
729
+ #
730
+ op.each do |k, v|
731
+ shared = '%s.%d.%s' % [ op_kind, op_index + 1, k ]
732
+ query[shared] = v
733
+ end
734
+ end
735
+ end
736
+
737
+ # Add the operation list.
738
+ #
739
+ { 'Operation' => @params.keys.join( ',' ) }.merge( query )
740
+ end
741
+
742
+ end
743
+
744
+
745
+ # This class can be used to encapsulate multiple operations in a single
746
+ # operation for greater efficiency.
747
+ #
748
+ class MultipleOperation < Operation
749
+
750
+ # This allows you to take multiple Operation objects and encapsulate them
751
+ # to form a single object, which can then be used to send a single
752
+ # request to AWS. This allows for greater efficiency, reducing the number
753
+ # of requests sent to AWS.
754
+ #
755
+ # AWS currently imposes a limit of two operations when encapsulating
756
+ # operations in a multiple operation. Note, however, that one or both of
757
+ # these operations may be a batched operation. Combining two batched
758
+ # operations in this way makes it possible to send as many as four
759
+ # simple operations to AWS in a single MultipleOperation request.
760
+ #
761
+ # _operations_ is an array of objects subclassed from Operation, such as
762
+ # ItemSearch, ItemLookup, etc.
763
+ #
764
+ # Please note the following implementation details:
765
+ #
766
+ # - As mentioned above, Amazon currently imposes a limit of two
767
+ # operations encapsulated in a MultipleOperation.
768
+ #
769
+ # - To use a different set of response groups for each encapsulated
770
+ # operation, assign to each operation's @response_group attribute prior
771
+ # to encapulation in a MultipleOperation.
772
+ #
773
+ # - To use the same set of response groups for all encapsulated
774
+ # operations, you can directly assign to the @response_group attribute
775
+ # of the MultipleOperation. This will propagate to the encapsulated
776
+ # operations.
777
+ #
778
+ # - One or both operations may have multiple results pages available,
779
+ # but only the first page will be returned by your requests. If you
780
+ # need subsequent pages, you must perform the operations separately.
781
+ # It is not possible to page through the results of a MultipleOperation
782
+ # response.
783
+ #
784
+ # - In this implementation, an error in any of the constituent operations
785
+ # will cause an exception to be thrown. If you don't want partial
786
+ # success (i.e. the success of fewer than all of the operations) to be
787
+ # treated as failure, you should perform the operations separately.
788
+ #
789
+ # - MultipleOperation is intended for encapsulation of objects from
790
+ # different classes, e.g. an ItemSearch and an ItemLookup. If you just
791
+ # want to batch operations of the same class, Operation#batch
792
+ # provides an alternative.
793
+ #
794
+ # In fact, if you create a MultipleOperation encapsulating objects of
795
+ # the same class, Ruby/AWS will actually apply simple batch syntax to
796
+ # your request, so it amounts to the same as using Operation#batch.
797
+ #
798
+ # - Although all of the encapsulated operations can be batched
799
+ # operations, Amazon places a limit of two on the number of same-class
800
+ # operations that can be carried out in any one request. This means
801
+ # that you cannot encapsulate two batched requests from the same
802
+ # class, so attempting, for example, four ItemLookup operations via
803
+ # two batched ItemLookup operations will not work.
804
+ #
805
+ # Example:
806
+ #
807
+ # is = ItemSearch.new( 'Books', { 'Title' => 'Ruby' } )
808
+ # il = ItemLookup.new( 'ASIN', { 'ItemId' => 'B0013DZAYO',
809
+ # 'MerchantId' => 'Amazon' } )
810
+ # is.response_group = ResponseGroup.new( :Large )
811
+ # il.response_group = ResponseGroup.new( :Small )
812
+ # mo = MultipleOperation.new( is, il )
813
+ #
814
+ def initialize(*operations)
815
+
816
+ # Start with an empty parameter hash.
817
+ #
818
+ super( {} )
819
+
820
+ # Start off with the first operation and duplicate the original's
821
+ # parameters to avoid accidental in-place modification.
822
+ #
823
+ operations.flatten!
824
+ @params = operations.shift.params.freeze.dup
825
+
826
+ # Add subsequent operations' parameter hashes, protecting them
827
+ # against accidental in-place modification.
828
+ #
829
+ operations.each do |op|
830
+ op.params.freeze.each do |op_kind, op_arr|
831
+ @params[op_kind].concat( op_arr )
832
+ end
833
+ end
834
+
835
+ end
836
+
837
+ end
838
+
839
+
840
+ # This class of operation aids in finding out about AWS operations and
841
+ # response groups.
842
+ #
843
+ class Help < Operation
844
+
845
+ # Return information on AWS operations and response groups.
846
+ #
847
+ # For operations, required and optional parameters are returned, along
848
+ # with information about which response groups the operation can use.
849
+ #
850
+ # For response groups, The list of operations that can use that group is
851
+ # returned, as well as the list of response tags returned by the group.
852
+ #
853
+ # _help_type_ is the type of object for which help is being sought, such
854
+ # as *Operation* or *ResponseGroup*. _about_ is the name of the
855
+ # operation or response group you need help with, and _parameters_ is an
856
+ # optional hash of parameters that further refine the request for help.
857
+ #
858
+ def initialize(help_type, about, parameters={})
859
+ super( { 'HelpType' => help_type,
860
+ 'About' => about
861
+ }.merge( parameters ) )
862
+ end
863
+
864
+ end
865
+
866
+
867
+ # This is the class for the most common type of AWS look-up, an
868
+ # ItemSearch. This allows you to search for items that match a set of
869
+ # broad criteria. It returns items for sale by Amazon merchants and most
870
+ # types of seller.
871
+ #
872
+ class ItemSearch < Operation
873
+
874
+ # Not all search indices work in all locales. It is the user's
875
+ # responsibility to ensure that a given index is valid within a given
876
+ # locale.
877
+ #
878
+ # According to the AWS documentation:
879
+ #
880
+ # - *All* searches through all indices.
881
+ # - *Blended* combines Apparel, Automotive, Books, DVD, Electronics,
882
+ # GourmetFood, Kitchen, Music, PCHardware, PetSupplies, Software,
883
+ # SoftwareVideoGames, SportingGoods, Tools, Toys, VHS and VideoGames.
884
+ # - *Merchants* combines all search indices for a merchant given with
885
+ # MerchantId.
886
+ # - *Music* combines the Classical, DigitalMusic, and MusicTracks
887
+ # indices.
888
+ # - *Video* combines the DVD and VHS search indices.
889
+ #
890
+ SEARCH_INDICES = %w[
891
+ All
892
+ Apparel
893
+ Automotive
894
+ Baby
895
+ Beauty
896
+ Blended
897
+ Books
898
+ Classical
899
+ DigitalMusic
900
+ DVD
901
+ Electronics
902
+ ForeignBooks
903
+ GourmetFood
904
+ Grocery
905
+ HealthPersonalCare
906
+ Hobbies
907
+ HomeGarden
908
+ HomeImprovement
909
+ Industrial
910
+ Jewelry
911
+ KindleStore
912
+ Kitchen
913
+ Lighting
914
+ Magazines
915
+ Merchants
916
+ Miscellaneous
917
+ MP3Downloads
918
+ Music
919
+ MusicalInstruments
920
+ MusicTracks
921
+ OfficeProducts
922
+ OutdoorLiving
923
+ Outlet
924
+ PCHardware
925
+ PetSupplies
926
+ Photo
927
+ Shoes
928
+ SilverMerchants
929
+ Software
930
+ SoftwareVideoGames
931
+ SportingGoods
932
+ Tools
933
+ Toys
934
+ UnboxVideo
935
+ VHS
936
+ Video
937
+ VideoGames
938
+ Watches
939
+ Wireless
940
+ WirelessAccessories
941
+ ]
942
+
943
+
944
+ # Search AWS for items. _search_index_ must be one of _SEARCH_INDICES_
945
+ # and _parameters_ is an optional hash of parameters that further refine
946
+ # the scope of the search.
947
+ #
948
+ # Example:
949
+ #
950
+ # is = ItemSearch.new( 'Books', { 'Title' => 'ruby programming' } )
951
+ #
952
+ # In the above example, we search for books with <b>Ruby Programming</b>
953
+ # in the title.
954
+ #
955
+ def initialize(search_index, parameters)
956
+ unless SEARCH_INDICES.include? search_index.to_s
957
+ raise "Invalid search index: #{search_index}"
958
+ end
959
+
960
+ super( { 'SearchIndex' => search_index }.merge( parameters ) )
961
+ end
962
+
963
+ end
964
+
965
+
966
+ # This class of look-up deals with searching for *specific* items by some
967
+ # uniquely identifying attribute, such as the ASIN (*A*mazon *S*tandard
968
+ # *I*tem *N*umber).
969
+ #
970
+ class ItemLookup < Operation
971
+
972
+ # Look up a specific item in the AWS catalogue. _id_type_ is the type of
973
+ # identifier and _parameters_ is a hash that identifies the item to be
974
+ # located and narrows the scope of the search.
975
+ #
976
+ # Example:
977
+ #
978
+ # il = ItemLookup.new( 'ASIN', { 'ItemId' => 'B000AE4QEC'
979
+ # 'MerchantId' => 'Amazon' } )
980
+ #
981
+ # In the above example, we search for an item, based on its ASIN. The
982
+ # use of _MerchantId_ restricts the offers returned to those for sale
983
+ # by Amazon (as opposed to third-party sellers).
984
+ #
985
+ def initialize(id_type, parameters)
986
+ super( { 'IdType' => id_type }.merge( parameters ) )
987
+ end
988
+
989
+ end
990
+
991
+
992
+ # Search for items for sale by a particular seller.
993
+ #
994
+ class SellerListingSearch < Operation
995
+
996
+ # Search for items for sale by a particular seller. _seller_id_ is the
997
+ # Amazon seller ID and _parameters_ is an optional hash of parameters
998
+ # that further refine the scope of the search.
999
+ #
1000
+ # Example:
1001
+ #
1002
+ # sls = SellerListingSearch.new( 'A33J388YD2MWJZ',
1003
+ # { 'Keywords' => 'Killing Joke' } )
1004
+ #
1005
+ # In the above example, we search seller <b>A33J388YD2MWJ</b>'s listings
1006
+ # for items with the keywords <b>Killing Joke</b>.
1007
+ #
1008
+ def initialize(seller_id, parameters)
1009
+ super( { 'SellerId' => seller_id }.merge( parameters ) )
1010
+ end
1011
+
1012
+ end
1013
+
1014
+
1015
+ # Return specified items in a seller's store.
1016
+ #
1017
+ class SellerListingLookup < ItemLookup
1018
+
1019
+ # Look up a specific item for sale by a specific seller. _id_type_ is
1020
+ # the type of identifier and _parameters_ is a hash that identifies the
1021
+ # item to be located and narrows the scope of the search.
1022
+ #
1023
+ # Example:
1024
+ #
1025
+ # sll = SellerListingLookup.new( 'AP8U6Y3PYQ9VO', 'ASIN',
1026
+ # { 'Id' => 'B0009RRRC8' } )
1027
+ #
1028
+ # In the above example, we search seller <b>AP8U6Y3PYQ9VO</b>'s listings
1029
+ # to find items for sale with the ASIN <b>B0009RRRC8</b>.
1030
+ #
1031
+ def initialize(seller_id, id_type, parameters)
1032
+ super( id_type, { 'SellerId' => seller_id }.merge( parameters ) )
1033
+ end
1034
+
1035
+ end
1036
+
1037
+
1038
+ # Return information about a specific seller.
1039
+ #
1040
+ class SellerLookup < Operation
1041
+
1042
+ # Search for the details of a specific seller. _seller_id_ is the Amazon
1043
+ # ID of the seller in question and _parameters_ is an optional hash of
1044
+ # parameters that further refine the scope of the search.
1045
+ #
1046
+ # Example:
1047
+ #
1048
+ # sl = SellerLookup.new( 'A3QFR0K2KCB7EG' )
1049
+ #
1050
+ # In the above example, we look up the details of the seller with ID
1051
+ # <b>A3QFR0K2KCB7EG</b>.
1052
+ #
1053
+ def initialize(seller_id, parameters={})
1054
+ super( { 'SellerId' => seller_id }.merge( parameters ) )
1055
+ end
1056
+
1057
+ end
1058
+
1059
+
1060
+ # Obtain the information an Amazon customer has made public about
1061
+ # themselves.
1062
+ #
1063
+ class CustomerContentLookup < Operation
1064
+
1065
+ # Search for public customer data. _customer_id_ is the unique ID
1066
+ # identifying the customer on Amazon and _parameters_ is an optional
1067
+ # hash of parameters that further refine the scope of the search.
1068
+ #
1069
+ # Example:
1070
+ #
1071
+ # ccl = CustomerContentLookup.new( 'AJDWXANG1SYZP' )
1072
+ #
1073
+ # In the above example, we look up public data about the customer with
1074
+ # the ID <b>AJDWXANG1SYZP</b>.
1075
+ #
1076
+ def initialize(customer_id, parameters={})
1077
+ super( { 'CustomerId' => customer_id }.merge( parameters ) )
1078
+ end
1079
+
1080
+ end
1081
+
1082
+
1083
+ # Retrieve basic Amazon customer data.
1084
+ #
1085
+ class CustomerContentSearch < Operation
1086
+
1087
+ # Retrieve customer information, using an e-mail address or name.
1088
+ #
1089
+ # If _customer_id_ contains an '@' sign, it is assumed to be an e-mail
1090
+ # address. Otherwise, it is assumed to be the customer's name.
1091
+ #
1092
+ # Example:
1093
+ #
1094
+ # ccs = CustomerContentSearch.new( 'ian@caliban.org' )
1095
+ #
1096
+ # In the above example, we look up customer information about
1097
+ # <b>ian@caliban.org</b>. The *CustomerInfo* response group will return,
1098
+ # amongst other things, a _customer_id_ property, which can then be
1099
+ # plugged into CustomerContentLookup to retrieve more detailed customer
1100
+ # information.
1101
+ #
1102
+ def initialize(customer_id)
1103
+ id = customer_id =~ /@/ ? 'Email' : 'Name'
1104
+ super( { id => customer_id } )
1105
+ end
1106
+
1107
+ end
1108
+
1109
+
1110
+ # Find wishlists, registry lists, etc. created by users and placed on
1111
+ # Amazon. These are items that customers would like to receive as
1112
+ # presnets.
1113
+ #
1114
+ class ListSearch < Operation
1115
+
1116
+ # Search for Amazon lists. _list_type_ is the type of list to search for
1117
+ # and _parameters_ is an optional hash of parameters that narrow the
1118
+ # scope of the search.
1119
+ #
1120
+ # Example:
1121
+ #
1122
+ # ls = ListSearch.new( 'WishList', { 'Name' => 'Peter Duff' }
1123
+ #
1124
+ # In the above example, we retrieve the wishlist for the Amazon user,
1125
+ # <b>Peter Duff</b>.
1126
+ #
1127
+ def initialize(list_type, parameters)
1128
+ super( { 'ListType' => list_type }.merge( parameters ) )
1129
+ end
1130
+
1131
+ end
1132
+
1133
+
1134
+ # Find the details of specific wishlists, registries, etc.
1135
+ #
1136
+ class ListLookup < Operation
1137
+
1138
+ # Look up and return details about a specific list. _list_id_ is the
1139
+ # Amazon list ID, _list_type_ is the type of list and _parameters_ is an
1140
+ # optional hash of parameters that narrow the scope of the search.
1141
+ #
1142
+ # Example:
1143
+ #
1144
+ # ll = ListLookup.new( '3P722DU4KUPCP', 'Listmania' )
1145
+ #
1146
+ # In the above example, a *Listmania* list with the ID
1147
+ # <b>3P722DU4KUPCP</b> is retrieved from AWS.
1148
+ #
1149
+ def initialize(list_id, list_type, parameters={})
1150
+ super( { 'ListId' => list_id,
1151
+ 'ListType' => list_type
1152
+ }.merge( parameters ) )
1153
+ end
1154
+
1155
+ end
1156
+
1157
+
1158
+ # Amazon use browse nodes as a means of organising the millions of items
1159
+ # in their inventory. An example might be *Carving Knives*. Looking up a
1160
+ # browse node enables you to determine that group's ancestors and
1161
+ # descendants.
1162
+ #
1163
+ class BrowseNodeLookup < Operation
1164
+
1165
+ # Look up and return the details of an Amazon browse node. _node_ is the
1166
+ # browse node to look up and _parameters_ is an optional hash of
1167
+ # parameters that further refine the scope of the search. _parameters_
1168
+ # is currently unused.
1169
+ #
1170
+ # Example:
1171
+ #
1172
+ # bnl = BrowseNodeLookup.new( '11232', {} )
1173
+ #
1174
+ # In the above example, we look up the browse node with the ID
1175
+ # <b>11232</b>. This is the <b>Social Sciences</b> browse node.
1176
+ #
1177
+ def initialize(node, parameters={})
1178
+ super( { 'BrowseNodeId' => node }.merge( parameters ) )
1179
+ end
1180
+
1181
+ end
1182
+
1183
+
1184
+ # Similarity look-up is for items similar to others.
1185
+ #
1186
+ class SimilarityLookup < Operation
1187
+
1188
+ # Look up items similar to _asin_, which can be a single item or an
1189
+ # array. _parameters_ is an optional hash of parameters that further
1190
+ # refine the scope of the search.
1191
+ #
1192
+ # Example:
1193
+ #
1194
+ # sl = SimilarityLookup.new( 'B000051WBE' )
1195
+ #
1196
+ # In the above example, we search for items similar to the one with ASIN
1197
+ # <b>B000051WBE</b>.
1198
+ #
1199
+ def initialize(asin, parameters={})
1200
+ super( { 'ItemId' => asin.to_a.join( ',' ) }.merge( parameters ) )
1201
+ end
1202
+
1203
+ end
1204
+
1205
+
1206
+ # Search for entities based on user-defined tags. A tag is a descriptive
1207
+ # word that a customer uses to label entities on Amazon's Web site.
1208
+ # Entities can be items for sale, Listmania lists, guides, etc.
1209
+ #
1210
+ class TagLookup < Operation
1211
+
1212
+ # Look up entities based on user-defined tags. _tag_name_ is the tag to
1213
+ # search on and _parameters_ is an optional hash of parameters that
1214
+ # further refine the scope of the search.
1215
+ #
1216
+ # Example:
1217
+ #
1218
+ # tl = TagLookup.new( 'Awful' )
1219
+ #
1220
+ # In the example above, we search for entities tagged by users with the
1221
+ # word *Awful*.
1222
+ #
1223
+ def initialize(tag_name, parameters={})
1224
+ super( { 'TagName' => tag_name }.merge( parameters ) )
1225
+ end
1226
+
1227
+ end
1228
+
1229
+
1230
+ # Search for information on previously completed purchases.
1231
+ #
1232
+ class TransactionLookup < Operation
1233
+
1234
+ # Return information on an already completed purchase. _transaction_id_
1235
+ # is actually the order number that is created when you place an order
1236
+ # on Amazon.
1237
+ #
1238
+ # Example:
1239
+ #
1240
+ # tl = TransactionLookup.new( '103-5663398-5028241' )
1241
+ #
1242
+ # In the above example, we retrieve the details of order number
1243
+ # <b>103-5663398-5028241</b>.
1244
+ #
1245
+ def initialize(transaction_id)
1246
+ super( { 'TransactionId' => transaction_id } )
1247
+ end
1248
+
1249
+ end
1250
+
1251
+
1252
+ # Look up individual vehicle parts.
1253
+ #
1254
+ class VehiclePartLookup < Operation
1255
+
1256
+ # Look up a particular vehicle part. _item_id_ is the ASIN of the part
1257
+ # in question and _parameters_ is an optional hash of parameters that
1258
+ # further refine the scope of the search.
1259
+ #
1260
+ # Although the _item_id_ alone is enough to locate the part, providing
1261
+ # _parameters_ can be useful in determining whether the part looked up
1262
+ # is a fit for a particular vehicle type, as with the *VehiclePartFit*
1263
+ # response group.
1264
+ #
1265
+ # Example:
1266
+ #
1267
+ # vpl = VehiclePartLookup.new( 'B000C1ZLI8',
1268
+ # { 'Year' => 2008,
1269
+ # 'MakeId' => 73,
1270
+ # 'ModelId' => 6039,
1271
+ # 'TrimId' => 20 } )
1272
+ #
1273
+ # Here, we search for a <b>2008</b> model *Audi* <b>R8</b> with *Base*
1274
+ # trim. The required Ids can be found using VehiclePartSearch.
1275
+ #
1276
+ def initialize(item_id, parameters={})
1277
+ super( { 'ItemId' => item_id }.merge( parameters ) )
1278
+ end
1279
+
1280
+ end
1281
+
1282
+
1283
+ # Search for parts for a given vehicle.
1284
+ #
1285
+ class VehiclePartSearch < Operation
1286
+
1287
+ # Find parts for a given _year_, _make_id_ and _model_id_ of vehicle.
1288
+ # _parameters_ is an optional hash of parameters that further refine the
1289
+ # scope of the search.
1290
+ #
1291
+ # Example:
1292
+ #
1293
+ # vps = VehiclePartSearch.new( 2008, 73, 6039,
1294
+ # { 'TrimId' => 20,
1295
+ # 'EngineId' => 8914 } )
1296
+ #
1297
+ # In this example, we look for parts that will fit a <b>2008</b> model
1298
+ # *Audi* <b>R8</b> with *Base* trim and a <b>4.2L V8 Gas DOHC
1299
+ # Distributorless Naturally Aspirated Bosch Motronic Electronic FI
1300
+ # MFI</b> engine.
1301
+ #
1302
+ # Note that pagination of VehiclePartSearch results is not currently
1303
+ # supported.
1304
+ #
1305
+ # Use VehicleSearch to learn the MakeId and ModelId of the vehicle in
1306
+ # which you are interested.
1307
+ #
1308
+ def initialize(year, make_id, model_id, parameters={})
1309
+ super( { 'Year' => year,
1310
+ 'MakeId' => make_id,
1311
+ 'ModelId' => model_id }.merge( parameters ) )
1312
+ end
1313
+
1314
+ end
1315
+
1316
+
1317
+ # Search for vehicles.
1318
+ #
1319
+ class VehicleSearch < Operation
1320
+
1321
+ # Search for vehicles, based on one or more of the following
1322
+ # _parameters_: Year, MakeId, ModelId and TrimId.
1323
+ #
1324
+ # This method is best used iteratively. For example, first search on
1325
+ # year with a response group of *VehicleMakes* to return all makes for
1326
+ # that year.
1327
+ #
1328
+ # Next, search on year and make with a response group of *VehicleModels*
1329
+ # to find all models for that year and make.
1330
+ #
1331
+ # Then, search on year, make and model with a response group of
1332
+ # *VehicleTrims* to find all trim packages for that year, make and model.
1333
+ #
1334
+ # Finally, if required, search on year, make, model and trim package
1335
+ # with a response group of *VehicleOptions* to find all vehicle options
1336
+ # for that year, make, model and trim package.
1337
+ #
1338
+ # Example:
1339
+ #
1340
+ # vs = VehicleSearch.new( { 'Year' => 2008,
1341
+ # 'MakeId' => 20,
1342
+ # 'ModelId' => 6039,
1343
+ # 'TrimId' => 20 } )
1344
+ #
1345
+ # In this example, we search for <b>2008 Audi R8</b> vehicles with a
1346
+ # *Base* trim package. Used with the *VehicleOptions* response group,
1347
+ # a list of vehicle options would be returned.
1348
+ #
1349
+ def initialize(parameters={})
1350
+ super
1351
+ end
1352
+
1353
+ end
1354
+
1355
+ # Response groups determine which data pertaining to the item(s) being
1356
+ # sought is returned. They strongly influence the amount of data returned,
1357
+ # so you should always use the smallest response group(s) containing the
1358
+ # data of interest to you, to avoid masses of unnecessary data being
1359
+ # returned.
1360
+ #
1361
+ class ResponseGroup
1362
+
1363
+ # The default type of response group to use with each type of operation.
1364
+ #
1365
+ DEFAULT = { 'BrowseNodeLookup' => [ :BrowseNodeInfo, :TopSellers ],
1366
+ 'CustomerContentLookup' => [ :CustomerInfo, :CustomerLists ],
1367
+ 'CustomerContentSearch' => :CustomerInfo,
1368
+ 'Help' => :Help,
1369
+ 'ItemLookup' => :Large,
1370
+ 'ItemSearch' => :Large,
1371
+ 'ListLookup' => [ :ListInfo, :Small ],
1372
+ 'ListSearch' => :ListInfo,
1373
+ 'SellerListingLookup' => :SellerListing,
1374
+ 'SellerListingSearch' => :SellerListing,
1375
+ 'SellerLookup' => :Seller,
1376
+ 'SimilarityLookup' => :Large,
1377
+ 'TagLookup' => [ :Tags, :TagsSummary ],
1378
+ 'TransactionLookup' => :TransactionDetails,
1379
+ 'VehiclePartLookup' => :VehiclePartFit,
1380
+ 'VehiclePartSearch' => :VehicleParts,
1381
+ 'VehicleSearch' => :VehicleMakes
1382
+ }
1383
+
1384
+ # Define a set of one or more response groups to be applied to items
1385
+ # retrieved by an AWS operation.
1386
+ #
1387
+ # Example:
1388
+ #
1389
+ # rg = ResponseGroup.new( 'Medium', 'Offers', 'Reviews' )
1390
+ #
1391
+ def initialize(*rg)
1392
+ @list = rg.join( ',' )
1393
+ end
1394
+
1395
+
1396
+ # We need a form we can interpolate into query strings.
1397
+ #
1398
+ def to_s # :nodoc:
1399
+ @list
1400
+ end
1401
+
1402
+ end
1403
+
1404
+
1405
+ # All dynamically generated exceptions occur within this namespace.
1406
+ #
1407
+ module Error
1408
+
1409
+ # The base exception class for errors that result from AWS operations.
1410
+ # Classes for these are dynamically generated as subclasses of this one.
1411
+ #
1412
+ class AWSError < AmazonError; end
1413
+
1414
+ def Error.exception(xml)
1415
+ err_class = xml.elements['Code'].text.sub( /^AWS.*\./, '' )
1416
+ err_msg = xml.elements['Message'].text
1417
+
1418
+ # Dynamically define a new exception class for this class of error,
1419
+ # unless it already exists.
1420
+ #
1421
+ # Note that Ruby 1.9's Module.const_defined? needs a second parameter
1422
+ # of *false*, or it will also search AWSError's ancestors.
1423
+ #
1424
+ cd_params = [ err_class ]
1425
+ cd_params << false if RUBY_VERSION >= '1.9.0'
1426
+
1427
+ unless Amazon::AWS::Error.const_defined?( *cd_params )
1428
+ Amazon::AWS::Error.const_set( err_class, Class.new( AWSError ) )
1429
+ end
1430
+
1431
+ # Generate and return a new exception from the relevant class.
1432
+ #
1433
+ Amazon::AWS::Error.const_get( err_class ).new( err_msg )
1434
+ end
1435
+
1436
+ end
1437
+
1438
+
1439
+ # Create a shorthand module method for each of the AWS operations. These
1440
+ # can be used to create less verbose code at the expense of flexibility.
1441
+ #
1442
+ # For example, we might normally write the following code:
1443
+ #
1444
+ # is = ItemSearch.new( 'Books', { 'Title' => 'Ruby' } )
1445
+ # rg = ResponseGroup.new( 'Large' )
1446
+ # req = Request.new
1447
+ # response = req.search( is, rg )
1448
+ #
1449
+ # but we could instead use ItemSearch's associated module method as
1450
+ # follows:
1451
+ #
1452
+ # response = Amazon::AWS.item_search( 'Books', { 'Title' => 'Ruby' } )
1453
+ #
1454
+ # Note that these equivalent module methods all attempt to use the *Large*
1455
+ # response group, which may or may not work. If an
1456
+ # Amazon::AWS::Error::InvalidResponseGroup is raised, we will scan the
1457
+ # text of the error message returned by AWS to try to glean a valid
1458
+ # response group and then retry the operation using that instead.
1459
+
1460
+
1461
+ # Obtain a list of all subclasses of the Operation class.
1462
+ #
1463
+ classes =
1464
+ ObjectSpace.enum_for( :each_object, class << Operation; self; end ).to_a
1465
+
1466
+ classes.each do |cl|
1467
+ # Convert class name to Ruby case, e.g. ItemSearch => item_search.
1468
+ #
1469
+ class_name = cl.to_s.sub( /^.+::/, '' )
1470
+ uncamelised_name = Amazon.uncamelise( class_name )
1471
+
1472
+ # Define the module method counterpart of each operation.
1473
+ #
1474
+ module_eval %Q(
1475
+ def AWS.#{uncamelised_name}(*params)
1476
+ # Instantiate an object of the desired operational class.
1477
+ #
1478
+ op = #{cl.to_s}.new( *params )
1479
+
1480
+ # Attempt a search for the given operation using its default
1481
+ # response group types.
1482
+ #
1483
+ results = Search::Request.new.search( op )
1484
+ yield results if block_given?
1485
+ return results
1486
+
1487
+ end
1488
+ )
1489
+ end
1490
+
1491
+ end
1492
+
1493
+ end