bare-ruby-aws 0.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.
@@ -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