papercavalier-ruby-aaws 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
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