kernow-ruby-aaws 0.5.4

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