alandipert-ruby-aaws 0.7.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 (60) hide show
  1. data/COPYING +340 -0
  2. data/INSTALL +260 -0
  3. data/NEWS +710 -0
  4. data/README +653 -0
  5. data/README.rdoc +145 -0
  6. data/Rakefile +35 -0
  7. data/VERSION +1 -0
  8. data/example/batch_operation +27 -0
  9. data/example/browse_node_lookup1 +46 -0
  10. data/example/customer_content_lookup1 +27 -0
  11. data/example/customer_content_search1 +21 -0
  12. data/example/example1 +87 -0
  13. data/example/help1 +25 -0
  14. data/example/item_lookup1 +56 -0
  15. data/example/item_lookup2 +56 -0
  16. data/example/item_search1 +30 -0
  17. data/example/item_search2 +37 -0
  18. data/example/item_search3 +23 -0
  19. data/example/list_lookup1 +29 -0
  20. data/example/list_search1 +30 -0
  21. data/example/multiple_operation1 +68 -0
  22. data/example/seller_listing_lookup1 +30 -0
  23. data/example/seller_listing_search1 +28 -0
  24. data/example/seller_lookup1 +45 -0
  25. data/example/shopping_cart1 +42 -0
  26. data/example/similarity_lookup1 +48 -0
  27. data/example/tag_lookup1 +34 -0
  28. data/example/transaction_lookup1 +26 -0
  29. data/example/vehicle_search +22 -0
  30. data/lib/amazon/aws/cache.rb +141 -0
  31. data/lib/amazon/aws/search.rb +342 -0
  32. data/lib/amazon/aws/shoppingcart.rb +504 -0
  33. data/lib/amazon/aws.rb +1217 -0
  34. data/lib/amazon/locale.rb +102 -0
  35. data/lib/amazon.rb +145 -0
  36. data/ruby-aaws.gemspec +117 -0
  37. data/setup.rb +1306 -0
  38. data/test/setup.rb +34 -0
  39. data/test/tc_amazon.rb +20 -0
  40. data/test/tc_aws.rb +151 -0
  41. data/test/tc_browse_node_lookup.rb +62 -0
  42. data/test/tc_customer_content_lookup.rb +64 -0
  43. data/test/tc_help.rb +60 -0
  44. data/test/tc_item_lookup.rb +60 -0
  45. data/test/tc_item_search.rb +106 -0
  46. data/test/tc_list_lookup.rb +55 -0
  47. data/test/tc_list_search.rb +55 -0
  48. data/test/tc_multiple_operation.rb +265 -0
  49. data/test/tc_operation_request.rb +58 -0
  50. data/test/tc_seller_listing_lookup.rb +58 -0
  51. data/test/tc_seller_listing_search.rb +70 -0
  52. data/test/tc_seller_lookup.rb +54 -0
  53. data/test/tc_serialisation.rb +103 -0
  54. data/test/tc_shopping_cart.rb +214 -0
  55. data/test/tc_similarity_lookup.rb +59 -0
  56. data/test/tc_tag_lookup.rb +35 -0
  57. data/test/tc_transaction_lookup.rb +35 -0
  58. data/test/tc_vehicle_operations.rb +106 -0
  59. data/test/ts_aws.rb +24 -0
  60. metadata +135 -0
