kernow-ruby-aaws 0.5.4 → 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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/ruby -w
2
2
  #
3
- # $Id: multiple_operation1,v 1.1 2008/04/11 15:00:56 ianmacd Exp $
3
+ # $Id: multiple_operation1,v 1.2 2009/02/20 00:25:59 ianmacd Exp $
4
4
 
5
5
  require 'amazon/aws/search'
6
6
 
@@ -14,9 +14,10 @@ include Amazon::AWS::Search
14
14
  # availability status.
15
15
  #
16
16
  il = ItemLookup.new( 'ASIN', { 'ItemId' => 'B000AE4QEC',
17
- 'MerchantId' => 'Amazon' },
18
- { 'ItemId' => 'B000051WBE',
19
17
  'MerchantId' => 'Amazon' } )
18
+ il2 = ItemLookup.new( 'ASIN', { 'ItemId' => 'B000051WBE',
19
+ 'MerchantId' => 'Amazon' } )
20
+ il.batch( il2 )
20
21
  is = ItemSearch.new( 'Books', { 'Title' => 'Ruby' } )
21
22
 
22
23
  mo = MultipleOperation.new( is, il )
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/ruby -w
2
+ #
3
+ # $Id: vehicle_search,v 1.1 2009/02/19 15:48:57 ianmacd Exp $
4
+
5
+ require 'amazon/aws/search'
6
+
7
+ include Amazon::AWS
8
+ include Amazon::AWS::Search
9
+
10
+ is = VehicleSearch.new( { 'Year' => 2008 } )
11
+ rg = ResponseGroup.new( 'VehicleMakes' )
12
+
13
+ req = Request.new
14
+ req.locale = 'us'
15
+
16
+ resp = req.search( is, rg )
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, '' }
data/lib/amazon.rb CHANGED
@@ -1,4 +1,4 @@
1
- # $Id: amazon.rb,v 1.25 2008/10/03 09:35:37 ianmacd Exp $
1
+ # $Id: amazon.rb,v 1.29 2009/06/08 15:20:11 ianmacd Exp $
2
2
  #
3
3
 
4
4
  module Amazon
@@ -8,6 +8,7 @@ module Amazon
8
8
  class AmazonError < StandardError; end
9
9
 
10
10
  NAME = 'Ruby/Amazon'
11
+
11
12
  @@config = {}
12
13
 
13
14
  # Prints debugging messages and works like printf, except that it prints
@@ -22,28 +23,31 @@ module Amazon
22
23
  #
23
24
  def Amazon.url_encode(string)
24
25
 
25
- # Shamelessly plagiarised from Wakou Aoyama's cgi.rb.
26
+ # Shamelessly plagiarised from Wakou Aoyama's cgi.rb, but then altered
27
+ # slightly to please AWS.
26
28
  #
27
- string.gsub( /([^ a-zA-Z0-9_.-]+)/n ) do
28
- '%' + $1.unpack( 'H2' * $1.size ).join( '%' ).upcase
29
- end.tr( ' ', '+' )
29
+ string.gsub( /([^a-zA-Z0-9_.~-]+)/ ) do
30
+ '%' + $1.unpack( 'H2' * $1.bytesize ).join( '%' ).upcase
31
+ end
30
32
  end
31
33
 
32
34
 
33
35
  # Convert a string from CamelCase to ruby_case.
34
36
  #
35
- def Amazon.uncamelise(str)
37
+ def Amazon.uncamelise(string)
36
38
  # Avoid modifying by reference.
37
39
  #
38
- str = str.dup
40
+ string = string.dup
39
41
 
40
42
  # Don't mess with string if all caps.
41
43
  #
42
- str.gsub!( /(.+?)(([A-Z][a-z]|[A-Z]+$))/, "\\1_\\2" ) if str =~ /[a-z]/
44
+ if string =~ /[a-z]/
45
+ string.gsub!( /(.+?)(([A-Z][a-z]|[A-Z]+$))/, "\\1_\\2" )
46
+ end
43
47
 
44
48
  # Convert to lower case.
45
49
  #
46
- str.downcase
50
+ string.downcase
47
51
  end
48
52
 
49
53
 
@@ -63,6 +67,7 @@ module Amazon
63
67
  # and are readable.
64
68
  #
65
69
  def initialize(config_str=nil)
70
+ locale = nil
66
71
 
67
72
  if config_str
68
73
 
@@ -105,16 +110,24 @@ module Amazon
105
110
  if readable
106
111
 
107
112
  Amazon.dprintf( 'Opening %s ...', cf ) if config_class == File
108
-
113
+
109
114
  config_class.open( cf ) { |f| lines = f.readlines }.each do |line|
110
115
  line.chomp!
111
-
116
+
112
117
  # Skip comments and blank lines.
113
118
  #
114
119
  next if line =~ /^(#|$)/
115
-
120
+
116
121
  Amazon.dprintf( 'Read: %s', line )
117
-
122
+
123
+ # Determine whether we're entering the subsection of a new locale.
124
+ #
125
+ if match = line.match( /^\[(\w+)\]$/ )
126
+ locale = match[1]
127
+ Amazon.dprintf( "Config locale is now '%s'.", locale )
128
+ next
129
+ end
130
+
118
131
  # Store these, because we'll probably find a use for these later.
119
132
  #
120
133
  begin
@@ -125,9 +138,14 @@ module Amazon
125
138
  rescue NoMethodError, ConfigError
126
139
  raise ConfigError, "bad config line: #{line}"
127
140
  end
128
-
129
- self[key] = val
130
-
141
+
142
+ if locale && locale != 'global'
143
+ self[locale] ||= {}
144
+ self[locale][key] = val
145
+ else
146
+ self[key] = val
147
+ end
148
+
131
149
  end
132
150
  end
133
151
 
data/lib/amazon/aws.rb CHANGED
@@ -1,4 +1,4 @@
1
- # $Id: aws.rb,v 1.72 2008/10/03 09:37:25 ianmacd Exp $
1
+ # $Id: aws.rb,v 1.118 2009/06/15 23:51:53 ianmacd Exp $
2
2
  #
3
3
  #:include: ../../README.rdoc
4
4
 
@@ -6,13 +6,15 @@ module Amazon
6
6
 
7
7
  module AWS
8
8
 
9
- require 'uri'
10
9
  require 'amazon'
11
10
  require 'amazon/aws/cache'
11
+ require 'enumerator'
12
+ require 'iconv'
12
13
  require 'rexml/document'
14
+ require 'uri'
13
15
 
14
16
  NAME = '%s/%s' % [ Amazon::NAME, 'AWS' ]
15
- VERSION = '0.4.4'
17
+ VERSION = '0.7.0'
16
18
  USER_AGENT = '%s %s' % [ NAME, VERSION ]
17
19
 
18
20
  # Default Associate tags to use per locale.
@@ -26,10 +28,11 @@ module Amazon
26
28
  'us' => 'calibanorg-20'
27
29
  }
