ruby-paa 0.0.1

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