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 +102 -0
- data/Gemfile +8 -0
- data/MIT-LICENSE +21 -0
- data/Rakefile +24 -0
- data/Readme.rdoc +141 -0
- data/amazon-ecs.gemspec +34 -0
- data/lib/amazon/ecs.rb +403 -0
- data/lib/amazon/version.rb +5 -0
- data/test/amazon/ecs_test.rb +208 -0
- data/test/fixtures/item_search.xml +1 -0
- metadata +106 -0
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
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
|
data/amazon-ecs.gemspec
ADDED
@@ -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
|