willnet-amazon-ecs 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
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