28
30
 
29
- # Service name and version for AWS.
31
+ # Service name and API version for AWS. The version of the API used can be
32
+ # changed via the user configuration file.
30
33
  #
31
34
  SERVICE = { 'Service' => 'AWSECommerceService',
32
- 'Version' => '2008-08-19'
35
+ 'Version' => '2009-03-31'
33
36
  }
34
37
 
35
38
  # Maximum number of 301 and 302 HTTP responses to follow, should Amazon
@@ -53,7 +56,9 @@ module Amazon
53
56
  'CustomerContentLookup' => { 'parameter' => 'ReviewPage',
54
57
  'max_page' => 10 },
55
58
  'CustomerContentSearch' => { 'parameter' => 'CustomerPage',
56
- 'max_page' => 20 }
59
+ 'max_page' => 20 },
60
+ 'VehiclePartLookup' => { 'parameter' => 'FitmentPage',
61
+ 'max_page' => 10 }
57
62
  }
58
63
  # N.B. ItemLookup can also use the following two pagination parameters
59
64
  #
@@ -61,11 +66,23 @@ module Amazon
61
66
  # ---------
62
67
  # VariationPage 150
63
68
  # ReviewPage 20
64
-
69
+
70
+
71
+ # A hash to store character encoding converters.
72
+ #
73
+ @@encodings = {}
74
+
75
+
65
76
  # Exception class for HTTP errors.
66
77
  #
67
78
  class HTTPError < AmazonError; end
68
79
 
80
+
81
+ # Exception class for faulty batch operations.
82
+ #
83
+ class BatchError < AmazonError; end
84
+
85
+
69
86
  class Endpoint
70
87
 
71
88
  attr_reader :host, :path
@@ -86,11 +103,12 @@ module Amazon
86
103
  'us' => Endpoint.new( 'http://ecs.amazonaws.com/onca/xml' )
87
104
  }
88
105
 
106
+
89
107
  # Fetch a page, either from the cache or by HTTP. This is used internally.
90
108
  #
91
- def AWS.get_page(request, query) # :nodoc:
109
+ def AWS.get_page(request) # :nodoc:
92
110
 
93
- url = ENDPOINT[request.locale].path + query
111
+ url = ENDPOINT[request.locale].path + request.query
94
112
  cache_url = ENDPOINT[request.locale].host + url
95
113
 
96
114
  # Check for cached page and return that if it's there.
@@ -100,6 +118,17 @@ module Amazon
100
118
  return body if body
101
119
  end
102
120
 
121
+ # Check whether we have a secret key available for signing the request.
122
+ # If so, sign the request for authentication.
123
+ #
124
+ if request.config['secret_key_id']
125
+ unless request.sign
126
+ Amazon.dprintf( 'Warning! Failed to sign request. No OpenSSL support for SHA256 digest.' )
127
+ end
128
+
129
+ url = ENDPOINT[request.locale].path + request.query
130
+ end
131
+
103
132
  # Get the existing connection. If there isn't one, force a new one.
104
133
  #
105
134
  conn = request.conn || request.reconnect.conn
@@ -114,7 +143,10 @@ module Amazon
114
143
  # just not passed by here recently), the HTTP connection to the server
115
144
  # will probably have timed out.
116
145
  #
117
- rescue Errno::ECONNRESET
146
+ rescue EOFError, Errno::ECONNABORTED, Errno::ECONNREFUSED,
147
+ Errno::ECONNRESET, Errno::EPIPE, Errno::ETIMEDOUT,
148
+ Timeout::Error => error
149
+ Amazon.dprintf( 'Connection to server lost: %s. Retrying...', error )
118
150
  conn = request.reconnect.conn
119
151
  retry
120
152
  end
@@ -148,16 +180,23 @@ module Amazon
148
180
  end
149
181
 
150
182
 
151
- def AWS.assemble_query(items) # :nodoc:
183
+ def AWS.assemble_query(items, encoding=nil) # :nodoc:
184
+
152
185
  query = ''
186
+ @@encodings[encoding] ||= Iconv.new( 'utf-8', encoding ) if encoding
153
187
 
154
188
  # We must sort the items into an array to get reproducible ordering
155
189
  # of the query parameters. Otherwise, URL caching would not work. We
156
- # must also convert the keys to strings, in case Symbols have been used
157
- # as the keys.
190
+ # must also convert the parameter values to strings, in case Symbols
191
+ # have been used as the values.
158
192
  #
159
193
  items.sort { |a,b| a.to_s <=> b.to_s }.each do |k, v|
160
- query << '&%s=%s' % [ k, Amazon.url_encode( v.to_s ) ]
194
+ if encoding
195
+ query << '&%s=%s' %
196
+ [ k, Amazon.url_encode( @@encodings[encoding].iconv( v.to_s ) ) ]
197
+ else
198
+ query << '&%s=%s' % [ k, Amazon.url_encode( v.to_s ) ]
199
+ end
161
200
  end
162
201
 
163
202
  # Replace initial ampersand with question-mark.
@@ -217,7 +256,7 @@ module Amazon
217
256
  # dynamically defined by a separate process.
218
257
  #
219
258
  def AWSObject.yaml_load(io)
220
- io.each do |line|
259
+ io.each do |line|
221
260
 
222
261
  # File data is external, so it's deemed unsafe when $SAFE > 0, which
223
262
  # is the case with mod_ruby, for example, where $SAFE == 1.
@@ -234,7 +273,7 @@ module Amazon
234
273
 
