willnet-amazon-ecs 0.5.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.
Files changed (5) hide show
  1. data/CHANGELOG +29 -0
  2. data/README +97 -0
  3. data/lib/amazon/ecs.rb +335 -0
  4. data/test/amazon/ecs_test.rb +110 -0
  5. metadata +66 -0
@@ -0,0 +1,29 @@
1
+ 0.5.5 2009-07-10
2
+ ----------------
3
+ * Added function of managing new request authentication
4
+
5
+ 0.5.4 2008-01-02
6
+ ----------------
7
+ * Add Response#error_code
8
+
9
+ 0.5.3 2007-09-12
10
+ ----------------
11
+ * send_request to use default options.
12
+
13
+ 0.5.2 2007-09-08
14
+ ----------------
15
+ * Fixed Amazon::Element.get_unescaped error when result returned for given element path is nil
16
+
17
+ 0.5.1 2007-02-08
18
+ ----------------
19
+ * Fixed Amazon Japan and France URL error
20
+ * Removed opts.delete(:search_index) from item_lookup, SearchIndex param is allowed
21
+ when looking for a book with IdType other than the ASIN.
22
+ * Check for defined? RAILS_DEFAULT_LOGGER to avoid exception for non-rails ruby app
23
+ * Added check for LOGGER constant if RAILS_DEFAULT_LOGGER is not defined
24
+ * Added Ecs.configure(&proc) method for easier configuration of default options
25
+ * Added Element#search_and_convert method
26
+
27
+ 0.5.0 2006-09-12
28
+ ----------------
29
+ Initial Release
data/README ADDED
@@ -0,0 +1,97 @@
1
+ == amazon-ecs
2
+
3
+ Generic Amazon E-commerce REST API using Hpricot with configurable
4
+ default options and method call options. Uses Response and
5
+ Element wrapper classes for easy access to REST XML output. It supports ECS 4.0.
6
+
7
+ It is generic, so you can easily extend <tt>Amazon::Ecs</tt> to support
8
+ other not implemented REST operations; and it is also generic because it just wraps around
9
+ Hpricot element object, instead of providing one-to-one object/attributes to XML elements map.
10
+
11
+ If in the future, there is a change in REST XML output structure,
12
+ no changes will be required on <tt>amazon-ecs</tt> library,
13
+ instead you just need to change the element path.
14
+
15
+ Version: 0.5.5
16
+
17
+ == INSTALLATION
18
+
19
+ $ gem install amazon-ecs
20
+
21
+ == EXAMPLE
22
+
23
+ require 'amazon/ecs'
24
+
25
+ # set the default options; options will be camelized and converted to REST request parameters.
26
+ Amazon::Ecs.options = {:aWS_access_key_id => [your developer token]}
27
+
28
+ # options provided on method call will merge with the default options
29
+ res = Amazon::Ecs.item_search('ruby', {:response_group => 'Medium', :sort => 'salesrank'})
30
+
31
+ # some common response object methods
32
+ res.is_valid_request? # return true if request is valid
33
+ res.has_error? # return true if there is an error
34
+ res.error # return error message if there is any
35
+ res.total_pages # return total pages
36
+ res.total_results # return total results
37
+ res.item_page # return current page no if :item_page option is provided
38
+
39
+ # traverse through each item (Amazon::Element)
40
+ res.items.each do |item|
41
+ # retrieve string value using XML path
42
+ item.get('asin')
43
+ item.get('itemattributes/title')
44
+
45
+ # or return Amazon::Element instance
46
+ atts = item.search_and_convert('itemattributes')
47
+ atts.get('title')
48
+
49
+ # return first author or a string array of authors
50
+ atts.get('author') # 'Author 1'
51
+ atts.get_array('author') # ['Author 1', 'Author 2', ...]
52
+
53
+ # return an hash of children text values with the element names as the keys
54
+ item.get_hash('smallimage') # {:url => ..., :width => ..., :height => ...}
55
+
56
+ # note that '/' returns Hpricot::Elements array object, nil if not found
57
+ reviews = item/'editorialreview'
58
+
59
+ # traverse through Hpricot elements
60
+ reviews.each do |review|
61
+ # Getting hash value out of Hpricot element
62
+ Amazon::Element.get_hash(review) # [:source => ..., :content ==> ...]
63
+
64
+ # Or to get unescaped HTML values
65
+ Amazon::Element.get_unescaped(review, 'source')
66
+ Amazon::Element.get_unescaped(review, 'content')
67
+
68
+ # Or this way
69
+ el = Amazon::Element.new(review)
70
+ el.get_unescaped('source')
71
+ el.get_unescaped('content')
72
+ end
73
+
74
+ # returns Amazon::Element instead of string
75
+ item.search_and_convert('itemattributes').
76
+ end
77
+
78
+ Refer to Amazon ECS documentation for more information on Amazon REST request parameters and XML output:
79
+ http://docs.amazonwebservices.com/AWSEcommerceService/2006-09-13/
80
+
81
+ To get a sample of Amazon REST response XML output, use AWSZone.com scratch pad:
82
+ http://www.awszone.com/scratchpads/aws/ecs.us/index.aws
83
+
84
+ == SOURCE CODES
85
+
86
+ * http://github.com/jugend/amazon-ecs/tree/master
87
+
88
+ == LINKS
89
+
90
+ * http://amazon-ecs.rubyforge.org
91
+ * http://www.pluitsolutions.com/amazon-ecs
92
+
93
+ == LICENSE
94
+
95
+ (The MIT License)
96
+
97
+ Copyright (c) 2006 Herryanto Siatono, Pluit Solutions
@@ -0,0 +1,335 @@
1
+ #--
2
+ # Copyright (c) 2006 Herryanto Siatono, Pluit Solutions
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 'hpricot'
26
+ require 'cgi'
27
+ require 'openssl'
28
+ require 'uri'
29
+ require 'digest/sha2'
30
+ require 'base64'
31
+
32
+ module Amazon
33
+ class RequestError < StandardError; end
34
+
35
+ class Ecs
36
+ SERVICE_URLS = {:us => 'http://webservices.amazon.com/onca/xml',
37
+ :uk => 'http://webservices.amazon.co.uk/onca/xml',
38
+ :ca => 'http://webservices.amazon.ca/onca/xml',
39
+ :de => 'http://webservices.amazon.de/onca/xml',
40
+ :jp => 'http://webservices.amazon.co.jp/onca/xml',
41
+ :fr => 'http://webservices.amazon.fr/onca/xml'
42
+ }
43
+
44
+ @@options = {}
45
+ @@debug = false
46
+
47
+ # Default search options
48
+ def self.options
49
+ @@options
50
+ end
51
+
52
+ # Set default search options
53
+ def self.options=(opts)
54
+ @@options = opts
55
+ end
56
+
57
+ # Get debug flag.
58
+ def self.debug
59
+ @@debug
60
+ end
61
+
62
+ # Set debug flag to true or false.
63
+ def self.debug=(dbg)
64
+ @@debug = dbg
65
+ end
66
+
67
+ def self.configure(&proc)
68
+ raise ArgumentError, "Block is required." unless block_given?
69
+ yield @@options
70
+ end
71
+
72
+ # Search amazon items with search terms. Default search index option is 'Books'.
73
+ # For other search type other than keywords, please specify :type => [search type param name].
74
+ def self.item_search(terms, opts = {})
75
+ opts[:operation] = 'ItemSearch'
76
+ opts[:search_index] = opts[:search_index] || 'Books'
77
+
78
+ type = opts.delete(:type)
79
+ if type
80
+ opts[type.to_sym] = terms
81
+ else
82
+ opts[:keywords] = terms
83
+ end
84
+
85
+ self.send_request(opts)
86
+ end
87
+
88
+ # Search an item by ASIN no.
89
+ def self.item_lookup(item_id, opts = {})
90
+ opts[:operation] = 'ItemLookup'
91
+ opts[:item_id] = item_id
92
+
93
+ self.send_request(opts)
94
+ end
95
+
96
+ # Generic send request to ECS REST service. You have to specify the :operation parameter.
97
+ def self.send_request(opts)
98
+ opts = self.options.merge(opts) if self.options
99
+ request_url = prepare_url(opts)
100
+ log "Request URL: #{request_url}"
101
+
102
+ res = Net::HTTP.get_response(URI::parse(request_url))
103
+ unless res.kind_of? Net::HTTPSuccess
104
+ raise Amazon::RequestError, "HTTP Response: #{res.code} #{res.message}"
105
+ end
106
+ Response.new(res.body)
107
+ end
108
+
109
+ # Response object returned after a REST call to Amazon service.
110
+ class Response
111
+ # XML input is in string format
112
+ def initialize(xml)
113
+ @doc = Hpricot(xml)
114
+ end
115
+
116
+ # Return Hpricot object.
117
+ def doc
118
+ @doc
119
+ end
120
+
121
+ # Return true if request is valid.
122
+ def is_valid_request?
123
+ (@doc/"isvalid").inner_html == "True"
124
+ end
125
+
126
+ # Return true if response has an error.
127
+ def has_error?
128
+ !(error.nil? || error.empty?)
129
+ end
130
+
131
+ # Return error message.
132
+ def error
133
+ Element.get(@doc, "error/message")
134
+ end
135
+
136
+ # Return error code
137
+ def error_code
138
+ Element.get(@doc, "error/code")
139
+ end
140
+
141
+ # Return an array of Amazon::Element item objects.
142
+ def items
143
+ unless @items
144
+ @items = (@doc/"item").collect {|item| Element.new(item)}
145
+ end
146
+ @items
147
+ end
148
+
149
+ # Return the first item (Amazon::Element)
150
+ def first_item
151
+ items.first
152
+ end
153
+
154
+ # Return current page no if :item_page option is when initiating the request.
155
+ def item_page
156
+ unless @item_page
157
+ @item_page = (@doc/"itemsearchrequest/itempage").inner_html.to_i
158
+ end
159
+ @item_page
160
+ end
161
+
162
+ # Return total results.
163
+ def total_results
164
+ unless @total_results
165
+ @total_results = (@doc/"totalresults").inner_html.to_i
166
+ end
167
+ @total_results
168
+ end
169
+
170
+ # Return total pages.
171
+ def total_pages
172
+ unless @total_pages
173
+ @total_pages = (@doc/"totalpages").inner_html.to_i
174
+ end
175
+ @total_pages
176
+ end
177
+ end
178
+
179
+ protected
180
+ def self.log(s)
181
+ return unless self.debug
182
+ if defined? RAILS_DEFAULT_LOGGER
183
+ RAILS_DEFAULT_LOGGER.error(s)
184
+ elsif defined? LOGGER
185
+ LOGGER.error(s)
186
+ else
187
+ puts s
188
+ end
189
+ end
190
+
191
+ private
192
+ def self.prepare_url(opts)
193
+ country = opts.delete(:country)
194
+ country = (country.nil?) ? 'us' : country
195
+ request_url = SERVICE_URLS[country.to_sym]
196
+ raise Amazon::RequestError, "Invalid country '#{country}'" unless request_url
197
+ secret_access_key = opts.delete(:secret_access_key)
198
+ raise Amazon::RequestError, "secret_access_key is nil" unless secret_access_key
199
+ opts[:Timestamp] = DateTime.now.new_offset.strftime('%Y-%m-%dT%XZ')
200
+ opts[:Service] = "AWSECommerceService"
201
+ opts[:Version] = "2009-01-06"
202
+ qs = opts.map do |k, v|
203
+ [camelize(k.to_s), v]
204
+ end
205
+ qs.reject! do |k,v| v.to_s.empty? end
206
+ qs.sort!
207
+ qs.map! do |k,v|
208
+ v = v.join(',') if v.is_a? Array
209
+ [k, CGI.escape(v.to_s)] * "="
210
+ end
211
+ qs = qs * "&"
212
+ uri = URI.parse(request_url + "?" + qs)
213
+ msg = [ 'GET', uri.host, uri.path, uri.query ].join("\n")
214
+ dig = hmac_sha256(secret_access_key, msg)
215
+ sig = CGI.escape(Base64.encode64(dig).chomp)
216
+ "#{request_url}?#{qs}&Signature=#{sig}"
217
+ end
218
+
219
+ IPAD = "\x36"
220
+ OPAD = "\x5c"
221
+ def self.hmac_sha256(key, message)
222
+ ikey = IPAD * 64
223
+ okey = OPAD * 64
224
+ key.size.times do |i|
225
+ ikey[i] = key[i] ^ ikey[i]
226
+ okey[i] = key[i] ^ okey[i]
227
+ end
228
+ value = Digest::SHA256.digest(ikey + message)
229
+ value = Digest::SHA256.digest(okey + value)
230
+ end
231
+
232
+ def self.camelize(s)
233
+ s.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
234
+ end
235
+ end
236
+
237
+ # Internal wrapper class to provide convenient method to access Hpricot element value.
238
+ class Element
239
+ # Pass Hpricot::Elements object
240
+ def initialize(element)
241
+ @element = element
242
+ end
243
+
244
+ # Returns Hpricot::Elments object
245
+ def elem
246
+ @element
247
+ end
248
+
249
+ # Find Hpricot::Elements matching the given path. Example: element/"author".
250
+ def /(path)
251
+ elements = @element/path
252
+ return nil if elements.size == 0
253
+ elements
254
+ end
255
+
256
+ # Find Hpricot::Elements matching the given path, and convert to Amazon::Element.
257
+ # Returns an array Amazon::Elements if more than Hpricot::Elements size is greater than 1.
258
+ def search_and_convert(path)
259
+ elements = self./(path)
260
+ return unless elements
261
+ elements = elements.map{|element| Element.new(element)}
262
+ return elements.first if elements.size == 1
263
+ elements
264
+ end
265
+
266
+ # Get the text value of the given path, leave empty to retrieve current element value.
267
+ def get(path='')
268
+ Element.get(@element, path)
269
+ end
270
+
271
+ # Get the unescaped HTML text of the given path.
272
+ def get_unescaped(path='')
273
+ Element.get_unescaped(@element, path)
274
+ end
275
+
276
+ # Get the array values of the given path.
277
+ def get_array(path='')
278
+ Element.get_array(@element, path)
279
+ end
280
+
281
+ # Get the children element text values in hash format with the element names as the hash keys.
282
+ def get_hash(path='')
283
+ Element.get_hash(@element, path)
284
+ end
285
+
286
+ # Similar to #get, except an element object must be passed-in.
287
+ def self.get(element, path='')
288
+ return unless element
289
+ result = element.at(path)
290
+ result = result.inner_html if result
291
+ result
292
+ end
293
+
294
+ # Similar to #get_unescaped, except an element object must be passed-in.
295
+ def self.get_unescaped(element, path='')
296
+ result = get(element, path)
297
+ CGI::unescapeHTML(result) if result
298
+ end
299
+
300
+ # Similar to #get_array, except an element object must be passed-in.
301
+ def self.get_array(element, path='')
302
+ return unless element
303
+
304
+ result = element/path
305
+ if (result.is_a? Hpricot::Elements) || (result.is_a? Array)
306
+ parsed_result = []
307
+ result.each {|item|
308
+ parsed_result << Element.get(item)
309
+ }
310
+ parsed_result
311
+ else
312
+ [Element.get(result)]
313
+ end
314
+ end
315
+
316
+ # Similar to #get_hash, except an element object must be passed-in.
317
+ def self.get_hash(element, path='')
318
+ return unless element
319
+
320
+ result = element.at(path)
321
+ if result
322
+ hash = {}
323
+ result = result.children
324
+ result.each do |item|
325
+ hash[item.name.to_sym] = item.inner_html
326
+ end
327
+ hash
328
+ end
329
+ end
330
+
331
+ def to_s
332
+ elem.to_s if elem
333
+ end
334
+ end
335
+ end
@@ -0,0 +1,110 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class Amazon::EcsTest < Test::Unit::TestCase
4
+
5
+ AWS_ACCESS_KEY_ID = '1ZD53WRGR730ZCVWBSG2'
6
+ raise "Please specify set your AWS_ACCESS_KEY_ID" if AWS_ACCESS_KEY_ID.empty?
7
+
8
+ Amazon::Ecs.configure do |options|
9
+ options[:response_group] = 'Large'
10
+ options[:aWS_access_key_id] = AWS_ACCESS_KEY_ID
11
+ end
12
+
13
+ ## Test item_search
14
+
15
+ def test_item_search
16
+ resp = Amazon::Ecs.item_search('ruby')
17
+ assert(resp.is_valid_request?)
18
+ assert(resp.total_results >= 3600)
19
+ assert(resp.total_pages >= 360)
20
+ end
21
+
22
+ def test_item_search_with_paging
23
+ resp = Amazon::Ecs.item_search('ruby', :item_page => 2)
24
+ assert resp.is_valid_request?
25
+ assert 2, resp.item_page
26
+ end
27
+
28
+ def test_item_search_with_invalid_request
29
+ resp = Amazon::Ecs.item_search(nil)
30
+ assert !resp.is_valid_request?
31
+ end
32
+
33
+ def test_item_search_with_no_result
34
+ resp = Amazon::Ecs.item_search('afdsafds')
35
+
36
+ assert resp.is_valid_request?
37
+ assert_equal "We did not find any matches for your request.",
38
+ resp.error
39
+ end
40
+
41
+ def test_item_search_uk
42
+ resp = Amazon::Ecs.item_search('ruby', :country => :uk)
43
+ assert resp.is_valid_request?
44
+ end
45
+
46
+ def test_item_search_by_author
47
+ resp = Amazon::Ecs.item_search('dave', :type => :author)
48
+ assert resp.is_valid_request?
49
+ end
50
+
51
+ def test_item_get
52
+ resp = Amazon::Ecs.item_search("0974514055")
53
+ item = resp.first_item
54
+
55
+ # test get
56
+ assert_equal "Programming Ruby: The Pragmatic Programmers' Guide, Second Edition",
57
+ item.get("itemattributes/title")
58
+
59
+ # test get_array
60
+ assert_equal ['Dave Thomas', 'Chad Fowler', 'Andy Hunt'],
61
+ item.get_array("author")
62
+
63
+ # test get_hash
64
+ small_image = item.get_hash("smallimage")
65
+
66
+ assert_equal 3, small_image.keys.size
67
+ assert_match ".jpg", small_image[:url]
68
+ assert_equal "75", small_image[:height]
69
+ assert_equal "59", small_image[:width]
70
+
71
+ # test /
72
+ reviews = item/"editorialreview"
73
+ reviews.each do |review|
74
+ # returns unescaped HTML content, Hpricot escapes all text values
75
+ assert Amazon::Element.get_unescaped(review, 'source')
76
+ assert Amazon::Element.get_unescaped(review, 'content')
77
+ end
78
+ end
79
+
80
+ ## Test item_lookup
81
+ def test_item_lookup
82
+ resp = Amazon::Ecs.item_lookup('0974514055')
83
+ assert_equal "Programming Ruby: The Pragmatic Programmers' Guide, Second Edition",
84
+ resp.first_item.get("itemattributes/title")
85
+ end
86
+
87
+ def test_item_lookup_with_invalid_request
88
+ resp = Amazon::Ecs.item_lookup(nil)
89
+ assert resp.has_error?
90
+ assert resp.error
91
+ end
92
+
93
+ def test_item_lookup_with_no_result
94
+ resp = Amazon::Ecs.item_lookup('abc')
95
+
96
+ assert resp.is_valid_request?
97
+ assert_match(/ABC is not a valid value for ItemId/, resp.error)
98
+ end
99
+
100
+ def test_search_and_convert
101
+ resp = Amazon::Ecs.item_lookup('0974514055')
102
+ title = resp.first_item.get("itemattributes/title")
103
+ authors = resp.first_item.search_and_convert("author")
104
+
105
+ assert_equal "Programming Ruby: The Pragmatic Programmers' Guide, Second Edition", title
106
+ assert authors.is_a?(Array)
107
+ assert 3, authors.size
108
+ assert_equal "Dave Thomas", authors.first.get
109
+ end
110
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: willnet-amazon-ecs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.5
5
+ platform: ruby
6
+ authors:
7
+ - Herryanto Siatono
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-16 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hpricot
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0.4"
24
+ version:
25
+ description:
26
+ email: herryanto@pluitsolutions.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README
33
+ - CHANGELOG
34
+ files:
35
+ - lib/amazon
36
+ - lib/amazon/ecs.rb
37
+ - README
38
+ - CHANGELOG
39
+ has_rdoc: true
40
+ homepage: http://amazon-ecs.rubyforge.net/
41
+ post_install_message:
42
+ rdoc_options: []
43
+
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ requirements: []
59
+
60
+ rubyforge_project:
61
+ rubygems_version: 1.2.0
62
+ signing_key:
63
+ specification_version: 2
64
+ summary: Generic Amazon E-commerce Service (ECS) REST API. Supports ECS 4.0.
65
+ test_files:
66
+ - test/amazon/ecs_test.rb