bare-ruby-aws 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,144 @@
1
+ # $Id: amazon.rb,v 1.33 2010/03/19 17:20:46 ianmacd Exp $
2
+ #
3
+
4
+ module Amazon
5
+
6
+ # A top-level exception container class.
7
+ #
8
+ class AmazonError < StandardError; end
9
+
10
+ NAME = 'Ruby/Amazon'
11
+
12
+ @@config = {}
13
+
14
+ # We're going to have to use String#size if String#bytesize isn't available.
15
+ # This is for Ruby pre-1.8.7.
16
+ #
17
+ unless String.instance_methods.include? 'bytesize'
18
+ String.module_eval( 'alias :bytesize :size' )
19
+ end
20
+
21
+ # Prints debugging messages and works like printf, except that it prints
22
+ # only when Ruby is run with the -d switch.
23
+ #
24
+ def Amazon.dprintf(format='', *args)
25
+ $stderr.printf( format + "\n", *args ) if $DEBUG
26
+ end
27
+
28
+ # Encode a string, such that it is suitable for HTTP transmission.
29
+ #
30
+ def Amazon.url_encode(string)
31
+
32
+ # Shamelessly plagiarised from Wakou Aoyama's cgi.rb, but then altered
33
+ # slightly to please AWS.
34
+ #
35
+ string.gsub( /([^a-zA-Z0-9_.~-]+)/ ) do
36
+ '%' + $1.unpack( 'H2' * $1.bytesize ).join( '%' ).upcase
37
+ end
38
+ end
39
+
40
+ # Convert a string from CamelCase to ruby_case.
41
+ #
42
+ def Amazon.uncamelise(string)
43
+ # Avoid modifying by reference.
44
+ #
45
+ string = string.dup
46
+
47
+ # Don't mess with string if all caps.
48
+ #
49
+ if string =~ /[a-z]/
50
+ string.gsub!( /(.+?)(([A-Z][a-z]|[A-Z]+$))/, "\\1_\\2" )
51
+ end
52
+
53
+ # Convert to lower case.
54
+ #
55
+ string.downcase
56
+ end
57
+
58
+
59
+ # A Class for dealing with configuration files, such as
60
+ # <tt>/etc/amazonrc</tt> and <tt>~/.amazonrc</tt>.
61
+ #
62
+ class Config < Hash
63
+
64
+ require 'stringio'
65
+
66
+ # Exception class for configuration file errors.
67
+ #
68
+ class ConfigError < AmazonError; end
69
+
70
+ # A configuration may be passed in as a string. Otherwise, the files
71
+ # <tt>/etc/amazonrc</tt> and <tt>~/.amazonrc</tt> are read if they exist
72
+ # and are readable.
73
+ #
74
+ def initialize(config_str)
75
+ locale = nil
76
+
77
+ if config_str
78
+
79
+ # We have been passed a config file as a string.
80
+ #
81
+ config_files = [ config_str ]
82
+ config_class = File
83
+
84
+ else
85
+ puts 'No config file specified'
86
+
87
+ end
88
+
89
+ config_files.each do |cf|
90
+
91
+ # We must determine whether the file is readable.
92
+ #
93
+ readable = File.exists?( cf ) && File.readable?( cf )
94
+
95
+ if readable
96
+
97
+ Amazon.dprintf( 'Opening %s ...', cf ) if config_class == File
98
+
99
+ config_class.open( cf ) { |f| lines = f.readlines }.each do |line|
100
+ line.chomp!
101
+
102
+ # Skip comments and blank lines.
103
+ #
104
+ next if line =~ /^(#|$)/
105
+
106
+ Amazon.dprintf( 'Read: %s', line )
107
+
108
+ # Determine whether we're entering the subsection of a new locale.
109
+ #
110
+ if match = line.match( /^\[(\w+)\]$/ )
111
+ locale = match[1]
112
+ Amazon.dprintf( "Config locale is now '%s'.", locale )
113
+ next
114
+ end
115
+
116
+ # Store these, because we'll probably find a use for these later.
117
+ #
118
+ begin
119
+ match = line.match( /^\s*(\S+)\s*=\s*(['"]?)([^'"]+)(['"]?)/ )
120
+ key, begin_quote, val, end_quote = match[1, 4]
121
+ raise ConfigError if begin_quote != end_quote
122
+
123
+ rescue NoMethodError, ConfigError
124
+ raise ConfigError, "bad config line: #{line}"
125
+ end
126
+
127
+ if locale && locale != 'global'
128
+ self[locale] ||= {}
129
+ self[locale][key] = val
130
+ else
131
+ self[key] = val
132
+ end
133
+
134
+ end
135
+ else
136
+ puts "could not open file"
137
+ end
138
+
139
+ end
140
+
141
+ end
142
+ end
143
+
144
+ end
@@ -0,0 +1,963 @@
1
+ # $Id: aws.rb,v 1.130 2010/03/20 11:58:50 ianmacd Exp $
2
+ #
3
+ #:include: ../../README.rdoc
4
+
5
+ module Amazon
6
+
7
+ module AWS
8
+
9
+ require 'amazon'
10
+ require 'amazon/aws/cache'
11
+ require 'enumerator'
12
+ require 'iconv'
13
+ require 'rexml/document'
14
+ require 'uri'
15
+
16
+ NAME = '%s/%s' % [ Amazon::NAME, 'AWS' ]
17
+ VERSION = '0.8.1'
18
+ USER_AGENT = '%s %s' % [ NAME, VERSION ]
19
+
20
+ # Default Associate tags to use per locale.
21
+ #
22
+ DEF_ASSOC = {
23
+ 'ca' => 'caliban-20',
24
+ 'de' => 'calibanorg0a-21',
25
+ 'fr' => 'caliban08-21',
26
+ 'jp' => 'calibanorg-20',
27
+ 'uk' => 'caliban-21',
28
+ 'us' => 'calibanorg-20'
29
+ }
30
+
31
+ # Service name and API version for AWS. The version of the API used can be
32
+ # changed via the user configuration file.
33
+ #
34
+ SERVICE = { 'Service' => 'AWSECommerceService',
35
+ 'Version' => '2009-11-01'
36
+ }
37
+
38
+ # Maximum number of 301 and 302 HTTP responses to follow, should Amazon
39
+ # later decide to change the location of the service.
40
+ #
41
+ MAX_REDIRECTS = 3
42
+
43
+ # Maximum number of results pages that can be retrieved for a given
44
+ # search operation, using whichever pagination parameter is appropriate
45
+ # for that kind of operation.
46
+ #
47
+ PAGINATION = {
48
+ 'ItemSearch' => { 'parameter' => 'ItemPage',
49
+ 'max_page' => 400 },
50
+ 'ItemLookup' => { 'parameter' => 'OfferPage',
51
+ 'max_page' => 100 },
52
+ 'ListLookup' => { 'parameter' => 'ProductPage',
53
+ 'max_page' => 30 },
54
+ 'ListSearch' => { 'parameter' => 'ListPage',
55
+ 'max_page' => 20 },
56
+ 'CustomerContentLookup' => { 'parameter' => 'ReviewPage',
57
+ 'max_page' => 10 },
58
+ 'CustomerContentSearch' => { 'parameter' => 'CustomerPage',
59
+ 'max_page' => 20 },
60
+ 'VehiclePartLookup' => { 'parameter' => 'FitmentPage',
61
+ 'max_page' => 10 }
62
+ }
63
+ # N.B. ItemLookup can also use the following two pagination parameters
64
+ #
65
+ # max. page
66
+ # ---------
67
+ # VariationPage 150
68
+ # ReviewPage 20
69
+
70
+
71
+ # A hash to store character encoding converters.
72
+ #
73
+ @@encodings = {}
74
+
75
+
76
+ # Exception class for HTTP errors.
77
+ #
78
+ class HTTPError < AmazonError; end
79
+
80
+
81
+ # Exception class for faulty batch operations.
82
+ #
83
+ class BatchError < AmazonError; end
84
+
85
+
86
+ # Exception class for obsolete features.
87
+ #
88
+ class ObsolescenceError < AmazonError; end
89
+
90
+
91
+ class Endpoint
92
+
93
+ attr_reader :host, :path
94
+
95
+ def initialize(endpoint)
96
+ uri = URI.parse( endpoint )
97
+ @host = uri.host
98
+ @path = uri.path
99
+ end
100
+ end
101
+
102
+ ENDPOINT = {
103
+ 'ca' => Endpoint.new( 'http://ecs.amazonaws.ca/onca/xml' ),
104
+ 'de' => Endpoint.new( 'http://ecs.amazonaws.de/onca/xml' ),
105
+ 'fr' => Endpoint.new( 'http://ecs.amazonaws.fr/onca/xml' ),
106
+ 'jp' => Endpoint.new( 'http://ecs.amazonaws.jp/onca/xml' ),
107
+ 'uk' => Endpoint.new( 'http://ecs.amazonaws.co.uk/onca/xml' ),
108
+ 'us' => Endpoint.new( 'http://ecs.amazonaws.com/onca/xml' )
109
+ }
110
+
111
+
112
+ # Fetch a page, either from the cache or by HTTP. This is used internally.
113
+ #
114
+ def AWS.get_page(request) # :nodoc:
115
+
116
+ url = ENDPOINT[request.locale].path + request.query
117
+ cache_url = ENDPOINT[request.locale].host + url
118
+
119
+ # Check for cached page and return that if it's there.
120
+ #
121
+ if request.cache && request.cache.cached?( cache_url )
122
+ body = request.cache.fetch( cache_url )
123
+ return body if body
124
+ end
125
+
126
+ # Check whether we have a secret key available for signing the request.
127
+ # If so, sign the request for authentication.
128
+ #
129
+ if request.config['secret_key_id']
130
+ unless request.sign
131
+ Amazon.dprintf( 'Warning! Failed to sign request. No OpenSSL support for SHA256 digest.' )
132
+ end
133
+
134
+ url = ENDPOINT[request.locale].path + request.query
135
+ end
136
+
137
+ # Get the existing connection. If there isn't one, force a new one.
138
+ #
139
+ conn = request.conn || request.reconnect.conn
140
+ user_agent = request.user_agent
141
+
142
+ Amazon.dprintf( 'Fetching http://%s%s ...', conn.address, url )
143
+
144
+ begin
145
+ response = conn.get( url, { 'user-agent' => user_agent } )
146
+
147
+ # If we've pulled and processed a lot of pages from the cache (or
148
+ # just not passed by here recently), the HTTP connection to the server
149
+ # will probably have timed out.
150
+ #
151
+ rescue EOFError, Errno::ECONNABORTED, Errno::ECONNREFUSED,
152
+ Errno::ECONNRESET, Errno::EPIPE, Errno::ETIMEDOUT,
153
+ Timeout::Error => error
154
+ Amazon.dprintf( 'Connection to server lost: %s. Retrying...', error )
155
+ conn = request.reconnect.conn
156
+ retry
157
+ end
158
+
159
+ redirects = 0
160
+ while response.key? 'location'
161
+ if ( redirects += 1 ) > MAX_REDIRECTS
162
+ raise HTTPError, "More than #{MAX_REDIRECTS} redirections"
163
+ end
164
+
165
+ old_url = url
166
+ url = URI.parse( response['location'] )
167
+ url.scheme = old_url.scheme unless url.scheme
168
+ url.host = old_url.host unless url.host
169
+ Amazon.dprintf( 'Following HTTP %s to %s ...', response.code, url )
170
+ response = Net::HTTP::start( url.host ).
171
+ get( url.path, { 'user-agent' => user_agent } )
172
+ end
173
+
174
+ if response.code != '200'
175
+ raise HTTPError, "HTTP response code #{response.code}"
176
+ end
177
+
178
+ # Cache the page if we're using a cache.
179
+ #
180
+ if request.cache
181
+ request.cache.store( cache_url, response.body )
182
+ end
183
+
184
+ response.body
185
+ end
186
+
187
+
188
+ def AWS.assemble_query(items, encoding=nil) # :nodoc:
189
+
190
+ query = ''
191
+ @@encodings[encoding] ||= Iconv.new( 'utf-8', encoding ) if encoding
192
+
193
+ # We must sort the items into an array to get reproducible ordering
194
+ # of the query parameters. Otherwise, URL caching would not work. We
195
+ # must also convert the parameter values to strings, in case Symbols
196
+ # have been used as the values.
197
+ #
198
+ items.sort { |a,b| a.to_s <=> b.to_s }.each do |k, v|
199
+ if encoding
200
+ query << '&%s=%s' %
201
+ [ k, Amazon.url_encode( @@encodings[encoding].iconv( v.to_s ) ) ]
202
+ else
203
+ query << '&%s=%s' % [ k, Amazon.url_encode( v.to_s ) ]
204
+ end
205
+ end
206
+
207
+ # Replace initial ampersand with question-mark.
208
+ #
209
+ query[0] = '?'
210
+
211
+ query
212
+ end
213
+
214
+
215
+ # Everything returned by AWS is an AWSObject.
216
+ #
217
+ class AWSObject
218
+
219
+ include REXML
220
+
221
+ # This method can be used to load AWSObject data previously serialised
222
+ # by Marshal.dump.
223
+ #
224
+ # Example:
225
+ #
226
+ # File.open( 'aws.dat' ) { |f| Amazon::AWS::AWSObject.load( f ) }
227
+ #
228
+ # Marshal.load cannot be used directly, because subclasses of AWSObject
229
+ # are dynamically defined as needed when AWS XML responses are parsed.
230
+ #
231
+ # Later attempts to load objects instantiated from these classes cause a
232
+ # problem for Marshal, because it knows nothing of classes that were
233
+ # dynamically defined by a separate process.
234
+ #
235
+ def AWSObject.load(io)
236
+ begin
237
+ Marshal.load( io )
238
+ rescue ArgumentError => ex
239
+ m = ex.to_s.match( /Amazon::AWS::AWSObject::([^ ]+)/ )
240
+ const_set( m[1], Class.new( AWSObject ) )
241
+
242
+ io.rewind
243
+ retry
244
+ end
245
+ end
246
+
247
+
248
+ # This method can be used to load AWSObject data previously serialised
249
+ # by YAML.dump.
250
+ #
251
+ # Example:
252
+ #
253
+ # File.open( 'aws.yaml' ) { |f| Amazon::AWS::AWSObject.yaml_load( f ) }
254
+ #
255
+ # The standard YAML.load cannot be used directly, because subclasses of
256
+ # AWSObject are dynamically defined as needed when AWS XML responses are
257
+ # parsed.
258
+ #
259
+ # Later attempts to load objects instantiated from these classes cause a
260
+ # problem for YAML, because it knows nothing of classes that were
261
+ # dynamically defined by a separate process.
262
+ #
263
+ def AWSObject.yaml_load(io)
264
+ io.each do |line|
265
+
266
+ # File data is external, so it's deemed unsafe when $SAFE > 0, which
267
+ # is the case with mod_ruby, for example, where $SAFE == 1.
268
+ #
269
+ # YAML data isn't eval'ed or anything dangerous like that, so we
270
+ # consider it safe to untaint it. If we don't, mod_ruby will complain
271
+ # when Module#const_defined? is invoked a few lines down from here.
272
+ #
273
+ line.untaint
274
+
275
+ m = line.match( /Amazon::AWS::AWSObject::([^ ]+)/ )
276
+ if m
277
+ cl_name = [ m[1] ]
278
+
279
+ # Module#const_defined? takes 2 parameters in Ruby 1.9.
280
+ #
281
+ cl_name << false if RUBY_VERSION >= '1.9.0'
282
+
283
+ unless AWSObject.const_defined?( *cl_name )
284
+ AWSObject.const_set( m[1], Class.new( AWSObject ) )
285
+ end
286
+
287
+ end
288
+ end
289
+
290
+ io.rewind
291
+ YAML.load( io )
292
+ end
293
+
294
+
295
+ def initialize(op=nil)
296
+ # The name of this instance variable must never clash with the
297
+ # uncamelised name of an Amazon tag.
298
+ #
299
+ # This is used to store the REXML::Text value of an element, which
300
+ # exists only when the element contains no children.
301
+ #
302
+ @__val__ = nil
303
+ @__op__ = op if op
304
+ end
305
+
306
+
307
+ def method_missing(method, *params)
308
+ iv = '@' + method.id2name
309
+
310
+ if instance_variables.include?( iv )
311
+
312
+ # Return the instance variable that matches the method called.
313
+ #
314
+ instance_variable_get( iv )
315
+ elsif instance_variables.include?( iv.to_sym )
316
+
317
+ # Ruby 1.9 Object#instance_variables method returns Array of Symbol,
318
+ # not String.
319
+ #
320
+ instance_variable_get( iv.to_sym )
321
+ elsif @__val__.respond_to?( method.id2name )
322
+
323
+ # If our value responds to the method in question, call the method
324
+ # on that.
325
+ #
326
+ @__val__.send( method.id2name )
327
+ else
328
+ nil
329
+ end
330
+ end
331
+ private :method_missing
332
+
333
+
334
+ def remove_val
335
+ remove_instance_variable( :@__val__ )
336
+ end
337
+ private :remove_val
338
+
339
+
340
+ # Iterator method for cycling through an object's properties and values.
341
+ #
342
+ def each # :yields: property, value
343
+ self.properties.each do |iv|
344
+ yield iv, instance_variable_get( "@#{iv}" )
345
+ end
346
+ end
347
+
348
+ alias :each_property :each
349
+
350
+
351
+ def inspect # :nodoc:
352
+ remove_val if instance_variable_defined?( :@__val__ ) && @__val__.nil?
353
+ str = super
354
+ str.sub( /@__val__=/, 'value=' ) if str
355
+ end
356
+
357
+
358
+ def to_s # :nodoc:
359
+ if instance_variable_defined?( :@__val__ )
360
+ return @__val__ if @__val__.is_a?( String )
361
+ remove_val
362
+ end
363
+
364
+ string = ''
365
+
366
+ # Assemble the object's details.
367
+ #
368
+ each { |iv, value| string << "%s = %s\n" % [ iv, value ] }
369
+
370
+ string
371
+ end
372
+
373
+ alias :to_str :to_s
374
+
375
+
376
+ def ==(other) # :nodoc:
377
+ @__val__.to_s == other
378
+ end
379
+
380
+
381
+ def =~(other) # :nodoc:
382
+ @__val__.to_s =~ other
383
+ end
384
+
385
+
386
+ # This alias makes the ability to determine an AWSObject's properties a
387
+ # little more intuitive. It's pretty much just an alias for the
388
+ # inherited <em>Object#instance_variables</em> method, with a little
389
+ # tidying.
390
+ #
391
+ def properties
392
+ # Make sure we remove the leading @.
393
+ #
394
+ iv = instance_variables.collect { |v| v = v[1..-1] }
395
+ iv.delete( '__val__' )
396
+ iv
397
+ end
398
+
399
+
400
+ # Provide a shortcut down to the data likely to be of most interest.
401
+ # This method is experimental and may be removed.
402
+ #
403
+ def kernel # :nodoc:
404
+ # E.g. Amazon::AWS::SellerListingLookup -> seller_listing_lookup
405
+ #
406
+ stub = Amazon.uncamelise( @__op__.class.to_s.sub( /^.+::/, '' ) )
407
+
408
+ # E.g. seller_listing_response
409
+ #
410
+ level1 = stub + '_response'
411
+
412
+ # E.g. seller_listing
413
+ #
414
+ level3 = stub.sub( /_[^_]+$/, '' )
415
+
416
+ # E.g. seller_listings
417
+ #
418
+ level2 = level3 + 's'
419
+
420
+ # E.g.
421
+ # seller_listing_search_response[0].seller_listings[0].seller_listing
422
+ #
423
+ self.instance_variable_get( "@#{level1}" )[0].
424
+ instance_variable_get( "@#{level2}" )[0].
425
+ instance_variable_get( "@#{level3}" )
426
+ end
427
+
428
+
429
+ # Convert an AWSObject to a Hash.
430
+ #
431
+ def to_h
432
+ hash = {}
433
+
434
+ each do |iv, value|
435
+ if value.is_a? AWSObject
436
+ hash[iv] = value.to_h
437
+ elsif value.is_a?( AWSArray ) && value.size == 1
438
+ hash[iv] = value[0]
439
+ else
440
+ hash[iv] = value
441
+ end
442
+ end
443
+
444
+ hash
445
+ end
446
+
447
+
448
+ # Fake the appearance of an AWSObject as a hash. _key_ should be any
449
+ # attribute of the object and can be a String, Symbol or anything else
450
+ # that can be converted to a String with to_s.
451
+ #
452
+ def [](key)
453
+ instance_variable_get( "@#{key}" )
454
+ end
455
+
456
+
457
+ # Recursively walk through an XML tree, starting from _node_. This is
458
+ # called internally and is not intended for user code.
459
+ #
460
+ def walk(node) # :nodoc:
461
+
462
+ if node.instance_of?( REXML::Document )
463
+ walk( node.root )
464
+
465
+ elsif node.instance_of?( REXML::Element )
466
+ name = Amazon.uncamelise( node.name )
467
+
468
+ cl_name = [ node.name ]
469
+
470
+ # Module#const_defined? takes 2 parameters in Ruby 1.9.
471
+ #
472
+ cl_name << false if RUBY_VERSION >= '1.9.0'
473
+
474
+ # Create a class for the new element type unless it already exists.
475
+ #
476
+ unless AWS::AWSObject.const_defined?( *cl_name )
477
+ cl = AWS::AWSObject.const_set( node.name, Class.new( AWSObject ) )
478
+
479
+ # Give it an accessor for @attrib.
480
+ #
481
+ cl.send( :attr_accessor, :attrib )
482
+ end
483
+
484
+ # Instantiate an object in the newly created class.
485
+ #
486
+ obj = AWS::AWSObject.const_get( node.name ).new
487
+
488
+ sym_name = "@#{name}".to_sym
489
+
490
+ if instance_variable_defined?( sym_name)
491
+ instance_variable_set( sym_name,
492
+ instance_variable_get( sym_name ) << obj )
493
+ else
494
+ instance_variable_set( sym_name, AWSArray.new( [ obj ] ) )
495
+ end
496
+
497
+ if node.has_attributes?
498
+ obj.attrib = {}
499
+ node.attributes.each_pair do |a_name, a_value|
500
+ obj.attrib[a_name.downcase] =
501
+ a_value.to_s.sub( /^#{a_name}=/, '' )
502
+ end
503
+ end
504
+
505
+ node.children.each { |child| obj.walk( child ) }
506
+
507
+ else # REXML::Text
508
+ @__val__ = node.to_s
509
+ end
510
+ end
511
+
512
+
513
+ # For objects of class AWSObject::.*Image, fetch the image in question,
514
+ # optionally overlaying a discount icon for the percentage amount of
515
+ # _discount_ to the image.
516
+ #
517
+ def get(discount=nil)
518
+ if self.class.to_s =~ /Image$/ && @url
519
+ url = URI.parse( @url[0] )
520
+ url.path.sub!( /(\.\d\d\._)/, "\\1PE#{discount}" ) if discount
521
+
522
+ # FIXME: All HTTP in Ruby/AWS should go through the same method.
523
+ #
524
+ Net::HTTP.start( url.host, url.port ) do |http|
525
+ http.get( url.path )
526
+ end.body
527
+
528
+ else
529
+ nil
530
+ end
531
+ end
532
+
533
+ end
534
+
535
+
536
+ # Everything we get back from AWS is transformed into an array. Many of
537
+ # these, however, have only one element, because the corresponding XML
538
+ # consists of a parent element containing only a single child element.
539
+ #
540
+ # This class consists solely to allow single element arrays to pass a
541
+ # method call down to their one element, thus obviating the need for lots
542
+ # of references to <tt>foo[0]</tt> in user code.
543
+ #
544
+ # For example, the following:
545
+ #
546
+ # items = resp.item_search_response[0].items[0].item
547
+ #
548
+ # can be reduced to:
549
+ #
550
+ # items = resp.item_search_response.items.item
551
+ #
552
+ class AWSArray < Array
553
+
554
+ def method_missing(method, *params)
555
+ self.size == 1 ? self[0].send( method, *params ) : super
556
+ end
557
+ private :method_missing
558
+
559
+
560
+ # In the case of a single-element array, return the first element,
561
+ # converted to a String.
562
+ #
563
+ def to_s # :nodoc:
564
+ self.size == 1 ? self[0].to_s : super
565
+ end
566
+
567
+ alias :to_str :to_s
568
+
569
+
570
+ # In the case of a single-element array, return the first element,
571
+ # converted to an Integer.
572
+ #
573
+ def to_i # :nodoc:
574
+ self.size == 1 ? self[0].to_i : super
575
+ end
576
+
577
+
578
+ # In the case of a single-element array, compare the first element with
579
+ # _other_.
580
+ #
581
+ def ==(other) # :nodoc:
582
+ self.size == 1 ? self[0].to_s == other : super
583
+ end
584
+
585
+
586
+ # In the case of a single-element array, perform a pattern match on the
587
+ # first element against _other_.
588
+ #
589
+ def =~(other) # :nodoc:
590
+ self.size == 1 ? self[0].to_s =~ other : super
591
+ end
592
+
593
+ end
594
+
595
+
596
+ # This is the base class of all AWS operations.
597
+ #
598
+ class Operation
599
+
600
+ # These are the types of AWS operation currently implemented by Ruby/AWS.
601
+ #
602
+ OPERATIONS = %w[
603
+ ItemSearch
604
+
605
+
606
+ ]
607
+
608
+ attr_reader :kind
609
+ attr_accessor :params, :response_group
610
+
611
+ def initialize(parameters)
612
+
613
+ op_kind = self.class.to_s.sub( /^.*::/, '' )
614
+
615
+ raise "Bad operation: #{op_kind}" unless OPERATIONS.include?( op_kind )
616
+
617
+ if ResponseGroup::DEFAULT.key?( op_kind )
618
+ response_group =
619
+ ResponseGroup.new( ResponseGroup::DEFAULT[op_kind] )
620
+ else
621
+ response_group = nil
622
+ end
623
+
624
+ if op_kind =~ /^Cart/
625
+ @params = parameters
626
+ else
627
+ @params = Hash.new { |hash, key| hash[key] = [] }
628
+ @response_group = Hash.new { |hash, key| hash[key] = [] }
629
+
630
+ unless op_kind == 'MultipleOperation'
631
+ @params[op_kind] = [ parameters ]
632
+ @response_group[op_kind] = [ response_group ]
633
+ end
634
+ end
635
+
636
+ @kind = op_kind
637
+ end
638
+
639
+
640
+ # Make sure we can still get to the old @response_group= writer method.
641
+ #
642
+ alias :response_group_orig= :response_group=
643
+
644
+ # If the user assigns to @response_group, we need to set this response
645
+ # group for any and all operations that may have been batched.
646
+ #
647
+ def response_group=(rg) # :nodoc:
648
+ @params.each_value do |op_arr|
649
+ op_arr.each do |op|
650
+ op['ResponseGroup'] = rg
651
+ end
652
+ end
653
+ end
654
+
655
+
656
+ # Group together operations of the same class in a batch request.
657
+ # _operations_ should be either an operation of the same class as *self*
658
+ # or an array of such operations.
659
+ #
660
+ # If you need to batch operations of different classes, use a
661
+ # MultipleOperation instead.
662
+ #
663
+ # Example:
664
+ #
665
+ # is = ItemSearch.new( 'Books', { 'Title' => 'ruby programming' } )
666
+ # is2 = ItemSearch.new( 'Music', { 'Artist' => 'stranglers' } )
667
+ # is.response_group = ResponseGroup.new( :Small )
668
+ # is2.response_group = ResponseGroup.new( :Tracks )
669
+ # is.batch( is2 )
670
+ #
671
+ # Please see MultipleOperation.new for implementation details that also
672
+ # apply to batched operations.
673
+ #
674
+ def batch(*operations)
675
+
676
+ operations.flatten.each do |op|
677
+
678
+ unless self.class == op.class
679
+ raise BatchError, "You can't batch operations of different classes. Use class MultipleOperation."
680
+ end
681
+
682
+ # Add the operation's single element array containing the parameter
683
+ # hash to the array.
684
+ #
685
+ @params[op.kind].concat( op.params[op.kind] )
686
+
687
+ # Add the operation's response group array to the array.
688
+ #
689
+ @response_group[op.kind].concat( op.response_group[op.kind] )
690
+ end
691
+
692
+ end
693
+
694
+
695
+ # Return a hash of operation parameters and values, possibly converted to
696
+ # batch syntax, suitable for encoding in a query.
697
+ #
698
+ def query_parameters # :nodoc:
699
+ query = {}
700
+
701
+ @params.each do |op_kind, ops|
702
+
703
+ # If we have only one type of operation and only one operation of
704
+ # that type, return that one in non-batched syntax.
705
+ #
706
+ if @params.size == 1 && @params[op_kind].size == 1
707
+ return { 'Operation' => op_kind,
708
+ 'ResponseGroup' => @response_group[op_kind][0] }.
709
+ merge( @params[op_kind][0] )
710
+ end
711
+
712
+ # Otherwise, use batch syntax.
713
+ #
714
+ ops.each_with_index do |op, op_index|
715
+
716
+ # Make sure we use a response group of some kind.
717
+ #
718
+ shared = '%s.%d.ResponseGroup' % [ op_kind, op_index + 1 ]
719
+ query[shared] = op['ResponseGroup'] ||
720
+ ResponseGroup::DEFAULT[op_kind]
721
+
722
+ # Add all of the parameters to the query hash.
723
+ #
724
+ op.each do |k, v|
725
+ shared = '%s.%d.%s' % [ op_kind, op_index + 1, k ]
726
+ query[shared] = v
727
+ end
728
+ end
729
+ end
730
+
731
+ # Add the operation list.
732
+ #
733
+ { 'Operation' => @params.keys.join( ',' ) }.merge( query )
734
+ end
735
+
736
+ end
737
+
738
+
739
+ # This is the class for the most common type of AWS look-up, an
740
+ # ItemSearch. This allows you to search for items that match a set of
741
+ # broad criteria. It returns items for sale by Amazon merchants and most
742
+ # types of seller.
743
+ #
744
+ class ItemSearch < Operation
745
+
746
+ # Not all search indices work in all locales. It is the user's
747
+ # responsibility to ensure that a given index is valid within a given
748
+ # locale.
749
+ #
750
+ # According to the AWS documentation:
751
+ #
752
+ # - *All* searches through all indices.
753
+ # - *Blended* combines Apparel, Automotive, Books, DVD, Electronics,
754
+ # GourmetFood, Kitchen, Music, PCHardware, PetSupplies, Software,
755
+ # SoftwareVideoGames, SportingGoods, Tools, Toys, VHS and VideoGames.
756
+ # - *Merchants* combines all search indices for a merchant given with
757
+ # MerchantId.
758
+ # - *Music* combines the Classical, DigitalMusic, and MusicTracks
759
+ # indices.
760
+ # - *Video* combines the DVD and VHS search indices.
761
+ #
762
+ SEARCH_INDICES = %w[
763
+ All
764
+ Apparel
765
+ Automotive
766
+ Baby
767
+ Beauty
768
+ Blended
769
+ Books
770
+ Classical
771
+ DigitalMusic
772
+ DVD
773
+ Electronics
774
+ ForeignBooks
775
+ GourmetFood
776
+ Grocery
777
+ HealthPersonalCare
778
+ Hobbies
779
+ HomeGarden
780
+ HomeImprovement
781
+ Industrial
782
+ Jewelry
783
+ KindleStore
784
+ Kitchen
785
+ Lighting
786
+ Magazines
787
+ Merchants
788
+ Miscellaneous
789
+ MP3Downloads
790
+ Music
791
+ MusicalInstruments
792
+ MusicTracks
793
+ OfficeProducts
794
+ OutdoorLiving
795
+ Outlet
796
+ PCHardware
797
+ PetSupplies
798
+ Photo
799
+ Shoes
800
+ SilverMerchants
801
+ Software
802
+ SoftwareVideoGames
803
+ SportingGoods
804
+ Tools
805
+ Toys
806
+ UnboxVideo
807
+ VHS
808
+ Video
809
+ VideoGames
810
+ Watches
811
+ Wireless
812
+ WirelessAccessories
813
+ ]
814
+
815
+
816
+ # Search AWS for items. _search_index_ must be one of _SEARCH_INDICES_
817
+ # and _parameters_ is an optional hash of parameters that further refine
818
+ # the scope of the search.
819
+ #
820
+ # Example:
821
+ #
822
+ # is = ItemSearch.new( 'Books', { 'Title' => 'ruby programming' } )
823
+ #
824
+ # In the above example, we search for books with <b>Ruby Programming</b>
825
+ # in the title.
826
+ #
827
+ def initialize(search_index, parameters)
828
+ unless SEARCH_INDICES.include? search_index.to_s
829
+ raise "Invalid search index: #{search_index}"
830
+ end
831
+
832
+ super( { 'SearchIndex' => search_index }.merge( parameters ) )
833
+ end
834
+
835
+ end
836
+
837
+
838
+
839
+
840
+ # Response groups determine which data pertaining to the item(s) being
841
+ # sought is returned. They strongly influence the amount of data returned,
842
+ # so you should always use the smallest response group(s) containing the
843
+ # data of interest to you, to avoid masses of unnecessary data being
844
+ # returned.
845
+ #
846
+ class ResponseGroup
847
+
848
+ # The default type of response group to use with each type of operation.
849
+ #
850
+ DEFAULT = {
851
+ 'ItemSearch' => :Large
852
+ }
853
+
854
+ # Define a set of one or more response groups to be applied to items
855
+ # retrieved by an AWS operation.
856
+ #
857
+ # Example:
858
+ #
859
+ # rg = ResponseGroup.new( 'Medium', 'Offers', 'Reviews' )
860
+ #
861
+ def initialize(*rg)
862
+ @list = rg.join( ',' )
863
+ end
864
+
865
+
866
+ # We need a form we can interpolate into query strings.
867
+ #
868
+ def to_s # :nodoc:
869
+ @list
870
+ end
871
+
872
+ end
873
+
874
+
875
+ # All dynamically generated exceptions occur within this namespace.
876
+ #
877
+ module Error
878
+
879
+ # The base exception class for errors that result from AWS operations.
880
+ # Classes for these are dynamically generated as subclasses of this one.
881
+ #
882
+ class AWSError < AmazonError; end
883
+
884
+ def Error.exception(xml)
885
+ err_class = xml.elements['Code'].text.sub( /^AWS.*\./, '' )
886
+ err_msg = xml.elements['Message'].text
887
+
888
+ # Dynamically define a new exception class for this class of error,
889
+ # unless it already exists.
890
+ #
891
+ # Note that Ruby 1.9's Module.const_defined? needs a second parameter
892
+ # of *false*, or it will also search AWSError's ancestors.
893
+ #
894
+ cd_params = [ err_class ]
895
+ cd_params << false if RUBY_VERSION >= '1.9.0'
896
+
897
+ unless Amazon::AWS::Error.const_defined?( *cd_params )
898
+ Amazon::AWS::Error.const_set( err_class, Class.new( AWSError ) )
899
+ end
900
+
901
+ # Generate and return a new exception from the relevant class.
902
+ #
903
+ Amazon::AWS::Error.const_get( err_class ).new( err_msg )
904
+ end
905
+
906
+ end
907
+
908
+
909
+ # Create a shorthand module method for each of the AWS operations. These
910
+ # can be used to create less verbose code at the expense of flexibility.
911
+ #
912
+ # For example, we might normally write the following code:
913
+ #
914
+ # is = ItemSearch.new( 'Books', { 'Title' => 'Ruby' } )
915
+ # rg = ResponseGroup.new( 'Large' )
916
+ # req = Request.new
917
+ # response = req.search( is, rg )
918
+ #
919
+ # but we could instead use ItemSearch's associated module method as
920
+ # follows:
921
+ #
922
+ # response = Amazon::AWS.item_search( 'Books', { 'Title' => 'Ruby' } )
923
+ #
924
+ # Note that these equivalent module methods all attempt to use the *Large*
925
+ # response group, which may or may not work. If an
926
+ # Amazon::AWS::Error::InvalidResponseGroup is raised, we will scan the
927
+ # text of the error message returned by AWS to try to glean a valid
928
+ # response group and then retry the operation using that instead.
929
+
930
+
931
+ # Obtain a list of all subclasses of the Operation class.
932
+ #
933
+ classes =
934
+ ObjectSpace.enum_for( :each_object, class << Operation; self; end ).to_a
935
+
936
+ classes.each do |cl|
937
+ # Convert class name to Ruby case, e.g. ItemSearch => item_search.
938
+ #
939
+ class_name = cl.to_s.sub( /^.+::/, '' )
940
+ uncamelised_name = Amazon.uncamelise( class_name )
941
+
942
+ # Define the module method counterpart of each operation.
943
+ #
944
+ module_eval %Q(
945
+ def AWS.#{uncamelised_name}(*params)
946
+ # Instantiate an object of the desired operational class.
947
+ #
948
+ op = #{cl.to_s}.new( *params )
949
+
950
+ # Attempt a search for the given operation using its default
951
+ # response group types.
952
+ #
953
+ results = Search::Request.new.search( op )
954
+ yield results if block_given?
955
+ return results
956
+
957
+ end
958
+ )
959
+ end
960
+
961
+ end
962
+
963
+ end