kernow-ruby-aaws 0.5.4

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.
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