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