data/lib/amazon/aws.rb ADDED
@@ -0,0 +1,1217 @@
1
+ # $Id: aws.rb,v 1.72 2008/10/03 09:37:25 ianmacd Exp $
2
+ #
3
+ #:include: ../../README.rdoc
4
+
5
+
6
+ module HMAC
7
+ IPAD = "\x36" * 64
8
+ OPAD = "\x5c" * 64
9
+
10
+ module_function
11
+
12
+ def sha256( key, message )
13
+ ikey = IPAD.dup
14
+ okey = OPAD.dup
15
+ key.size.times do |i|
16
+ ikey[i] = key[i] ^ IPAD[i]
17
+ okey[i] = key[i] ^ OPAD[i]
18
+ end
19
+
20
+ value = Digest::SHA256.digest( ikey + message )
21
+ value = Digest::SHA256.digest( okey + value )
22
+ end
23
+ end
24
+
25
+
26
+ module Amazon
27
+
28
+ module AWS
29
+
30
+ require 'uri'
31
+ require 'amazon'
32
+ require 'amazon/aws/cache'
33
+ require 'rexml/document'
34
+
35
+ require 'base64' # mod netjungle
36
+
37
+ NAME = '%s/%s' % [ Amazon::NAME, 'AWS' ]
38
+ VERSION = '0.4.4'
39
+ USER_AGENT = '%s %s' % [ NAME, VERSION ]
40
+
41
+ # Default Associate tags to use per locale.
42
+ #
43
+ DEF_ASSOC = {
44
+ 'de' => 'magistrix-21'
45
+ }
46
+
47
+ # Service name and version for AWS.
48
+ #
49
+ SERVICE = { 'Service' => 'AWSECommerceService',
50
+ 'Version' => '2008-08-19'
51
+ }
52
+
53
+ # Maximum number of 301 and 302 HTTP responses to follow, should Amazon
54
+ # later decide to change the location of the service.
55
+ #
56
+ MAX_REDIRECTS = 3
57
+
58
+ # Maximum number of results pages that can be retrieved for a given
59
+ # search operation, using whichever pagination parameter is relevant to
60
+ # that type of operation.
61
+ #
62
+ PAGINATION = {
63
+ 'ItemSearch' => { 'parameter' => 'ItemPage',
64
+ 'max_page' => 400 },
65
+ 'ItemLookup' => { 'paraneter' => 'OfferPage',
66
+ 'max_page' => 100 },
67
+ 'ListLookup' => { 'parameter' => 'ProductPage',
68
+ 'max_page' => 30 },
69
+ 'ListSearch' => { 'parameter' => 'ListPage',
70
+ 'max_page' => 20 },
71
+ 'CustomerContentLookup' => { 'parameter' => 'ReviewPage',
72
+ 'max_page' => 10 },
73
+ 'CustomerContentSearch' => { 'parameter' => 'CustomerPage',
74
+ 'max_page' => 20 }
75
+ }
76
+ # N.B. ItemLookup can also use the following two pagination parameters
77
+ #
78
+ # max. page
79
+ # ---------
80
+ # VariationPage 150
81
+ # ReviewPage 20
82
+
83
+ # Exception class for HTTP errors.
84
+ #
85
+ class HTTPError < AmazonError; end
86
+
87
+ class Endpoint
88
+
89
+ attr_reader :host, :path
90
+
91
+ def initialize(endpoint)
92
+ uri = URI.parse( endpoint )
93
+ @host = uri.host
94
+ @path = uri.path
95
+ end
96
+ end
97
+
98
+ ENDPOINT = {
99
+ 'ca' => Endpoint.new( 'http://ecs.amazonaws.ca/onca/xml' ),
100
+ 'de' => Endpoint.new( 'http://ecs.amazonaws.de/onca/xml' ),
101
+ 'fr' => Endpoint.new( 'http://ecs.amazonaws.fr/onca/xml' ),
102
+ 'jp' => Endpoint.new( 'http://ecs.amazonaws.jp/onca/xml' ),
103
+ 'uk' => Endpoint.new( 'http://ecs.amazonaws.co.uk/onca/xml' ),
104
+ 'us' => Endpoint.new( 'http://ecs.amazonaws.com/onca/xml' )
105
+ }
106
+
107
+
108
+ def self.signature_for_request(request, query, method = 'GET' )
109
+ endpoint = ENDPOINT[request.locale]
110
+ host = endpoint.host
111
+ uri = endpoint.path
112
+ query.gsub!('?', '')
113
+ raw_signature = "#{method}\n#{host}\n#{uri}\n#{query}"
114
+ hash = HMAC::sha256(request.secret_id, raw_signature)
115
+ signature = Base64.encode64(hash).chomp
116
+ Amazon.rawurlencode signature
117
+ end
118
+
119
+ # Fetch a page, either from the cache or by HTTP. This is used internally.
120
+ #
121
+ def AWS.get_page(request, query) # :nodoc:
122
+
123
+ url = ENDPOINT[request.locale].path + query
124
+ cache_url = ENDPOINT[request.locale].host + url
125
+
126
+
127
+ # Check for cached page and return that if it's there.
128
+ #
129
+ if request.cache && request.cache.cached?( cache_url )
130
+ body = request.cache.fetch( cache_url )
131
+ return body if body
132
+ end
133
+
134
+ # Get the existing connection. If there isn't one, force a new one.
135
+ #
136
+ conn = request.conn || request.reconnect.conn
137
+ user_agent = request.user_agent
138
+
139
+
140
+
141
+ begin
142
+ url = ENDPOINT[request.locale].path + query + "&Signature=" + AWS.signature_for_request(request, query)
143
+
144
+
145
+
146
+ Amazon.dprintf( 'Fetching http://%s%s ...', conn.address, url )
147
+
148
+
149
+ response = conn.get( url, { 'user-agent' => user_agent } )
150
+
151
+ # If we've pulled and processed a lot of pages from the cache (or
152
+ # just not passed by here recently), the HTTP connection to the server
153
+ # will probably have timed out.
154
+ #
155
+ rescue Errno::ECONNRESET
156
+ conn = request.reconnect.conn
157
+ retry
158
+ end
159
+
160
+ redirects = 0
161
+ while response.key? 'location'
162
+ if ( redirects += 1 ) > MAX_REDIRECTS
163
+ raise HTTPError, "More than #{MAX_REDIRECTS} redirections"
164
+ end
165
+
166
+ old_url = url
167
+ url = URI.parse( response['location'] )
168
+ url.scheme = old_url.scheme unless url.scheme
169
+ url.host = old_url.host unless url.host
170
+ Amazon.dprintf( 'Following HTTP %s to %s ...', response.code, url )
171
+ response = Net::HTTP::start( url.host ).
172
+ get( url.path, { 'user-agent' => user_agent } )
173
+ end
174
+
175
+ if response.code != '200'
176
+ raise HTTPError, "HTTP response code #{response.code}"
177
+ end
178
+
179
+ # Cache the page if we're using a cache.
180
+ #
181
+ if request.cache
182
+ request.cache.store( cache_url, response.body )
183
+ end
184
+
185
+ response.body
186
+ end
187
+
188
+
189
+ def AWS.assemble_query(items) # :nodoc:
190
+ query = ''
191
+
192
+ # the new signature also needs a timestamp / mod netjungle
193
+
194
+ items[:Timestamp] = DateTime.now.new_offset.strftime('%Y-%m-%dT%XZ')
195
+
196
+ # We must sort the items into an array to get reproducible ordering
197
+ # of the query parameters. Otherwise, URL caching would not work. We
198
+ # must also convert the keys to strings, in case Symbols have been used
199
+ # as the keys.
200
+ #
201
+ items.sort { |a,b| a.to_s <=> b.to_s }.each do |k, v|
202
+ query << '&%s=%s' % [ k, Amazon.url_encode( v.to_s ) ]
203
+ end
204
+
205
+ # Replace initial ampersand with question-mark.
206
+ #
207
+ query[0] = '?'
208
+
209
+ query
210
+ end
211
+
212
+ # Everything returned by AWS is an AWSObject.
213
+ #
214
+ class AWSObject
215
+
216
+ include REXML
217
+
218
+ # This method can be used to load AWSObject data previously serialised
219
+ # by Marshal.dump.
220
+ #
221
+ # Example:
222
+ #
223
+ # File.open( 'aws.dat' ) { |f| Amazon::AWS::AWSObject.load( f ) }
224
+ #
225
+ # Marshal.load cannot be used directly, because subclasses of AWSObject
226
+ # are dynamically defined as needed when AWS XML responses are parsed.
227
+ #
228
+ # Later attempts to load objects instantiated from these classes cause a
229
+ # problem for Marshal, because it knows nothing of classes that were
230
+ # dynamically defined by a separate process.
231
+ #
232
+ def AWSObject.load(io)
233
+ begin
234
+ Marshal.load( io )
235
+ rescue ArgumentError => ex
236
+ m = ex.to_s.match( /Amazon::AWS::AWSObject::([^ ]+)/ )
237
+ const_set( m[1], Class.new( AWSObject ) )
238
+
239
+ io.rewind
240
+ retry
241
+ end
242
+ end
243
+
244
+
245
+ # This method can be used to load AWSObject data previously serialised
246
+ # by YAML.dump.
247
+ #
248
+ # Example:
249
+ #
250
+ # File.open( 'aws.yaml' ) { |f| Amazon::AWS::AWSObject.yaml_load( f ) }
251
+ #
252
+ # The standard YAML.load cannot be used directly, because subclasses of
253
+ # AWSObject are dynamically defined as needed when AWS XML responses are
254
+ # parsed.
255
+ #
256
+ # Later attempts to load objects instantiated from these classes cause a
257
+ # problem for YAML, because it knows nothing of classes that were
258
+ # dynamically defined by a separate process.
259
+ #
260
+ def AWSObject.yaml_load(io)
261
+ io.each do |line|
262
+
263
+ # File data is external, so it's deemed unsafe when $SAFE > 0, which
264
+ # is the case with mod_ruby, for example, where $SAFE == 1.
265
+ #
266
+ # YAML data isn't eval'ed or anything dangerous like that, so we
267
+ # consider it safe to untaint it. If we don't, mod_ruby will complain
268
+ # when Module#const_defined? is invoked a few lines down from here.
269
+ #
270
+ line.untaint
271
+
272
+ m = line.match( /Amazon::AWS::AWSObject::([^ ]+)/ )
273
+ if m
274
+ cl_name = [ m[1] ]
275
+
276
+ # Module#const_defined? takes 2 parameters in Ruby 1.9.
277
+ #
278
+ cl_name << false if Object.method( :const_defined? ).arity == -1
279
+
280
+ unless AWSObject.const_defined?( *cl_name )
281
+ AWSObject.const_set( m[1], Class.new( AWSObject ) )
282
+ end
283
+
284
+ end
285
+ end
286
+
287
+ io.rewind
288
+ YAML.load( io )
289
+ end
290
+
291
+
292
+ def initialize(op=nil)
293
+ # The name of this instance variable must never clash with the
294
+ # uncamelised name of an Amazon tag.
295
+ #
296
+ # This is used to store the REXML::Text value of an element, which
297
+ # exists only when the element contains no children.
298
+ #
299
+ @__val__ = nil
300
+ @__op__ = op if op
301
+ end
302
+
303
+
304
+ def method_missing(method, *params)
305
+ iv = '@' + method.id2name
306
+
307
+ if instance_variables.include?( iv )
308
+ instance_variable_get( iv )
309
+ elsif instance_variables.include?( iv.to_sym )
310
+
311
+ # Ruby 1.9 Object#instance_variables method returns Array of Symbol,
312
+ # not String.
313
+ #
314
+ instance_variable_get( iv.to_sym )
315
+ else
316
+ nil
317
+ end
318
+ end
319
+ private :method_missing
320
+
321
+
322
+ def remove_val
323
+ remove_instance_variable( :@__val__ )
324
+ end
325
+ private :remove_val
326
+
327
+
328
+ # Iterator method for cycling through an object's properties and values.
329
+ #
330
+ def each # :yields: property, value
331
+ self.properties.each do |iv|
332
+ yield iv, instance_variable_get( "@#{iv}" )
333
+ end
334
+ end
335
+
336
+ alias :each_property :each
337
+
338
+
339
+ def inspect # :nodoc:
340
+ remove_val if instance_variable_defined?( :@__val__ ) && @__val__.nil?
341
+ str = super
342
+ str.sub( /@__val__=/, 'value=' ) if str
343
+ end
344
+
345
+
346
+ def to_s # :nodoc:
347
+ if instance_variable_defined?( :@__val__ )
348
+ return @__val__ if @__val__.is_a?( String )
349
+ remove_val
350
+ end
351
+
352
+ string = ''
353
+
354
+ # Assemble the object's details.
355
+ #
356
+ each { |iv, value| string << "%s = %s\n" % [ iv, value ] }
357
+
358
+ string
359
+ end
360
+
361
+ alias :to_str :to_s
362
+
363
+
364
+ def to_i # :nodoc:
365
+ @__val__.to_i
366
+ end
367
+
368
+
369
+ def ==(other) # :nodoc:
370
+ @__val__.to_s == other
371
+ end
372
+
373
+
374
+ def =~(other) # :nodoc:
375
+ @__val__.to_s =~ other
376
+ end
377
+
378
+
379
+ # This alias makes the ability to determine an AWSObject's properties a
380
+ # little more intuitive. It's pretty much just an alias for the
381
+ # inherited <em>Object#instance_variables</em> method, with a little
382
+ # tidying.
383
+ #
384
+ def properties
385
+ # Make sure we remove the leading @.
386
+ #
387
+ iv = instance_variables.collect { |v| v = v[1..-1] }
388
+ iv.delete( '__val__' )
389
+ iv
390
+ end
391
+
392
+
393
+ # Provide a shortcut down to the data likely to be of most interest.
394
+ # This method is experimental and may be removed.
395
+ #
396
+ def kernel # :nodoc:
397
+ # E.g. Amazon::AWS::SellerListingLookup -> seller_listing_lookup
398
+ #
399
+ stub = Amazon.uncamelise( @__op__.class.to_s.sub( /^.+::/, '' ) )
400
+
401
+ # E.g. seller_listing_response
402
+ #
403
+ level1 = stub + '_response'
404
+
405
+ # E.g. seller_listing
406
+ #
407
+ level3 = stub.sub( /_[^_]+$/, '' )
408
+
409
+ # E.g. seller_listings
410
+ #
411
+ level2 = level3 + 's'
412
+
413
+ # E.g.
414
+ # seller_listing_search_response[0].seller_listings[0].seller_listing
415
+ #
416
+ self.instance_variable_get( "@#{level1}" )[0].
417
+ instance_variable_get( "@#{level2}" )[0].
418
+ instance_variable_get( "@#{level3}" )
419
+ end
420
+
421
+
422
+ # Convert an AWSObject to a Hash.
423
+ #
424
+ def to_h
425
+ hash = {}
426
+
427
+ each do |iv, value|
428
+ if value.is_a? AWSObject
429
+ hash[iv] = value.to_h
430
+ elsif value.is_a?( AWSArray ) && value.size == 1
431
+ hash[iv] = value[0]
432
+ else
433
+ hash[iv] = value
434
+ end
435
+ end
436
+
437
+ hash
438
+ end
439
+
440
+
441
+ # Fake the appearance of an AWSObject as a hash. _key_ should be any
442
+ # attribute of the object and can be a String, Symbol or anything else
443
+ # that can be converted to a String with to_s.
444
+ #
445
+ def [](key)
446
+ instance_variable_get( "@#{key}" )
447
+ end
448
+
449
+
450
+ # Recursively walk through an XML tree, starting from _node_. This is
451
+ # called internally and is not intended for user code.
452
+ #
453
+ def walk(node) # :nodoc:
454
+
455
+ if node.instance_of?( REXML::Document )
456
+ walk( node.root )
457
+
458
+ elsif node.instance_of?( REXML::Element )
459
+ name = Amazon.uncamelise( node.name )
460
+
461
+ cl_name = [ node.name ]
462
+
463
+ # Module#const_defined? takes 2 parameters in Ruby 1.9.
464
+ #
465
+ cl_name << false if Object.method( :const_defined? ).arity == -1
466
+
467
+ # Create a class for the new element type unless it already exists.
468
+ #
469
+ unless AWS::AWSObject.const_defined?( *cl_name )
470
+ cl = AWS::AWSObject.const_set( node.name, Class.new( AWSObject ) )
471
+
472
+ # Give it an accessor for @attrib.
473
+ #
474
+ cl.send( :attr_accessor, :attrib )
475
+ end
476
+
477
+ # Instantiate an object in the newly created class.
478
+ #
479
+ obj = AWS::AWSObject.const_get( node.name ).new
480
+
481
+ sym_name = "@#{name}".to_sym
482
+
483
+ if instance_variable_defined?( sym_name)
484
+ instance_variable_set( sym_name,
485
+ instance_variable_get( sym_name ) << obj )
486
+ else
487
+ instance_variable_set( sym_name, AWSArray.new( [ obj ] ) )
488
+ end
489
+
490
+ if node.has_attributes?
491
+ obj.attrib = {}
492
+ node.attributes.each_pair do |a_name, a_value|
493
+ obj.attrib[a_name.downcase] =
494
+ a_value.to_s.sub( /^#{a_name}=/, '' )
495
+ end
496
+ end
497
+
498
+ node.children.each { |child| obj.walk( child ) }
499
+
500
+ else # REXML::Text
501
+ @__val__ = node.to_s
502
+ end
503
+ end
504
+
505
+
506
+ # For objects of class AWSObject::.*Image, fetch the image in question,
507
+ # optionally overlaying a discount icon for the percentage amount of
508
+ # _discount_ to the image.
509
+ #
510
+ def get(discount=nil)
511
+ if self.class.to_s =~ /Image$/ && @url
512
+ url = URI.parse( @url[0] )
513
+ url.path.sub!( /(\.\d\d\._)/, "\\1PE#{discount}" ) if discount
514
+
515
+ # FIXME: All HTTP in Ruby/AWS should go through the same method.
516
+ #
517
+ Net::HTTP.start( url.host, url.port ) do |http|
518
+ http.get( url.path )
519
+ end.body
520
+
521
+ else
522
+ nil
523
+ end
524
+ end
525
+
526
+ end
527
+
528
+
529
+ # Everything we get back from AWS is transformed into an array. Many of
530
+ # these, however, have only one element, because the corresponding XML
531
+ # consists of a parent element containing only a single child element.
532
+ #
533
+ # This class consists solely to allow single element arrays to pass a
534
+ # method call down to their one element, thus obviating the need for lots
535
+ # of references to <tt>foo[0]</tt> in user code.
536
+ #
537
+ # For example, the following:
538
+ #
539
+ # items = resp.item_search_response[0].items[0].item
540
+ #
541
+ # can be reduced to:
542
+ #
543
+ # items = resp.item_search_response.items.item
544
+ #
545
+ class AWSArray < Array
546
+
547
+ def method_missing(method, *params)
548
+ self.size == 1 ? self[0].send( method, *params ) : super
549
+ end
550
+ private :method_missing
551
+
552
+
553
+ # In the case of a single-element array, return the first element,
554
+ # converted to a String.
555
+ #
556
+ def to_s # :nodoc:
557
+ self.size == 1 ? self[0].to_s : super
558
+ end
559
+
560
+ alias :to_str :to_s
561
+
562
+
563
+ # In the case of a single-element array, return the first element,
564
+ # converted to an Integer.
565
+ #
566
+ def to_i # :nodoc:
567
+ self.size == 1 ? self[0].to_i : super
568
+ end
569
+
570
+
571
+ # In the case of a single-element array, compare the first element with
572
+ # _other_.
573
+ #
574
+ def ==(other) # :nodoc:
575
+ self.size == 1 ? self[0].to_s == other : super
576
+ end
577
+
578
+
579
+ # In the case of a single-element array, perform a pattern match on the
580
+ # first element against _other_.
581
+ #
582
+ def =~(other) # :nodoc:
583
+ self.size == 1 ? self[0].to_s =~ other : super
584
+ end
585
+
586
+ end
587
+
588
+
589
+ # This is the base class of all AWS operations.
590
+ #
591
+ class Operation
592
+
593
+ # These are the types of AWS operation currently implemented by Ruby/AWS.
594
+ #
595
+ OPERATIONS = %w[
596
+ BrowseNodeLookup CustomerContentLookup CustomerContentSearch
597
+ Help ItemLookup ItemSearch
598
+ ListLookup ListSearch SellerListingLookup
599
+ SellerListingSearch SellerLookup SimilarityLookup
600
+ TagLookup TransactionLookup
601
+
602
+ CartAdd CartClear CartCreate
603
+ CartGet CartModify
604
+ ]
605
+
606
+ # These are the valid search parameters that can be used with
607
+ # ItemSearch.
608
+ #
609
+ PARAMETERS = %w[
610
+ Actor Artist AudienceRating Author
611
+ Brand BrowseNode City Composer Conductor
612
+ Director Keywords Manufacturer MusicLabel
613
+ Neighborhood Orchestra Power Publisher
614
+ TextStream Title
615
+ ]
616
+
617
+ OPT_PARAMETERS = %w[
618
+ Availability Condition MaximumPrice MerchantId
619
+ MinimumPrice OfferStatus Sort
620
+ ]
621
+
622
+ ALL_PARAMETERS = PARAMETERS + OPT_PARAMETERS
623
+
624
+ attr_reader :kind
625
+ attr_accessor :params
626
+
627
+ def initialize(parameters)
628
+
629
+ op_kind = self.class.to_s.sub( /^.*::/, '' )
630
+ unless OPERATIONS.include?( op_kind ) || op_kind == 'MultipleOperation'
631
+ raise "Bad operation: #{op_kind}"
632
+ end
633
+ #raise 'Too many parameters' if parameters.size > 10
634
+
635
+ @kind = op_kind
636
+ @params = { 'Operation' => op_kind }.merge( parameters )
637
+ end
638
+
639
+
640
+ # Convert parameters to batch format, e.g. ItemSearch.1.Title.
641
+ #
642
+ def batch_parameters(params, *b_params) # :nodoc:
643
+
644
+ @index ||= 1
645
+
646
+ unless b_params.empty?
647
+ op_str = self.class.to_s.sub( /^.+::/, '' )
648
+
649
+ # Fudge the operation string if we're dealing with a shopping cart.
650
+ #
651
+ op_str = 'Item' if op_str =~ /^Cart/
652
+
653
+ all_parameters = [ params ].concat( b_params )
654
+ params = {}
655
+
656
+ all_parameters.each_with_index do |hash, index|
657
+
658
+ # Don't batch an already batched hash.
659
+ #
660
+ if ! hash.empty? && hash.to_a[0][0] =~ /^.+\..+\..+$/
661
+ params = hash
662
+ next
663
+ end
664
+
665
+ hash.each do |tag, val|
666
+ shared_param = '%s.%d.%s' % [ op_str, @index + index, tag ]
667
+ params[shared_param] = val
668
+ end
669
+ end
670
+
671
+ @index += b_params.size
672
+
673
+ end
674
+
675
+ params
676
+ end
677
+
678
+
679
+ def parameter_check(parameters)
680
+ parameters.each_key do |key|
681
+ raise "Bad parameter: #{key}" unless ALL_PARAMETERS.include? key.to_s
682
+ end
683
+ end
684
+ private :parameter_check
685
+
686
+ end
687
+
688
+
689
+ # This class can be used to merge operations into a single operation.
690
+ # AWS currently supports combining two operations,
691
+ #
692
+ class MultipleOperation < Operation
693
+
694
+ # This will allow you to take two Operation objects and combine them to
695
+ # form a single object, which can then be used to perform searches. AWS
696
+ # itself imposes the maximum of two combined operations.
697
+ #
698
+ # <em>operation1</em> and <em>operation2</em> are both objects from a
699
+ # subclass of Operation, such as ItemSearch, ItemLookup, etc.
700
+ #
701
+ # There are currently a few restrictions in the Ruby/AWS implementation
702
+ # of multiple operations:
703
+ #
704
+ # - ResponseGroup objects used when calling AWS::Search::Request#search
705
+ # apply to both operations. You cannot have a separate ResponseGroup
706
+ # set per operation.
707
+ #
708
+ # - One or both operations may have multiple results pages available,
709
+ # but only the first page can be returned. If you need the other
710
+ # pages, perform the operations separately, not as part of a
711
+ # MultipleOperation.
712
+ #
713
+ # Example:
714
+ #
715
+ # is = ItemSearch.new( 'Books', { 'Title' => 'Ruby' } )
716
+ # il = ItemLookup.new( 'ASIN', { 'ItemId' => 'B0013DZAYO',
717
+ # 'MerchantId' => 'Amazon' } )
718
+ # mo = MultipleOperation.new( is, il )
719
+ #
720
+ # In the above example, we compose a multiple operation consisting of an
721
+ # ItemSearch and an ItemLookup.
722
+ #
723
+ def initialize(operation1, operation2)
724
+
725
+ # Safeguard against changing original Operation objects in place. This
726
+ # is to protect me, not for user code.
727
+ #
728
+ operation1.freeze
729
+ operation2.freeze
730
+
731
+ op_kind = '%s,%s' % [ operation1.kind, operation2.kind ]
732
+
733
+ # Duplicate Operation objects and remove their Operation parameter.
734
+ #
735
+ op1 = operation1.dup
736
+ op1.params = op1.params.dup
737
+ op1.params.delete( 'Operation' )
738
+
739
+ op2 = operation2.dup
740
+ op2.params = op2.params.dup
741
+ op2.params.delete( 'Operation' )
742
+
743
+ if op1.class == op2.class
744
+
745
+ # If both operations are of the same type, we combine the parameters
746
+ # of both.
747
+ #
748
+ b_params = op1.batch_parameters( op1.params, op2.params )
749
+ else
750
+
751
+ # We have to convert the parameters to batch format.
752
+ #
753
+ bp1 = op1.batch_parameters( op1.params, {} )
754
+ bp2 = op2.batch_parameters( op2.params, {} )
755
+ b_params = bp1.merge( bp2 )
756
+ end
757
+
758
+ params = { 'Operation' => op_kind }.merge( b_params )
759
+ super( params )
760
+
761
+ end
762
+
763
+ end
764
+
765
+
766
+ # This class of operation aids in finding out about AWS operations and
767
+ # response groups.
768
+ #
769
+ class Help < Operation
770
+
771
+ # Return information on AWS operations and response groups.
772
+ #
773
+ # For operations, required and optional parameters are returned, along
774
+ # with information about which response groups the operation can use.
775
+ #
776
+ # For response groups, The list of operations that can use that group is
777
+ # returned, as well as the list of response tags returned by the group.
778
+ #
779
+ # _help_type_ is the type of object for which help is being sought, such
780
+ # as *Operation* or *ResponseGroup*. _about_ is the name of the
781
+ # operation or response group you need help with, and _parameters_ is a
782
+ # hash of parameters that serve to further refine the request for help.
783
+ #
784
+ def initialize(help_type, about, parameters={})
785
+ super( { 'HelpType' => help_type,
786
+ 'About' => about
787
+ }.merge( parameters ) )
788
+ end
789
+
790
+ end
791
+
792
+
793
+ # This is the class for the most common type of AWS look-up, an
794
+ # ItemSearch. This allows you to search for items that match a set of
795
+ # broad criteria. It returns items for sale by Amazon merchants and most
796
+ # types of seller.
797
+ #
798
+ class ItemSearch < Operation
799
+
800
+ # Not all search indices work in all locales. It is the user's
801
+ # responsibility to ensure that a given index is valid within a given
802
+ # locale.
803
+ #
804
+ # According to the AWS documentation:
805
+ #
806
+ # - *All* searches through all indices (but currently exists only in the
807
+ # *US* locale).
808
+ # - *Blended* combines DVD, Electronics, Toys, VideoGames, PCHardware,
809
+ # Tools, SportingGoods, Books, Software, Music, GourmetFood, Kitchen
810
+ # and Apparel.
811
+ # - *Merchants* combines all search indices for a merchant given with
812
+ # MerchantId.
813
+ # - *Music* combines the Classical, DigitalMusic, and MusicTracks
814
+ # indices.
815
+ # - *Video* combines the DVD and VHS search indices.
816
+ #
817
+ SEARCH_INDICES = %w[
818
+ All
819
+ Apparel Hobbies PetSupplies
820
+ Automotive HomeGarden Photo
821
+ Baby Jewelry Software
822
+ Beauty Kitchen SoftwareVideoGames
823
+ Blended Magazines SportingGoods
824
+ Books Merchants Tools
825
+ Classical Miscellaneous Toys
826
+ DigitalMusic Music VHS
827
+ DVD MusicalInstruments Video
828
+ Electronics MusicTracks VideoGames
829
+ ForeignBooks OfficeProducts Wireless
830
+ GourmetFood OutdoorLiving WirelessAccessories
831
+ HealthPersonalCare PCHardware
832
+ ]
833
+
834
+
835
+ # Search AWS for items. _search_index_ must be one of _SEARCH_INDICES_
836
+ # and _parameters_ is a hash of relevant search parameters.
837
+ #
838
+ # Example:
839
+ #
840
+ # is = ItemSearch.new( 'Books', { 'Title' => 'ruby programming' } )
841
+ #
842
+ # In the above example, we search for books with <b>Ruby Programming</b>
843
+ # in the title.
844
+ #
845
+ def initialize(search_index, parameters)
846
+ unless SEARCH_INDICES.include? search_index.to_s
847
+ raise "Invalid search index: #{search_index}"
848
+ end
849
+
850
+ parameter_check( parameters )
851
+ super( { 'SearchIndex' => search_index }.merge( parameters ) )
852
+ end
853
+
854
+ end
855
+
856
+
857
+ # This class of look-up deals with searching for *specific* items by some
858
+ # uniquely identifying attribute, such as the ASIN (*A*mazon *S*tandard
859
+ # *I*tem *N*umber).
860
+ #
861
+ class ItemLookup < Operation
862
+
863
+ # Look up a specific item in the AWS catalogue. _id_type_ is the type of
864
+ # identifier, _parameters_ is a hash that identifies the item to be
865
+ # located and narrows the scope of the search, and _b_parameters_ is an
866
+ # optional hash of further items to be located. Use of _b_parameters_
867
+ # effectively results in a batch operation being sent to AWS.
868
+ #
869
+ # Example:
870
+ #
871
+ # il = ItemLookup.new( 'ASIN', { 'ItemId' => 'B000AE4QEC'
872
+ # 'MerchantId' => 'Amazon' },
873
+ # { 'ItemId' => 'B000051WBE',
874
+ # 'MerchantId' => 'Amazon' } )
875
+ #
876
+ # In the above example, we search for two items, based on their ASIN.
877
+ # The use of _MerchantId_ restricts the offers returned to those for
878
+ # sale by Amazon (as opposed to third-party sellers).
879
+ #
880
+ def initialize(id_type, parameters, *b_parameters)
881
+
882
+ id_type_str = 'IdType'
883
+
884
+ unless b_parameters.empty?
885
+ class_str = self.class.to_s.sub( /^.+::/, '' )
886
+ id_type_str = '%s.Shared.IdType' % [ class_str ]
887
+ parameters = batch_parameters( parameters, *b_parameters )
888
+ end
889
+
890
+ super( { id_type_str => id_type }.merge( parameters ) )
891
+ end
892
+
893
+ end
894
+
895
+
896
+ # Search for items for sale by a particular seller.
897
+ #
898
+ class SellerListingSearch < Operation
899
+
900
+ # Search for items for sale by a particular seller. _seller_id_ is the
901
+ # Amazon seller ID and _parameters_ is a hash of parameters that narrows
902
+ # the scope of the search.
903
+ #
904
+ # Example:
905
+ #
906
+ # sls = SellerListingSearch.new( 'A33J388YD2MWJZ',
907
+ # { 'Keywords' => 'Killing Joke' } )
908
+ #
909
+ # In the above example, we search seller <b>A33J388YD2MWJ</b>'s listings
910
+ # for items with the keywords <b>Killing Joke</b>.
911
+ #
912
+ def initialize(seller_id, parameters)
913
+ super( { 'SellerId' => seller_id }.merge( parameters ) )
914
+ end
915
+
916
+ end
917
+
918
+
919
+ # Return specified items in a seller's store.
920
+ #
921
+ class SellerListingLookup < ItemLookup
922
+
923
+ # Look up a specific item for sale by a specific seller. _id_type_ is
924
+ # the type of identifier, _parameters_ is a hash that identifies the
925
+ # item to be located and narrows the scope of the search, and
926
+ # _b_parameters_ is an optional hash of further items to be located. Use
927
+ # of _b_parameters_ effectively results in a batch operation being sent
928
+ # to AWS.
929
+ #
930
+ # Example:
931
+ #
932
+ # sll = SellerListingLookup.new( 'AP8U6Y3PYQ9VO', 'ASIN',
933
+ # { 'Id' => 'B0009RRRC8' } )
934
+ #
935
+ # In the above example, we search seller <b>AP8U6Y3PYQ9VO</b>'s listings
936
+ # to find items for sale with the ASIN <b>B0009RRRC8</b>.
937
+ #
938
+ def initialize(seller_id, id_type, parameters, *b_parameters)
939
+ super( id_type, { 'SellerId' => seller_id }.merge( parameters ),
940
+ b_parameters )
941
+ end
942
+
943
+ end
944
+
945
+
946
+ # Return information about a specific seller.
947
+ #
948
+ class SellerLookup < Operation
949
+
950
+ # Search for the details of a specific seller. _seller_id_ is the Amazon
951
+ # ID of the seller in question and _parameters_ is a hash of parameters
952
+ # that serve to further refine the search.
953
+ #
954
+ # Example:
955
+ #
956
+ # sl = SellerLookup.new( 'A3QFR0K2KCB7EG' )
957
+ #
958
+ # In the above example, we look up the details of the seller with ID
959
+ # <b>A3QFR0K2KCB7EG</b>.
960
+ #
961
+ def initialize(seller_id, parameters={})
962
+ super( { 'SellerId' => seller_id }.merge( parameters ) )
963
+ end
964
+
965
+ end
966
+
967
+
968
+ # Obtain the information an Amazon customer has made public about
969
+ # themselves.
970
+ #
971
+ class CustomerContentLookup < Operation
972
+
973
+ # Search for public customer data. _customer_id_ is the unique ID
974
+ # identifying the customer on Amazon and _parameters_ is a hash of
975
+ # parameters that serve to further refine the search.
976
+ #
977
+ # Example:
978
+ #
979
+ # ccl = CustomerContentLookup.new( 'AJDWXANG1SYZP' )
980
+ #
981
+ # In the above example, we look up public data about the customer with
982
+ # the ID <b>AJDWXANG1SYZP</b>.
983
+ #
984
+ def initialize(customer_id, parameters={})
985
+ super( { 'CustomerId' => customer_id }.merge( parameters ) )
986
+ end
987
+
988
+ end
989
+
990
+
991
+ # Retrieve basic Amazon customer data.
992
+ #
993
+ class CustomerContentSearch < Operation
994
+
995
+ # Retrieve customer information, using an e-mail address or name.
996
+ #
997
+ # If _customer_id_ contains an '@' sign, it is assumed to be an e-mail
998
+ # address. Otherwise, it is assumed to be the customer's name.
999
+ #
1000
+ # Example:
1001
+ #
1002
+ # ccs = CustomerContentSearch.new( 'ian@caliban.org' )
1003
+ #
1004
+ # In the above example, we look up customer information about
1005
+ # <b>ian@caliban.org</b>. The *CustomerInfo* response group will return,
1006
+ # amongst other things, a _customer_id_ property, which can then be
1007
+ # plugged into CustomerContentLookup to retrieve more detailed customer
1008
+ # information.
1009
+ #
1010
+ def initialize(customer_id)
1011
+ id = customer_id =~ /@/ ? 'Email' : 'Name'
1012
+ super( { id => customer_id } )
1013
+ end
1014
+
1015
+ end
1016
+
1017
+
1018
+ # Find wishlists, registry lists, etc. created by users and placed on
1019
+ # Amazon. These are items that customers would like to receive as
1020
+ # presnets.
1021
+ #
1022
+ class ListSearch < Operation
1023
+
1024
+ # Search for Amazon lists. _list_type_ is the type of list to search for
1025
+ # and _parameters_ is a hash of parameters that narrows the scope of the
1026
+ # search.
1027
+ #
1028
+ # Example:
1029
+ #
1030
+ # ls = ListSearch.new( 'WishList', { 'Name' => 'Peter Duff' }
1031
+ #
1032
+ # In the above example, we retrieve the wishlist for the Amazon user,
1033
+ # <b>Peter Duff</b>.
1034
+ #
1035
+ def initialize(list_type, parameters)
1036
+ super( { 'ListType' => list_type }.merge( parameters ) )
1037
+ end
1038
+
1039
+ end
1040
+
1041
+
1042
+ # Find the details of specific wishlists, registries, etc.
1043
+ #
1044
+ class ListLookup < Operation
1045
+
1046
+ # Look up and return details about a specific list. _list_id_ is the
1047
+ # Amazon list ID, _list_type_ is the type of list and _parameters_ is a
1048
+ # hash of parameters that narrows the scope of the search.
1049
+ #
1050
+ # Example:
1051
+ #
1052
+ # ll = ListLookup.new( '3P722DU4KUPCP', 'Listmania' )
1053
+ #
1054
+ # In the above example, a *Listmania* list with the ID
1055
+ # <b>3P722DU4KUPCP</b> is retrieved from AWS.
1056
+ #
1057
+ def initialize(list_id, list_type, parameters={})
1058
+ super( { 'ListId' => list_id,
1059
+ 'ListType' => list_type
1060
+ }.merge( parameters ) )
1061
+ end
1062
+
1063
+ end
1064
+
1065
+
1066
+ # Amazon use browse nodes as a means of organising the millions of items
1067
+ # in their inventory. An example might be *Carving Knives*. Looking up a
1068
+ # browse node enables you to determine that group's ancestors and
1069
+ # descendants.
1070
+ #
1071
+ class BrowseNodeLookup < Operation
1072
+
1073
+ # Look up and return the details of an Amazon browse node. _node_ is the
1074
+ # browse node to look up and _parameters_ is a hash of parameters that
1075
+ # serves to further define the search. _parameters_ is currently unused.
1076
+ #
1077
+ # Example:
1078
+ #
1079
+ # bnl = BrowseNodeLookup.new( '11232', {} )
1080
+ #
1081
+ # In the above example, we look up the browse node with the ID
1082
+ # <b>11232</b>. This is the <b>Social Sciences</b> browse node.
1083
+ #
1084
+ def initialize(node, parameters={})
1085
+ super( { 'BrowseNodeId' => node }.merge( parameters ) )
1086
+ end
1087
+
1088
+ end
1089
+
1090
+
1091
+ # Similarity look-up is for items similar to others.
1092
+ #
1093
+ class SimilarityLookup < Operation
1094
+
1095
+ # Look up items similar to _asin_, which can be a single item or an
1096
+ # array. _parameters_ is a hash of parameters that serve to further
1097
+ # refine the search.
1098
+ #
1099
+ # Example:
1100
+ #
1101
+ # sl = SimilarityLookup.new( 'B000051WBE' )
1102
+ #
1103
+ # In the above example, we search for items similar to the one with ASIN
1104
+ # <b>B000051WBE</b>.
1105
+ #
1106
+ def initialize(asin, parameters={})
1107
+ super( { 'ItemId' => asin.to_a.join( ',' ) }.merge( parameters ) )
1108
+ end
1109
+
1110
+ end
1111
+
1112
+
1113
+ # Search for entities based on user-defined tags. A tag is a descriptive
1114
+ # word that a customer uses to label entities on Amazon's Web site.
1115
+ # Entities can be items for sale, Listmania lists, guides, etc.
1116
+ #
1117
+ class TagLookup < Operation
1118
+
1119
+ # Look up entities based on user-defined tags. _tag_name_ is the tag to
1120
+ # search on and _parameters_ is a hash of parameters that serve to
1121
+ # further refine the search.
1122
+ #
1123
+ # Example:
1124
+ #
1125
+ # tl = TagLookup.new( 'Awful' )
1126
+ #
1127
+ # In the example above, we search for entities tagged by users with the
1128
+ # word *Awful*.
1129
+ #
1130
+ def initialize(tag_name, parameters={})
1131
+ super( { 'TagName' => tag_name }.merge( parameters ) )
1132
+ end
1133
+
1134
+ end
1135
+
1136
+
1137
+ # Search for information on previously completed purchases.
1138
+ #
1139
+ class TransactionLookup < Operation
1140
+
1141
+ # Return information on an already completed purchase. _transaction_id_
1142
+ # is actually the order number that is created when you place an order
1143
+ # on Amazon.
1144
+ #
1145
+ # Example:
1146
+ #
1147
+ # tl = TransactionLookup.new( '103-5663398-5028241' )
1148
+ #
1149
+ # In the above example, we retrieve the details of order number
1150
+ # <b>103-5663398-5028241</b>.
1151
+ #
1152
+ def initialize(transaction_id)
1153
+ super( { 'TransactionId' => transaction_id } )
1154
+ end
1155
+
1156
+ end
1157
+
1158
+
1159
+ # Response groups determine which data pertaining to the item(s) being
1160
+ # sought is returned. They can strongly influence the amount of data
1161
+ # returned, so you should always use the smallest response group(s)
1162
+ # containing the data of interest to you, to avoid masses of unnecessary
1163
+ # data being returned.
1164
+ #
1165
+ class ResponseGroup
1166
+
1167
+ attr_reader :list, :params
1168
+
1169
+ # Define a set of one or more response groups to be applied to items
1170
+ # retrieved by an AWS operation.
1171
+ #
1172
+ # If no response groups are given in _rg_ when instantiating an object,
1173
+ # *Small* will be used by default.
1174
+ #
1175
+ # Example:
1176
+ #
1177
+ # rg = ResponseGroup.new( 'Medium', 'Offers', 'Reviews' )
1178
+ #
1179
+ def initialize(*rg)
1180
+ rg << 'Small' if rg.empty?
1181
+ @list = rg
1182
+ @params = { 'ResponseGroup' => @list.join( ',' ) }
1183
+ end
1184
+
1185
+ end
1186
+
1187
+
1188
+ # All dynamically generated exceptions occur within this namespace.
1189
+ #
1190
+ module Error
1191
+
1192
+ # The base exception class for errors that result from AWS operations.
1193
+ # Classes for these are dynamically generated as subclasses of this one.
1194
+ #
1195
+ class AWSError < AmazonError; end
1196
+
1197
+ def Error.exception(xml)
1198
+ err_class = xml.elements['Code'].text.sub( /^AWS.*\./, '' )
1199
+ err_msg = xml.elements['Message'].text
1200
+
1201
+ # Dynamically define a new exception class for this class of error,
1202
+ # unless it already exists.
1203
+ #
1204
+ unless Amazon::AWS::Error.const_defined?( err_class )
1205
+ Amazon::AWS::Error.const_set( err_class, Class.new( AWSError ) )
1206
+ end
1207
+
1208
+ # Generate and return a new exception from the relevant class.
1209
+ #
1210
+ Amazon::AWS::Error.const_get( err_class ).new( err_msg )
1211
+ end
1212
+
1213
+ end
1214
+
1215
+ end
1216
+
1217
+ end