235
274
  # Module#const_defined? takes 2 parameters in Ruby 1.9.
236
275
  #
237
- cl_name << false if Object.method( :const_defined? ).arity == -1
276
+ cl_name << false if RUBY_VERSION >= '1.9.0'
238
277
 
239
278
  unless AWSObject.const_defined?( *cl_name )
240
279
  AWSObject.const_set( m[1], Class.new( AWSObject ) )
@@ -264,6 +303,9 @@ module Amazon
264
303
  iv = '@' + method.id2name
265
304
 
266
305
  if instance_variables.include?( iv )
306
+
307
+ # Return the instance variable that matches the method called.
308
+ #
267
309
  instance_variable_get( iv )
268
310
  elsif instance_variables.include?( iv.to_sym )
269
311
 
@@ -271,6 +313,12 @@ module Amazon
271
313
  # not String.
272
314
  #
273
315
  instance_variable_get( iv.to_sym )
316
+ elsif @__val__.respond_to?( method.id2name )
317
+
318
+ # If our value responds to the method in question, call the method
319
+ # on that.
320
+ #
321
+ @__val__.send( method.id2name )
274
322
  else
275
323
  nil
276
324
  end
@@ -320,13 +368,8 @@ module Amazon
320
368
  alias :to_str :to_s
321
369
 
322
370
 
323
- def to_i # :nodoc:
324
- @__val__.to_i
325
- end
326
-
327
-
328
371
  def ==(other) # :nodoc:
329
- @__val__.to_s == other
372
+ @__val__.to_s == other
330
373
  end
331
374
 
332
375
 
@@ -421,7 +464,7 @@ module Amazon
421
464
 
422
465
  # Module#const_defined? takes 2 parameters in Ruby 1.9.
423
466
  #
424
- cl_name << false if Object.method( :const_defined? ).arity == -1
467
+ cl_name << false if RUBY_VERSION >= '1.9.0'
425
468
 
426
469
  # Create a class for the new element type unless it already exists.
427
470
  #
@@ -468,12 +511,12 @@ module Amazon
468
511
  #
469
512
  def get(discount=nil)
470
513
  if self.class.to_s =~ /Image$/ && @url
471
- url = URI.parse( @url[0] )
472
- url.path.sub!( /(\.\d\d\._)/, "\\1PE#{discount}" ) if discount
514
+ url = URI.parse( @url[0] )
515
+ url.path.sub!( /(\.\d\d\._)/, "\\1PE#{discount}" ) if discount
473
516
 
474
517
  # FIXME: All HTTP in Ruby/AWS should go through the same method.
475
518
  #
476
- Net::HTTP.start( url.host, url.port ) do |http|
519
+ Net::HTTP.start( url.host, url.port ) do |http|
477
520
  http.get( url.path )
478
521
  end.body
479
522
 
@@ -554,45 +597,82 @@ module Amazon
554
597
  OPERATIONS = %w[
555
598
  BrowseNodeLookup CustomerContentLookup CustomerContentSearch
556
599
  Help ItemLookup ItemSearch
557
- ListLookup ListSearch SellerListingLookup
558
- SellerListingSearch SellerLookup SimilarityLookup
559
- TagLookup TransactionLookup
600
+ ListLookup ListSearch MultipleOperation
601
+ SellerListingLookup SellerListingSearch SellerLookup
602
+ SimilarityLookup TagLookup TransactionLookup
603
+ VehiclePartLookup VehiclePartSearch VehicleSearch
560
604
 
561
605
  CartAdd CartClear CartCreate
562
606
  CartGet CartModify
563
607
  ]
564
608
 
565
- # These are the valid search parameters that can be used with
566
- # ItemSearch.
567
- #
568
- PARAMETERS = %w[
569
- Actor Artist AudienceRating Author
570
- Brand BrowseNode City Composer Conductor
571
- Director Keywords Manufacturer MusicLabel
572
- Neighborhood Orchestra Power Publisher
573
- TextStream Title
574
- ]
575
-
576
- OPT_PARAMETERS = %w[
577
- Availability Condition MaximumPrice MerchantId
578
- MinimumPrice OfferStatus Sort
579
- ]
580
-
581
- ALL_PARAMETERS = PARAMETERS + OPT_PARAMETERS
582
-
583
609
  attr_reader :kind
584
- attr_accessor :params
610
+ attr_accessor :params, :response_group
585
611
 
586
612
  def initialize(parameters)
587
613
 
588
614
  op_kind = self.class.to_s.sub( /^.*::/, '' )
589
- unless OPERATIONS.include?( op_kind ) || op_kind == 'MultipleOperation'
615
+ unless OPERATIONS.include?( op_kind )
590
616
  raise "Bad operation: #{op_kind}"
591
617
  end
592
618
  #raise 'Too many parameters' if parameters.size > 10
593
619
 
594
620
  @kind = op_kind
595
621
  @params = { 'Operation' => op_kind }.merge( parameters )
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
+ end
630
+
631
+
632
+ # Group together operations of the same class in a batch request.
633
+ # _operations_ should be either an operation of the same class as *self*
634
+ # or an array of such operations.
635
+ #
636
+ # If you need to batch operations from different classes, use a
637
+ # MultipleOperation instead.
638
+ #
639
+ # Example:
640
+ #
641
+ # is = ItemSearch.new( 'Books', { 'Title' => 'ruby programming' } )
642
+ # is2 = ItemSearch.new( 'Music', { 'Artist' => 'stranglers' } )
643
+ # is.response_group = ResponseGroup.new( :Small )
644
+ # is2.response_group = ResponseGroup.new( :Tracks )
645
+ # is.batch( is2 )
646
+ #
647
+ # Please see MultipleOperation.new for implementation details that also
648
+ # apply to batched operations.
649
+ #
650
+ def batch(*operations)
651
+
652
+ # Remove the Operation parameter to avoid batch syntax being applied.
653
+ # We'll readd it at the end.
654
+ #
655
+ op_type = @params.delete( 'Operation' )
656
+
657
+ operations.flatten.each do |op|
658
+
659
+ unless self.class == op.class
660
+ raise BatchError, "You can't batch different classes of operation. Use class MultipleOperation."
661
+ end
662
+
663
+ # Remove the Operation parameter.
664
+ #
665
+ op.params.delete( 'Operation' )
666
+
667
+ # Apply batch syntax.
668
+ #
669
+ @params = batch_parameters( @params, op.params )
670
+ @response_group.params = batch_response_groups( op )
671
+ end
672
+
673
+ # Reinstate the Operation parameter.
674
+ #
675
+ @params.merge!( { 'Operation' => op_type } )
596
676
  end
