jy-amazon-ecs 2.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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