jy-amazon-ecs 2.2.5

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,102 @@
1
+ 2.2.4 2012-01-01
2
+ ----------------
3
+ * Parse with UTF-8 encoding, happy new year :)
4
+
5
+ 2.2.3 2011-12-05
6
+ ----------------
7
+ * Add Amazon Spain API
8
+
9
+ 2.2.2 2011-11-18
10
+ ----------------
11
+ * Add VERSION constant in Amazon::Ecs
12
+ * Update dependent gems version in Gemfile
13
+ * Add Gemfile as part of the gem content
14
+ * Update amazon-ecs.gemspec
15
+
16
+ 2.2.1 2011-10-25
17
+ ----------------
18
+ * Add Gemfile
19
+ * Rake, run test task by default
20
+ * Rename README to README.rdoc
21
+ * Upate README, add search index information
22
+
23
+ 2.2.0 2011-9-06
24
+ ----------------
25
+ * Upgrade to Product Advertising API version to 2011-08-01
26
+ * Fix IT service url and add support for CN URL
27
+ * Add unit test for all service urls
28
+
29
+ 2.1.1 2011-09-05
30
+ ----------------
31
+ * Fix wrong JP service url
32
+ * Add Ecs.browser_node_lookup method
33
+
34
+ 2.1.0 2011-08-18
35
+ ----------------
36
+ * Update service urls
37
+ * Raise error when aws_secret_key is not found
38
+
39
+ 2.0.0 2011-05-09
40
+ ----------------
41
+ * Replace Hpricot parser with Nokogiri
42
+ * Element/Attribute retrieval to match XPath, name not automatically downcased as in Hpricot
43
+
44
+ 1.2.2 2011-05-07
45
+ ----------------
46
+ * Add marshal_dump and marshal_load support
47
+
48
+ 1.2.1 2011-02-23
49
+ ----------------
50
+ * Add support for Amazon Italy
51
+
52
+ 1.2.0 2011-02-07
53
+ ----------------
54
+ * Remove test/ecs_signature_test.rb and test/test_helper.rb
55
+ * Fix multibyte search on Ruby 1.9 returns HTTP Response: 403 Forbidden
56
+
57
+ 1.1.0 2010-11-11
58
+ ----------------
59
+ * Add get_elements, get_element, and attributes instance methods in Amazon::Element
60
+ * Deprecate Amazon::Element#search_and_convert
61
+
62
+ 1.0.0 2010-11-09
63
+ ----------------
64
+ * Set default Amazon API version to 2010-10-01
65
+
66
+ 0.5.7 2009-08-28
67
+ ----------------
68
+ * Added support for to sign request using openssl with fallback on ruby-hmac
69
+
70
+ 0.5.6 2009-07-21
71
+ ----------------
72
+ * Update parameter value encoding to support special characters
73
+
74
+ 0.5.5 2009-07-18
75
+ ----------------
76
+ * Sign request
77
+
78
+ 0.5.4 2008-01-02
79
+ ----------------
80
+ * Add Response#error_code
81
+
82
+ 0.5.3 2007-09-12
83
+ ----------------
84
+ * send_request to use default options.
85
+
86
+ 0.5.2 2007-09-08
87
+ ----------------
88
+ * Fixed Amazon::Element.get_unescaped error when result returned for given element path is nil
89
+
90
+ 0.5.1 2007-02-08
91
+ ----------------
92
+ * Fixed Amazon Japan and France URL error
93
+ * Removed opts.delete(:search_index) from item_lookup, SearchIndex param is allowed
94
+ when looking for a book with IdType other than the ASIN.
95
+ * Check for defined? RAILS_DEFAULT_LOGGER to avoid exception for non-rails ruby app
96
+ * Added check for LOGGER constant if RAILS_DEFAULT_LOGGER is not defined
97
+ * Added Ecs.configure(&proc) method for easier configuration of default options
98
+ * Added Element#search_and_convert method
99
+
100
+ 0.5.0 2006-09-12
101
+ ----------------
102
+ Initial Release
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source :rubygems
2
+
3
+ gem 'nokogiri', '~> 1.4'
4
+ gem 'ruby-hmac', '~> 0.3'
5
+
6
+ group :dev do
7
+ gem 'rake'
8
+ end
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2011 Herryanto Siatono
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+ require 'rake/packagetask'
6
+
7
+ desc "Create the RDOC html files"
8
+ rd = Rake::RDocTask.new("rdoc") { |rdoc|
9
+ rdoc.rdoc_dir = 'doc'
10
+ rdoc.title = "amazon-ecs"
11
+ rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'Readme.rdoc'
12
+ rdoc.rdoc_files.include('Readme.rdoc', 'CHANGELOG')
13
+ rdoc.rdoc_files.include('lib/**/*.rb')
14
+ rdoc.rdoc_files.include('test/**/*.rb')
15
+ }
16
+
17
+ desc "Run the unit tests in test"
18
+ Rake::TestTask.new(:test) do |t|
19
+ t.libs << "test"
20
+ t.pattern = 'test/**/*_test.rb'
21
+ t.verbose = true
22
+ end
23
+
24
+ task :default => :test
data/Readme.rdoc ADDED
@@ -0,0 +1,141 @@
1
+ == amazon-ecs
2
+
3
+ Generic Product Advertising Ruby API using Nokogiri. Uses Response and
4
+ Element wrapper classes for easy access to the REST API XML output.
5
+
6
+ It is generic, so you can easily extend <tt>Amazon::Ecs</tt> to support
7
+ other not implemented REST operations; and it is also generic because it just wraps around
8
+ Nokogiri element object, instead of providing one-to-one object/attributes to XML elements map.
9
+
10
+ The idea is as the API evolves, there is a change in REST XML output structure,
11
+ no updates will be required on <tt>amazon-ecs</tt> gem,
12
+ instead you just need to update the element path.
13
+
14
+ For HPricot dependency implementation, please install 1.2.x version or checkout v1.2 branch.
15
+
16
+ == INSTALLATION
17
+
18
+ $ gem install amazon-ecs
19
+
20
+ == EXAMPLE
21
+
22
+ require 'amazon/ecs'
23
+
24
+ # Set the default options; options will be camelized and converted to REST request parameters.
25
+ # associate_tag and AWS_access_key_id are required options, associate_tag is required option
26
+ # since API version 2011-08-01.
27
+ #
28
+ # To sign your request, include AWS_secret_key.
29
+ Amazon::Ecs.options = {
30
+ :associate_tag => '[your associate tag]',
31
+ :AWS_access_key_id => '[your developer token]',
32
+ :AWS_secret_key => '[your secret access key]'
33
+ }
34
+
35
+ # alternatively,
36
+ Amazon::Ecs.configure do |options|
37
+ options[:associate_tag] = '[your associate tag]'
38
+ options[:AWS_access_key_id] = '[your access key]'
39
+ options[:AWS_secret_key] = '[you secret key]'
40
+ end
41
+
42
+ # options provided on method call will merge with the default options
43
+ res = Amazon::Ecs.item_search('ruby', {:response_group => 'Medium', :sort => 'salesrank'})
44
+
45
+ # search amazon uk
46
+ res = Amazon::Ecs.item_search('ruby', :country => 'uk')
47
+
48
+ # search all items, default search index is Books
49
+ res = Amazon::Ecs.item_search('ruby', :search_index => 'All')
50
+
51
+ # some common response object methods
52
+ res.is_valid_request? # return true if request is valid
53
+ res.has_error? # return true if there is an error
54
+ res.error # return error message if there is any
55
+ res.total_pages # return total pages
56
+ res.total_results # return total results
57
+ res.item_page # return current page no if :item_page option is provided
58
+
59
+ # traverse through each item (Amazon::Element)
60
+ res.items.each do |item|
61
+ # retrieve string value using XML path
62
+ item.get('ASIN')
63
+ item.get('ItemAttributes/Title')
64
+
65
+ # return Amazon::Element instance
66
+ item_attributes = item.get_element('ItemAttributes')
67
+ item_attributes.get('Title')
68
+
69
+ # return first author or a string array of authors
70
+ item_attributes.get('Author') # 'Author 1'
71
+ item_attributes.get_array('Author') # ['Author 1', 'Author 2', ...]
72
+
73
+ # return an hash of children text values with the element names as the keys
74
+ item.get_hash('SmallImage') # {:url => ..., :width => ..., :height => ...}
75
+
76
+ # return the first matching path as Amazon::Element
77
+ item_height = item.get_element('ItemDimensions/Height')
78
+
79
+ # retrieve attributes from Amazon::Element
80
+ item_height.attributes['Units'] # 'hundredths-inches'
81
+
82
+ # return an array of Amazon::Element
83
+ authors = item.get_elements('Author')
84
+
85
+ # return Nokogiri::XML::NodeSet object or nil if not found
86
+ reviews = item/'EditorialReview'
87
+
88
+ # traverse through Nokogiri elements
89
+ reviews.each do |review|
90
+ # Getting hash value out of Nokogiri element
91
+ Amazon::Element.get_hash(review) # [:source => ..., :content ==> ...]
92
+
93
+ # Or to get unescaped HTML values
94
+ Amazon::Element.get_unescaped(review, 'Source')
95
+ Amazon::Element.get_unescaped(review, 'Content')
96
+
97
+ # Or this way
98
+ el = Amazon::Element.new(review)
99
+ el.get_unescaped('Source')
100
+ el.get_unescaped('Content')
101
+ end
102
+ end
103
+
104
+ # Extend Amazon::Ecs, replace 'other_operation' with the appropriate name
105
+ module Amazon
106
+ class Ecs
107
+ def self.other_operation(item_id, opts={})
108
+ opts[:operation] = '[other valid operation supported by Product Advertising API]'
109
+
110
+ # setting default option value
111
+ opts[:item_id] = item_id
112
+
113
+ self.send_request(opts)
114
+ end
115
+ end
116
+ end
117
+
118
+ Amazon::Ecs.other_operation('[item_id]', :param1 => 'abc', :param2 => 'xyz')
119
+
120
+ Refer to Amazon Product Advertising API documentation for more information
121
+ on other valid operations, request parameters and the XML response output:
122
+ https://affiliate-program.amazon.com/gp/advertising/api/detail/main.html
123
+
124
+ To get a sample of Amazon REST XML response output, use AWSZone.com scratch pad:
125
+ http://www.awszone.com/scratchpads/aws/ecs.us/index.aws
126
+
127
+ == SOURCE CODES
128
+
129
+ * http://github.com/jugend/amazon-ecs
130
+
131
+ == CREDITS
132
+
133
+ Thanks to Dan Milne (http://da.nmilne.com/) for the signed request patch.
134
+
135
+ Thanks to Bryan Housel (https://github.com/bhousel/amazon-ecs) for the initial Nokogiri patch
136
+
137
+ == LICENSE
138
+
139
+ (The MIT License)
140
+
141
+ Copyright (c) 2011 Herryanto Siatono, http://www.siatono.com
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require 'amazon/version'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = %q{jy-amazon-ecs}
7
+ gem.version = Amazon::Ecs::VERSION
8
+ gem.platform = Gem::Platform::RUBY
9
+ gem.authors = ["Herryanto Siatono"]
10
+ gem.email = %q{herryanto@gmail.com}
11
+ gem.homepage = %q{https://github.com/jugend/amazon-ecs}
12
+ gem.summary = %q{Generic Amazon Product Advertising Ruby API.}
13
+ gem.description = %q{Generic Amazon Product Advertising Ruby API.}
14
+
15
+ gem.files = `git ls-files`.split("\n")
16
+ gem.test_files = `git ls-files -- test/*`.split("\n")
17
+ gem.require_paths = ["lib"]
18
+
19
+ if gem.respond_to? :specification_version then
20
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
21
+ gem.specification_version = 2
22
+
23
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
24
+ gem.add_runtime_dependency("nokogiri", "~> 1.4")
25
+ gem.add_runtime_dependency("ruby-hmac", "~> 0.3")
26
+ else
27
+ gem.add_dependency("nokogiri", "~> 1.4")
28
+ gem.add_dependency("ruby-hmac", "~> 0.3")
29
+ end
30
+ else
31
+ gem.add_dependency("nokogiri", "~> 1.4")
32
+ gem.add_dependency("ruby-hmac", "~> 0.3")
33
+ end
34
+ end
data/lib/amazon/ecs.rb ADDED
@@ -0,0 +1,403 @@
1
+ #--
2
+ # Copyright (c) 2010 Herryanto Siatono
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require 'net/http'
25
+ require 'nokogiri'
26
+ require 'cgi'
27
+ require 'hmac-sha2'
28
+ require 'base64'
29
+ require 'openssl'
30
+
31
+ module Amazon
32
+ class RequestError < StandardError; end
33
+
34
+ class Ecs
35
+
36
+ SERVICE_URLS = {
37
+ :us => 'http://ecs.amazonaws.com/onca/xml',
38
+ :uk => 'http://ecs.amazonaws.co.uk/onca/xml',
39
+ :ca => 'http://ecs.amazonaws.ca/onca/xml',
40
+ :de => 'http://ecs.amazonaws.de/onca/xml',
41
+ :jp => 'http://ecs.amazonaws.jp/onca/xml',
42
+ :fr => 'http://ecs.amazonaws.fr/onca/xml',
43
+ :it => 'http://webservices.amazon.it/onca/xml',
44
+ :cn => 'http://webservices.amazon.cn/onca/xml',
45
+ :es => 'http://webservices.amazon.es/onca/xml'
46
+ }
47
+
48
+ OPENSSL_DIGEST_SUPPORT = OpenSSL::Digest.constants.include?( 'SHA256' ) ||
49
+ OpenSSL::Digest.constants.include?( :SHA256 )
50
+
51
+ OPENSSL_DIGEST = OpenSSL::Digest::Digest.new( 'sha256' ) if OPENSSL_DIGEST_SUPPORT
52
+
53
+ @@options = {
54
+ :version => "2011-08-01",
55
+ :service => "AWSECommerceService"
56
+ }
57
+
58
+ @@debug = false
59
+
60
+ # Default search options
61
+ def self.options
62
+ @@options
63
+ end
64
+
65
+ # Set default search options
66
+ def self.options=(opts)
67
+ @@options = opts
68
+ end
69
+
70
+ # Get debug flag.
71
+ def self.debug
72
+ @@debug
73
+ end
74
+
75
+ # Set debug flag to true or false.
76
+ def self.debug=(dbg)
77
+ @@debug = dbg
78
+ end
79
+
80
+ def self.configure(&proc)
81
+ raise ArgumentError, "Block is required." unless block_given?
82
+ yield @@options
83
+ end
84
+
85
+ # Search amazon items with search terms. Default search index option is 'Books'.
86
+ # For other search type other than keywords, please specify :type => [search type param name].
87
+ def self.item_search(terms, opts = {})
88
+ opts[:operation] = 'ItemSearch'
89
+ opts[:search_index] = opts[:search_index] || 'Books'
90
+
91
+ type = opts.delete(:type)
92
+ if type
93
+ opts[type.to_sym] = terms
94
+ else
95
+ opts[:keywords] = terms
96
+ end
97
+
98
+ self.send_request(opts)
99
+ end
100
+
101
+ # Search an item by ASIN no.
102
+ def self.item_lookup(item_ids, opts = {})
103
+ item_ids = [item_ids] unless item_ids.is_a?(Array)
104
+ opts[:operation] = 'ItemLookup'
105
+ opts[:item_id] = item_ids.join(',')
106
+
107
+ self.send_request(opts)
108
+ end
109
+
110
+ # Search a browse node by BrowseNodeId
111
+ def self.browse_node_lookup(browse_node_id, opts = {})
112
+ opts[:operation] = 'BrowseNodeLookup'
113
+ opts[:browse_node_id] = browse_node_id
114
+
115
+ self.send_request(opts)
116
+ end
117
+
118
+ # Search a browse node by BrowseNodeId
119
+ def self.browse_node_lookup_multi(browse_node_ids, opts = {})
120
+ opts[:operation] = 'BrowseNodeLookup'
121
+ browse_node_ids.each_with_index do |id, index|
122
+ opts[:"browse_node_id.#{index + 1}"] = id
123
+ end
124
+ raise "Response parsing not implemented!"
125
+ self.send_request(opts)
126
+ end
127
+
128
+
129
+ # Generic send request to ECS REST service. You have to specify the :operation parameter.
130
+ def self.send_request(opts)
131
+ opts = self.options.merge(opts) if self.options
132
+
133
+ # Include other required options
134
+ opts[:timestamp] = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
135
+
136
+ request_url = prepare_url(opts)
137
+ log "Request URL: #{request_url}"
138
+
139
+ res = Net::HTTP.get_response(URI::parse(request_url))
140
+ unless res.kind_of? Net::HTTPSuccess
141
+ raise Amazon::RequestError, "HTTP Response: #{res.code} #{res.message}"
142
+ end
143
+ Response.new(res.body)
144
+ end
145
+
146
+ def self.validate_request(opts)
147
+ raise Amazon::RequestError, "" if opts[:associate_tag]
148
+ end
149
+
150
+ # Response object returned after a REST call to Amazon service.
151
+ class Response
152
+
153
+ # XML input is in string format
154
+ def initialize(xml)
155
+ @doc = Nokogiri::XML(xml, nil, 'UTF-8')
156
+ @doc.remove_namespaces!
157
+ # @doc.xpath("//*").each { |elem| elem.name = elem.name.downcase }
158
+ # @doc.xpath("//@*").each { |att| att.name = att.name.downcase }
159
+ end
160
+
161
+ # Return Nokogiri::XML::Document object.
162
+ def doc
163
+ @doc
164
+ end
165
+
166
+ # Return true if request is valid.
167
+ def is_valid_request?
168
+ Element.get(@doc, "//IsValid") == "True"
169
+ end
170
+
171
+ # Return true if response has an error.
172
+ def has_error?
173
+ !(error.nil? || error.empty?)
174
+ end
175
+
176
+ # Return error message.
177
+ def error
178
+ Element.get(@doc, "//Error/Message")
179
+ end
180
+
181
+ # Return error code
182
+ def error_code
183
+ Element.get(@doc, "//Error/Code")
184
+ end
185
+
186
+ # Return an array of Amazon::Element item objects.
187
+ def items
188
+ @items ||= (@doc/"Item").collect { |item| Element.new(item) }
189
+ end
190
+
191
+ # Return the first item (Amazon::Element)
192
+ def first_item
193
+ items.first
194
+ end
195
+
196
+ def children_browse_nodes
197
+ @children_browse_nodes ||= (@doc/"BrowseNodes/BrowseNode/Children/BrowseNode").collect { |item| Element.new(item) }
198
+ end
199
+
200
+ # Return current page no if :item_page option is when initiating the request.
201
+ def item_page
202
+ @item_page ||= Element.get(@doc, "//ItemPage").to_i
203
+ end
204
+
205
+ # Return total results.
206
+ def total_results
207
+ @total_results ||= Element.get(@doc, "//TotalResults").to_i
208
+ end
209
+
210
+ # Return total pages.
211
+ def total_pages
212
+ @total_pages ||= Element.get(@doc, "//TotalPages").to_i
213
+ end
214
+
215
+ def marshal_dump
216
+ @doc.to_s
217
+ end
218
+
219
+ def marshal_load(xml)
220
+ initialize(xml)
221
+ end
222
+ end
223
+
224
+ protected
225
+ def self.log(s)
226
+ return unless self.debug
227
+ if defined? RAILS_DEFAULT_LOGGER
228
+ RAILS_DEFAULT_LOGGER.error(s)
229
+ elsif defined? LOGGER
230
+ LOGGER.error(s)
231
+ else
232
+ puts s
233
+ end
234
+ end
235
+
236
+ private
237
+ def self.prepare_url(opts)
238
+ country = opts.delete(:country)
239
+ country = (country.nil?) ? 'us' : country
240
+ request_url = SERVICE_URLS[country.to_sym]
241
+ raise Amazon::RequestError, "Invalid country '#{country}'" unless request_url
242
+
243
+ secret_key = opts.delete(:AWS_secret_key)
244
+ request_host = URI.parse(request_url).host
245
+
246
+ qs = ''
247
+
248
+ opts = opts.collect do |a,b|
249
+ [camelize(a.to_s), b.to_s]
250
+ end
251
+
252
+ opts = opts.sort do |c,d|
253
+ c[0].to_s <=> d[0].to_s
254
+ end
255
+
256
+ opts.each do |e|
257
+ log "Adding #{e[0]}=#{e[1]}"
258
+ next unless e[1]
259
+ e[1] = e[1].join(',') if e[1].is_a? Array
260
+ # v = URI.encode(e[1].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
261
+ v = self.url_encode(e[1].to_s)
262
+ qs << "&" unless qs.length == 0
263
+ qs << "#{e[0]}=#{v}"
264
+ end
265
+ #log "QS: @#{qs}"
266
+ signature = ''
267
+ unless secret_key.nil?
268
+ request_to_sign="GET\n#{request_host}\n/onca/xml\n#{qs}"
269
+ signature = "&Signature=#{sign_request(request_to_sign, secret_key)}"
270
+ end
271
+
272
+ "#{request_url}?#{qs}#{signature}"
273
+ end
274
+
275
+ def self.url_encode(string)
276
+ string.gsub( /([^a-zA-Z0-9_.~-]+)/ ) do
277
+ '%' + $1.unpack( 'H2' * $1.bytesize ).join( '%' ).upcase
278
+ end
279
+ end
280
+
281
+ def self.camelize(s)
282
+ s.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
283
+ end
284
+
285
+ def self.sign_request(url, key)
286
+ return nil if key.nil?
287
+
288
+ if (OPENSSL_DIGEST_SUPPORT)
289
+ signature = OpenSSL::HMAC.digest(OPENSSL_DIGEST, key, url)
290
+ signature = [signature].pack('m').chomp
291
+ else
292
+ signature = Base64.encode64( HMAC::SHA256.digest(key, url) ).strip
293
+ end
294
+ signature = URI.escape(signature, Regexp.new("[+=]"))
295
+ return signature
296
+ end
297
+ end
298
+
299
+ # Internal wrapper class to provide convenient method to access Nokogiri element value.
300
+ class Element
301
+ class << self
302
+ # Return the text value of an element.
303
+ def get(element, path='.')
304
+ return unless element
305
+ result = element.at_xpath(path)
306
+ result = result.inner_html if result
307
+ result
308
+ end
309
+
310
+ # Return an unescaped text value of an element.
311
+ def get_unescaped(element, path='.')
312
+ result = self.get(element, path)
313
+ CGI::unescapeHTML(result) if result
314
+ end
315
+
316
+ # Return an array of values based on the given path.
317
+ def get_array(element, path='.')
318
+ return unless element
319
+
320
+ result = element/path
321
+ if (result.is_a? Nokogiri::XML::NodeSet) || (result.is_a? Array)
322
+ result.collect { |item| self.get(item) }
323
+ else
324
+ [self.get(result)]
325
+ end
326
+ end
327
+
328
+ # Return child element text values of the given path.
329
+ def get_hash(element, path='.')
330
+ return unless element
331
+
332
+ result = element.at_xpath(path)
333
+ if result
334
+ hash = {}
335
+ result = result.children
336
+ result.each do |item|
337
+ hash[item.name] = item.inner_html
338
+ end
339
+ hash
340
+ end
341
+ end
342
+ end
343
+
344
+ # Pass Nokogiri::XML::Element object
345
+ def initialize(element)
346
+ @element = element
347
+ end
348
+
349
+ # Returns Nokogiri::XML::Element object
350
+ def elem
351
+ @element
352
+ end
353
+
354
+ # Returns a Nokogiri::XML::NodeSet of elements matching the given path. Example: element/"author".
355
+ def /(path)
356
+ elements = @element/path
357
+ return nil if elements.size == 0
358
+ elements
359
+ end
360
+
361
+ # Return an array of Amazon::Element matching the given path
362
+ def get_elements(path)
363
+ elements = self./(path)
364
+ return unless elements
365
+ elements = elements.map{|element| Element.new(element)}
366
+ end
367
+
368
+ # Similar with search_and_convert but always return first element if more than one elements found
369
+ def get_element(path)
370
+ elements = get_elements(path)
371
+ elements[0] if elements
372
+ end
373
+
374
+ # Get the text value of the given path, leave empty to retrieve current element value.
375
+ def get(path='.')
376
+ Element.get(@element, path)
377
+ end
378
+
379
+ # Get the unescaped HTML text of the given path.
380
+ def get_unescaped(path='.')
381
+ Element.get_unescaped(@element, path)
382
+ end
383
+
384
+ # Get the array values of the given path.
385
+ def get_array(path='.')
386
+ Element.get_array(@element, path)
387
+ end
388
+
389
+ # Get the children element text values in hash format with the element names as the hash keys.
390
+ def get_hash(path='.')
391
+ Element.get_hash(@element, path)
392
+ end
393
+
394
+ def attributes
395
+ return unless self.elem
396
+ self.elem.attributes
397
+ end
398
+
399
+ def to_s
400
+ elem.to_s if elem
401
+ end
402
+ end
403
+ end