597
677
 
598
678
 
@@ -603,21 +683,33 @@ module Amazon
603
683
  @index ||= 1
604
684
 
605
685
  unless b_params.empty?
606
- op_str = self.class.to_s.sub( /^.+::/, '' )
686
+ op_str = @kind || self.class.to_s.sub( /^.*::/, '' )
607
687
 
608
688
  # Fudge the operation string if we're dealing with a shopping cart.
609
689
  #
610
690
  op_str = 'Item' if op_str =~ /^Cart/
611
691
 
612
- all_parameters = [ params ].concat( b_params )
692
+ all_parameters = []
693
+
694
+ # Shopping carts pass an empty hash in params, so we have to ditch
695
+ # params in such a case to prevent the batch index from being off by
696
+ # one.
697
+ #
698
+ all_parameters.concat( [ params ] ) unless params.empty?
699
+ all_parameters.concat( b_params )
700
+
613
701
  params = {}
702
+ index = 0
614
703
 
615
- all_parameters.each_with_index do |hash, index|
704
+ all_parameters.each do |hash|
705
+
706
+ next if hash.empty?
616
707
 
617
708
  # Don't batch an already batched hash.
618
709
  #
619
- if ! hash.empty? && hash.to_a[0][0] =~ /^.+\..+\..+$/
620
- params = hash
710
+ if hash.to_a[0][0] =~ /^.+\..+\..+$/
711
+ params.merge!( hash )
712
+ @index += 1
621
713
  next
622
714
  end
623
715
 
@@ -625,6 +717,8 @@ module Amazon
625
717
  shared_param = '%s.%d.%s' % [ op_str, @index + index, tag ]
626
718
  params[shared_param] = val
627
719
  end
720
+
721
+ index += 1
628
722
  end
629
723
 
630
724
  @index += b_params.size
@@ -634,69 +728,109 @@ module Amazon
634
728
  params
635
729
  end
636
730
 
731
+ end
732
+
733
+
734
+ # Convert response groups to batch format, e.g. ItemSearch.1.ResponseGroup.
735
+ #
736
+ def batch_response_groups(operation)
737
+
738
+ rg = {}
739
+ op_count = Hash.new( 1 )
637
740
 
638
- def parameter_check(parameters)
639
- parameters.each_key do |key|
640
- raise "Bad parameter: #{key}" unless ALL_PARAMETERS.include? key.to_s
741
+ [ self, operation ].each do |op|
742
+ rg_hash = op.response_group.params
743
+
744
+ if m = rg_hash.to_a[0][0].match( /^(.+)\..+\..+$/ )
745
+ # This hash is already in batch format.
746
+ #
747
+ rg.merge!( rg_hash )
748
+
749
+ # Keep a record of the highest index currently in use for each type
750
+ # of operation.
751
+ #
752
+ rg_hash.each do |key, val|
753
+ op_kind, index = key.match( /^(.+)\.(\d+)\..+$/ )[1, 2]
754
+ if index.to_i == op_count[op_kind]
755
+ op_count[op_kind] += 1
756
+ end
757
+ end
758
+
759
+ else
760
+ # Convert hash to batch format.
761
+ #
762
+ rg_hash.each_value do |val|
763
+ rg_str = '%s.%d.ResponseGroup' % [ op.kind, op_count[op.kind] ]
764
+ op_count[op.kind] += 1
765
+ rg[rg_str] = val
766
+ end
641
767
  end
768
+
642
769
  end
643
- private :parameter_check
644
770
 
771
+ rg
645
772
  end
646
773
 
647
774
 
648
- # This class can be used to merge operations into a single operation.
649
- # AWS currently supports combining two operations,
775
+ # This class can be used to merge multiple operations into a single
776
+ # operation for greater efficiency.
650
777
  #
651
778
  class MultipleOperation < Operation
652
779
 
653
- # This will allow you to take two Operation objects and combine them to
654
- # form a single object, which can then be used to perform searches. AWS
655
- # itself imposes the maximum of two combined operations.
780
+ # This allows you to take two Operation objects and combine them to form
781
+ # a single object, which can then be used to perform a single request to
782
+ # AWS. This allows for greater efficiency, reducing the number of
783
+ # requests sent to AWS.
784
+ #
785
+ # AWS currently imposes a limit of two operations when encapsulating
786
+ # operations in a multiple operation.
656
787
  #
657
788
  # <em>operation1</em> and <em>operation2</em> are both objects from a
658
789
  # subclass of Operation, such as ItemSearch, ItemLookup, etc.
659
790
  #
660
- # There are currently a few restrictions in the Ruby/AWS implementation
661
- # of multiple operations:
791
+ # Please note the following implementation details:
792
+ #
793
+ # - If you use the _response_group_ parameter of Search::Request#search
794
+ # to pass the list of response groups, it will apply to both
795
+ # operations.
662
796
  #
663
- # - ResponseGroup objects used when calling AWS::Search::Request#search
664
- # apply to both operations. You cannot have a separate ResponseGroup
665
- # set per operation.
797
+ # If you want to use a different response group set for each
798
+ # operation, you should assign the relevant groups to the
799
+ # @response_group attribute of each Operation object. You must do this
800
+ # *before* you instantiate the MultipleOperation.
666
801
  #
667
802
  # - One or both operations may have multiple results pages available,
668
- # but only the first page can be returned. If you need the other
803
+ # but only the first page is returned. If you need the subsequent
669
804
  # pages, perform the operations separately, not as part of a
670
- # MultipleOperation.
805
+ # multiple operation.
671
806
  #
672
807
  # Example:
673
808
  #
674
809
  # is = ItemSearch.new( 'Books', { 'Title' => 'Ruby' } )
675
810
  # il = ItemLookup.new( 'ASIN', { 'ItemId' => 'B0013DZAYO',
