ruby-aaws 0.4.1

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