676
811
  # 'MerchantId' => 'Amazon' } )
677
- # mo = MultipleOperation.new( is, il )
812
+ # is.response_group = ResponseGroup.new( :Large )
813
+ # il.response_group = ResponseGroup.new( :Small )
814
+ # mo = MultipleOperation.new( is, il )
678
815
  #
679
- # In the above example, we compose a multiple operation consisting of an
680
- # ItemSearch and an ItemLookup.
816
+ # As you can see, the operations that are combined as a
817
+ # MultipleOperation do not have to belong to the same class. In the
818
+ # above example, we compose a multiple operation consisting of an
819
+ # ItemSearch and an ItemLookup.
820
+ #
821
+ # If you want to batch operations belonging to the same class,
822
+ # Operation#batch provides an alternative.
681
823
  #
682
824
  def initialize(operation1, operation2)
683
825
 
684
- # Safeguard against changing original Operation objects in place. This
685
- # is to protect me, not for user code.
686
- #
687
- operation1.freeze
688
- operation2.freeze
689
-
690
- op_kind = '%s,%s' % [ operation1.kind, operation2.kind ]
826
+ op_kind = [ operation1.kind, operation2.kind ].join( ',' )
691
827
 
692
828
  # Duplicate Operation objects and remove their Operation parameter.
693
829
  #
694
830
  op1 = operation1.dup
695
- op1.params = op1.params.dup
696
831
  op1.params.delete( 'Operation' )
697
832
 
698
833
  op2 = operation2.dup
699
- op2.params = op2.params.dup
700
834
  op2.params.delete( 'Operation' )
701
835
 
702
836
  if op1.class == op2.class
@@ -717,6 +851,9 @@ module Amazon
717
851
  params = { 'Operation' => op_kind }.merge( b_params )
718
852
  super( params )
719
853
 
854
+ @response_group = ResponseGroup.new( [] )
855
+ @response_group.params.delete( 'ResponseGroup' )
856
+ @response_group.params = op1.batch_response_groups( op2 )
720
857
  end
721
858
 
722
859
  end
@@ -737,8 +874,8 @@ module Amazon
737
874
  #
738
875
  # _help_type_ is the type of object for which help is being sought, such
739
876
  # as *Operation* or *ResponseGroup*. _about_ is the name of the
740
- # operation or response group you need help with, and _parameters_ is a
741
- # hash of parameters that serve to further refine the request for help.
877
+ # operation or response group you need help with, and _parameters_ is an
878
+ # optional hash of parameters that further refine the request for help.
742
879
  #
743
880
  def initialize(help_type, about, parameters={})
744
881
  super( { 'HelpType' => help_type,
@@ -764,9 +901,9 @@ module Amazon
764
901
  #
765
902
  # - *All* searches through all indices (but currently exists only in the
766
903
  # *US* locale).
767
- # - *Blended* combines DVD, Electronics, Toys, VideoGames, PCHardware,
768
- # Tools, SportingGoods, Books, Software, Music, GourmetFood, Kitchen
769
- # and Apparel.
904
+ # - *Blended* combines Apparel, Automotive, Books, DVD, Electronics,
905
+ # GourmetFood, Kitchen, Music, PCHardware, PetSupplies, Software,
906
+ # SoftwareVideoGames, SportingGoods, Tools, Toys, VHS and VideoGames.
770
907
  # - *Merchants* combines all search indices for a merchant given with
771
908
  # MerchantId.
772
909
  # - *Music* combines the Classical, DigitalMusic, and MusicTracks
@@ -774,25 +911,60 @@ module Amazon
774
911
  # - *Video* combines the DVD and VHS search indices.
775
912
  #
776
913
  SEARCH_INDICES = %w[
777
- All
778
- Apparel Hobbies PetSupplies
779
- Automotive HomeGarden Photo
780
- Baby Jewelry Software
781
- Beauty Kitchen SoftwareVideoGames
782
- Blended Magazines SportingGoods
783
- Books Merchants Tools
784
- Classical Miscellaneous Toys
785
- DigitalMusic Music VHS
786
- DVD MusicalInstruments Video
787
- Electronics MusicTracks VideoGames
788
- ForeignBooks OfficeProducts Wireless
789
- GourmetFood OutdoorLiving WirelessAccessories
790
- HealthPersonalCare PCHardware
791
- ]
914
+ All
915
+ Apparel
916
+ Automotive
917
+ Baby
918
+ Beauty
919
+ Blended
920
+ Books
921
+ Classical
922
+ DigitalMusic
923
+ DVD
924
+ Electronics
925
+ ForeignBooks
926
+ GourmetFood
927
+ Grocery
928
+ HealthPersonalCare
929
+ Hobbies
930
+ HomeGarden
931
+ HomeImprovement
932
+ Industrial
933
+ Jewelry
934
+ KindleStore
935
+ Kitchen
936
+ Magazines
937
+ Merchants
938
+ Miscellaneous
939
+ MP3Downloads
940
+ Music
941
+ MusicalInstruments
942
+ MusicTracks
943
+ OfficeProducts
944
+ OutdoorLiving
945
+ PCHardware
946
+ PetSupplies
947
+ Photo
948
+ Shoes
949
+ SilverMerchants
950
+ Software
951
+ SoftwareVideoGames
952
+ SportingGoods
953
+ Tools
954
+ Toys
955
+ UnboxVideo
956
+ VHS
957
+ Video
958
+ VideoGames
959
+ Watches
960
+ Wireless
961
+ WirelessAccessories
962
+ ]
792
963
 
793
964
 
794
965
  # Search AWS for items. _search_index_ must be one of _SEARCH_INDICES_
795
- # and _parameters_ is a hash of relevant search parameters.
966
+ # and _parameters_ is an optional hash of parameters that further refine
967
+ # the scope of the search.
796
968
  #
797
969
  # Example:
798
970
  #
@@ -806,7 +978,6 @@ module Amazon
806
978
  raise "Invalid search index: #{search_index}"
807
979
  end
808
980
 
809
- parameter_check( parameters )
810
981
  super( { 'SearchIndex' => search_index }.merge( parameters ) )
811
982
  end
812
983
 
@@ -820,33 +991,20 @@ module Amazon
820
991
  class ItemLookup < Operation
821
992
 
822
993
  # Look up a specific item in the AWS catalogue. _id_type_ is the type of
823
- # identifier, _parameters_ is a hash that identifies the item to be
824
- # located and narrows the scope of the search, and _b_parameters_ is an
825
- # optional hash of further items to be located. Use of _b_parameters_
826
- # effectively results in a batch operation being sent to AWS.
994
+ # identifier and _parameters_ is a hash that identifies the item to be
995
+ # located and narrows the scope of the search.
827
996
  #
828
997
  # Example:
829
998
  #
830
999
  # il = ItemLookup.new( 'ASIN', { 'ItemId' => 'B000AE4QEC'
831
- # 'MerchantId' => 'Amazon' },
832
- # { 'ItemId' => 'B000051WBE',
833
1000
  # 'MerchantId' => 'Amazon' } )
834
1001
  #
835
- # In the above example, we search for two items, based on their ASIN.
836
- # The use of _MerchantId_ restricts the offers returned to those for
837
- # sale by Amazon (as opposed to third-party sellers).
1002
+ # In the above example, we search for an item, based on its ASIN. The
1003
+ # use of _MerchantId_ restricts the offers returned to those for sale
1004
+ # by Amazon (as opposed to third-party sellers).
838
1005
  #
839
- def initialize(id_type, parameters, *b_parameters)
840
-
841
- id_type_str = 'IdType'
842
-
843
- unless b_parameters.empty?
844
- class_str = self.class.to_s.sub( /^.+::/, '' )
845
- id_type_str = '%s.Shared.IdType' % [ class_str ]
846
- parameters = batch_parameters( parameters, *b_parameters )
847
- end
848
-
849
- super( { id_type_str => id_type }.merge( parameters ) )
1006
+ def initialize(id_type, parameters)
1007
+ super( { 'IdType' => id_type }.merge( parameters ) )
850
1008
  end
851
1009
 
852
1010
  end
@@ -857,8 +1015,8 @@ module Amazon
857
1015
  class SellerListingSearch < Operation
858
1016
 
859
1017
  # Search for items for sale by a particular seller. _seller_id_ is the
860
- # Amazon seller ID and _parameters_ is a hash of parameters that narrows
861
- # the scope of the search.
1018
+ # Amazon seller ID and _parameters_ is an optional hash of parameters
1019
+ # that further refine the scope of the search.
862
1020
  #
863
1021
  # Example:
864
1022
  #
@@ -880,11 +1038,8 @@ module Amazon
880
1038
  class SellerListingLookup < ItemLookup
881
1039
 
882
1040
  # Look up a specific item for sale by a specific seller. _id_type_ is
883
- # the type of identifier, _parameters_ is a hash that identifies the
884
- # item to be located and narrows the scope of the search, and
885
- # _b_parameters_ is an optional hash of further items to be located. Use
886
- # of _b_parameters_ effectively results in a batch operation being sent
887
- # to AWS.
1041
+ # the type of identifier and _parameters_ is a hash that identifies the
1042
+ # item to be located and narrows the scope of the search.
888
1043
  #
889
1044
  # Example:
890
1045
  #
@@ -894,9 +1049,8 @@ module Amazon
894
1049
  # In the above example, we search seller <b>AP8U6Y3PYQ9VO</b>'s listings
895
1050
  # to find items for sale with the ASIN <b>B0009RRRC8</b>.
896
1051
  #
897
- def initialize(seller_id, id_type, parameters, *b_parameters)
898
- super( id_type, { 'SellerId' => seller_id }.merge( parameters ),
899
- b_parameters )
1052
+ def initialize(seller_id, id_type, parameters)
1053
+ super( id_type, { 'SellerId' => seller_id }.merge( parameters ) )
900
1054
  end
901
1055
 
902
1056
  end
@@ -907,8 +1061,8 @@ module Amazon
907
1061
  class SellerLookup < Operation
908
1062
 
909
1063
  # Search for the details of a specific seller. _seller_id_ is the Amazon
910
- # ID of the seller in question and _parameters_ is a hash of parameters
911
- # that serve to further refine the search.
1064
+ # ID of the seller in question and _parameters_ is an optional hash of
1065
+ # parameters that further refine the scope of the search.
912
1066
  #
913
1067
  # Example:
914
1068
  #
@@ -930,8 +1084,8 @@ module Amazon
930
1084
  class CustomerContentLookup < Operation
931
1085
 
932
1086
  # Search for public customer data. _customer_id_ is the unique ID
933
- # identifying the customer on Amazon and _parameters_ is a hash of
934
- # parameters that serve to further refine the search.
1087
+ # identifying the customer on Amazon and _parameters_ is an optional
1088
+ # hash of parameters that further refine the scope of the search.
935
1089
  #
936
1090
  # Example:
937
1091
  #
@@ -981,8 +1135,8 @@ module Amazon
981
1135
  class ListSearch < Operation
982
1136
 
983
1137
  # Search for Amazon lists. _list_type_ is the type of list to search for
984
- # and _parameters_ is a hash of parameters that narrows the scope of the
985
- # search.
1138
+ # and _parameters_ is an optional hash of parameters that narrow the
1139
+ # scope of the search.
986
1140
  #
987
1141
  # Example:
988
1142
  #
@@ -1003,8 +1157,8 @@ module Amazon
1003
1157
  class ListLookup < Operation
1004
1158
 
1005
1159
  # Look up and return details about a specific list. _list_id_ is the
1006
- # Amazon list ID, _list_type_ is the type of list and _parameters_ is a
1007
- # hash of parameters that narrows the scope of the search.
1160
+ # Amazon list ID, _list_type_ is the type of list and _parameters_ is an
1161
+ # optional hash of parameters that narrow the scope of the search.
1008
1162
  #
1009
1163
  # Example:
1010
1164
  #
@@ -1014,7 +1168,7 @@ module Amazon
1014
1168
  # <b>3P722DU4KUPCP</b> is retrieved from AWS.
1015
1169
  #
1016
1170
  def initialize(list_id, list_type, parameters={})
1017
- super( { 'ListId' => list_id,
1171
+ super( { 'ListId' => list_id,
1018
1172
  'ListType' => list_type
1019
1173
  }.merge( parameters ) )
1020
1174
  end
@@ -1030,8 +1184,9 @@ module Amazon
1030
1184
  class BrowseNodeLookup < Operation
1031
1185
 
1032
1186
  # Look up and return the details of an Amazon browse node. _node_ is the
1033
- # browse node to look up and _parameters_ is a hash of parameters that
1034
- # serves to further define the search. _parameters_ is currently unused.
1187
+ # browse node to look up and _parameters_ is an optional hash of
1188
+ # parameters that further refine the scope of the search. _parameters_
1189
+ # is currently unused.
1035
1190
  #
1036
1191
  # Example:
1037
1192
  #
@@ -1052,8 +1207,8 @@ module Amazon
1052
1207
  class SimilarityLookup < Operation
1053
1208
 
1054
1209
  # Look up items similar to _asin_, which can be a single item or an
1055
- # array. _parameters_ is a hash of parameters that serve to further
1056
- # refine the search.
1210
+ # array. _parameters_ is an optional hash of parameters that further
1211
+ # refine the scope of the search.
1057
1212
  #
1058
1213
  # Example:
1059
1214
  #
@@ -1076,8 +1231,8 @@ module Amazon
1076
1231
  class TagLookup < Operation
1077
1232
 
1078
1233
  # Look up entities based on user-defined tags. _tag_name_ is the tag to
1079
- # search on and _parameters_ is a hash of parameters that serve to
1080
- # further refine the search.
1234
+ # search on and _parameters_ is an optional hash of parameters that
1235
+ # further refine the scope of the search.
1081
1236
  #
1082
1237
  # Example:
1083
1238
  #
@@ -1115,28 +1270,149 @@ module Amazon
1115
1270
  end
1116
1271
 
1117
1272
 
1273
+ # Look up individual vehicle parts.
1274
+ #
1275
+ class VehiclePartLookup < Operation
1276
+
1277
+ # Look up a particular vehicle part. _item_id_ is the ASIN of the part
1278
+ # in question and _parameters_ is an optional hash of parameters that
1279
+ # further refine the scope of the search.
1280
+ #
1281
+ # Although the _item_id_ alone is enough to locate the part, providing
1282
+ # _parameters_ can be useful in determining whether the part looked up
1283
+ # is a fit for a particular vehicle type, as with the *VehiclePartFit*
1284
+ # response group.
1285
+ #
1286
+ # Example:
1287
+ #
1288
+ # vpl = VehiclePartLookup.new( 'B000C1ZLI8',
1289
+ # { 'Year' => 2008,
1290
+ # 'MakeId' => 73,
1291
+ # 'ModelId' => 6039,
1292
+ # 'TrimId' => 20 } )
1293
+ #
1294
+ # Here, we search for a <b>2008</b> model *Audi* <b>R8</b> with *Base*
1295
+ # trim. The required Ids can be found using VehiclePartSearch.
1296
+ #
1297
+ def initialize(item_id, parameters={})
1298
+ super( { 'ItemId' => item_id }.merge( parameters ) )
1299
+ end
1300
+
1301
+ end
1302
+
1303
+
1304
+ # Search for parts for a given vehicle.
1305
+ #
1306
+ class VehiclePartSearch < Operation
1307
+
1308
+ # Find parts for a given _year_, _make_id_ and _model_id_ of vehicle.
1309
+ # _parameters_ is an optional hash of parameters that further refine the
1310
+ # scope of the search.
1311
+ #
1312
+ # Example:
1313
+ #
1314
+ # vps = VehiclePartSearch.new( 2008, 73, 6039,
1315
+ # { 'TrimId' => 20,
1316
+ # 'EngineId' => 8914 } )
1317
+ #
1318
+ # In this example, we look for parts that will fit a <b>2008</b> model
1319
+ # *Audi* <b>R8</b> with *Base* trim and a <b>4.2L V8 Gas DOHC
1320
+ # Distributorless Naturally Aspirated Bosch Motronic Electronic FI
1321
+ # MFI</b> engine.
1322
+ #
1323
+ # Note that pagination of VehiclePartSearch results is not currently
1324
+ # supported.
1325
+ #
1326
+ # Use VehicleSearch to learn the MakeId and ModelId of the vehicle in
1327
+ # which you are interested.
1328
+ #
1329
+ def initialize(year, make_id, model_id, parameters={})
1330
+ super( { 'Year' => year,
1331
+ 'MakeId' => make_id,
1332
+ 'ModelId' => model_id }.merge( parameters ) )
1333
+ end
1334
+
1335
+ end
1336
+
1337
+
1338
+ # Search for vehicles.
1339
+ #
1340
+ class VehicleSearch < Operation
1341
+
1342
+ # Search for vehicles, based on one or more of the following
1343
+ # _parameters_: Year, MakeId, ModelId and TrimId.
1344
+ #
1345
+ # This method is best used iteratively. For example, first search on
1346
+ # year with a response group of *VehicleMakes* to return all makes for
1347
+ # that year.
1348
+ #
1349
+ # Next, search on year and make with a response group of *VehicleModels*
1350
+ # to find all models for that year and make.
1351
+ #
1352
+ # Then, search on year, make and model with a response group of
1353
+ # *VehicleTrims* to find all trim packages for that year, make and model.
1354
+ #
1355
+ # Finally, if required, search on year, make, model and trim package
1356
+ # with a response group of *VehicleOptions* to find all vehicle options
1357
+ # for that year, make, model and trim package.
1358
+ #
1359
+ # Example:
1360
+ #
1361
+ # vs = VehicleSearch.new( { 'Year' => 2008,
1362
+ # 'MakeId' => 20,
1363
+ # 'ModelId' => 6039,
1364
+ # 'TrimId' => 20 } )
1365
+ #
1366
+ # In this example, we search for <b>2008 Audi R8</b> vehicles with a
1367
+ # *Base* trim package. Used with the *VehicleOptions* response group,
1368
+ # a list of vehicle options would be returned.
1369
+ #
1370
+ def initialize(parameters={})
1371
+ super
1372
+ end
1373
+
1374
+ end
1375
+
1118
1376
  # Response groups determine which data pertaining to the item(s) being
1119
- # sought is returned. They can strongly influence the amount of data
1120
- # returned, so you should always use the smallest response group(s)
1121
- # containing the data of interest to you, to avoid masses of unnecessary
1122
- # data being returned.
1377
+ # sought is returned. They strongly influence the amount of data returned,
1378
+ # so you should always use the smallest response group(s) containing the
1379
+ # data of interest to you, to avoid masses of unnecessary data being
1380
+ # returned.
1123
1381
  #
1124
1382
  class ResponseGroup
1125
1383
 
1126
- attr_reader :list, :params
1384
+ # The default type of response group to use with each type of operation.
1385
+ #
1386
+ DEFAULT = { 'BrowseNodeLookup' => [ :BrowseNodeInfo, :TopSellers ],
1387
+ 'CustomerContentLookup' => [ :CustomerInfo, :CustomerLists ],
1388
+ 'CustomerContentSearch' => :CustomerInfo,
1389
+ 'Help' => :Help,
1390
+ 'ItemLookup' => :Large,
1391
+ 'ItemSearch' => :Large,
1392
+ 'ListLookup' => [ :ListInfo, :Small ],
1393
+ 'ListSearch' => :ListInfo,
1394
+ 'SellerListingLookup' => :SellerListing,
1395
+ 'SellerListingSearch' => :SellerListing,
1396
+ 'SellerLookup' => :Seller,
1397
+ 'SimilarityLookup' => :Large,
1398
+ 'TagLookup' => [ :Tags, :TagsSummary ],
1399
+ 'TransactionLookup' => :TransactionDetails,
1400
+ 'VehiclePartLookup' => :VehiclePartFit,
1401
+ 'VehiclePartSearch' => :VehicleParts,
1402
+ 'VehicleSearch' => :VehicleMakes
1403
+ }
1404
+
1405
+ attr_reader :list
1406
+ attr_accessor :params
1127
1407
 
1128
1408
  # Define a set of one or more response groups to be applied to items
1129
1409
  # retrieved by an AWS operation.
1130
1410
  #
1131
- # If no response groups are given in _rg_ when instantiating an object,
1132
- # *Small* will be used by default.
1133
- #
1134
1411
  # Example:
1135
1412
  #
1136
1413
  # rg = ResponseGroup.new( 'Medium', 'Offers', 'Reviews' )
1137
1414
  #
1138
1415
  def initialize(*rg)
1139
- rg << 'Small' if rg.empty?
1140
1416
  @list = rg
1141
1417
  @params = { 'ResponseGroup' => @list.join( ',' ) }
1142
1418
  end
@@ -1154,13 +1430,19 @@ module Amazon
1154
1430
  class AWSError < AmazonError; end
1155
1431
 
1156
1432
  def Error.exception(xml)
1157
- err_class = xml.elements['Code'].text.sub( /^AWS.*\./, '' )
1158
- err_msg = xml.elements['Message'].text
1433
+ err_class = xml.elements['Code'].text.sub( /^AWS.*\./, '' )
1434
+ err_msg = xml.elements['Message'].text
1159
1435
 
1160
1436
  # Dynamically define a new exception class for this class of error,
1161
1437
  # unless it already exists.
1162
1438
  #
1163
- unless Amazon::AWS::Error.const_defined?( err_class )
1439
+ # Note that Ruby 1.9's Module.const_defined? needs a second parameter
1440
+ # of *false*, or it will also search AWSError's ancestors.
1441
+ #
1442
+ cd_params = [ err_class ]
1443
+ cd_params << false if RUBY_VERSION >= '1.9.0'
1444
+
1445
+ unless Amazon::AWS::Error.const_defined?( *cd_params )
1164
1446
  Amazon::AWS::Error.const_set( err_class, Class.new( AWSError ) )
1165
1447
  end
1166
1448
 
@@ -1171,6 +1453,59 @@ module Amazon
1171
1453
 
1172
1454
  end
1173
1455
 
1456
+
1457
+ # Create a shorthand module method for each of the AWS operations. These
1458
+ # can be used to create less verbose code at the expense of flexibility.
1459
+ #
1460
+ # For example, we might normally write the following code:
1461
+ #
1462
+ # is = ItemSearch.new( 'Books', { 'Title' => 'Ruby' } )
1463
+ # rg = ResponseGroup.new( 'Large' )
1464
+ # req = Request.new
1465
+ # response = req.search( is, rg )
1466
+ #
1467
+ # but we could instead use ItemSearch's associated module method as
1468
+ # follows:
1469
+ #
1470
+ # response = Amazon::AWS.item_search( 'Books', { 'Title' => 'Ruby' } )
1471
+ #
1472
+ # Note that these equivalent module methods all attempt to use the *Large*
1473
+ # response group, which may or may not work. If an
1474
+ # Amazon::AWS::Error::InvalidResponseGroup is raised, we will scan the
1475
+ # text of the error message returned by AWS to try to glean a valid
1476
+ # response group and then retry the operation using that instead.
1477
+
1478
+
1479
+ # Ontain a list of all subclasses of the Operation class.
1480
+ #
1481
+ classes =
1482
+ ObjectSpace.enum_for( :each_object, class << Operation; self; end ).to_a
1483
+
1484
+ classes.each do |cl|
1485
+ # Convert class name to Ruby case, e.g. ItemSearch => item_search.
1486
+ #
1487
+ class_name = cl.to_s.sub( /^.+::/, '' )
1488
+ uncamelised_name = Amazon.uncamelise( class_name )
1489
+
1490
+ # Define the module method counterpart of each operation.
1491
+ #
1492
+ module_eval %Q(
1493
+ def AWS.#{uncamelised_name}(*params)
1494
+ # Instantiate an object of the desired operational class.
1495
+ #
1496
+ op = #{cl.to_s}.new( *params )
1497
+
1498
+ # Attempt a search for the given operation using its default
1499
+ # response group types.
1500
+ #
1501
+ results = Search::Request.new.search( op )
1502
+ yield results if block_given?
1503
+ return results
1504
+
1505
+ end
1506
+ )
1507
+ end
1508
+
1174
1509
  end
1175
1510
 
1176
1511
  end