vacuum 0.0.1 → 0.1.0
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/README.md +31 -49
- data/lib/vacuum/em.rb +23 -0
- data/lib/vacuum/{builder.rb → hash_builder.rb} +6 -12
- data/lib/vacuum/request.rb +79 -91
- data/lib/vacuum/response.rb +24 -46
- data/lib/vacuum/version.rb +1 -1
- data/lib/vacuum.rb +13 -12
- data/spec/vacuum/{builder_spec.rb → hash_builder_spec.rb} +5 -5
- data/spec/vacuum/request_spec.rb +54 -107
- data/spec/vacuum/response_spec.rb +30 -46
- metadata +11 -24
- data/lib/vacuum/cart.rb +0 -90
- data/lib/vacuum/cart_operations.rb +0 -12
- data/lib/vacuum/error.rb +0 -13
- data/lib/vacuum/locale.rb +0 -39
- data/lib/vacuum/lookup_operations.rb +0 -62
- data/lib/vacuum/search_operations.rb +0 -43
- data/spec/vacuum/cart_operations_spec.rb +0 -0
- data/spec/vacuum/cart_spec.rb +0 -0
- data/spec/vacuum/lookup_operations_spec.rb +0 -53
- data/spec/vacuum/search_operations_spec.rb +0 -59
data/README.md
CHANGED
@@ -1,61 +1,43 @@
|
|
1
1
|
# Vacuum
|
2
2
|
|
3
|
-
|
4
|
-
Advertising API] [2].
|
3
|
+
[![travis] [2]] [3]
|
5
4
|
|
6
|
-
|
5
|
+
![vacuum] [1]
|
7
6
|
|
8
|
-
|
7
|
+
Vacuum is a minimal Ruby wrapper to the [Amazon Product Advertising API] [4].
|
9
8
|
|
10
|
-
|
9
|
+
# Usage
|
11
10
|
|
12
|
-
|
11
|
+
```ruby
|
12
|
+
require 'vacuum'
|
13
13
|
|
14
|
-
|
14
|
+
# Create a request.
|
15
|
+
req = Vacuum.new key: 'key',
|
16
|
+
secret: 'secret',
|
17
|
+
tag: 'tag',
|
18
|
+
locale: 'us'
|
15
19
|
|
16
|
-
|
20
|
+
# Build query.
|
21
|
+
req.build 'Operation' => 'ItemSearch',
|
22
|
+
'SearchIndex' => 'All',
|
23
|
+
'Keywords' => 'Gilles Deleuze'
|
17
24
|
|
18
|
-
|
25
|
+
# Execute.
|
26
|
+
res = request.get
|
19
27
|
|
20
|
-
|
28
|
+
# Consume response.
|
29
|
+
if res.valid?
|
30
|
+
# res.to_hash
|
31
|
+
res.find('Item') do |item|
|
32
|
+
p item['ASIN']
|
33
|
+
end
|
34
|
+
end
|
35
|
+
```
|
21
36
|
|
22
|
-
|
23
|
-
c.key = AMAZON_KEY
|
24
|
-
c.secret = AMAZON_SECRET
|
25
|
-
c.tag = AMAZON_ASSOCIATE_TAG
|
26
|
-
end
|
37
|
+
Read further [here] [5].
|
27
38
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
res = request.get
|
34
|
-
|
35
|
-
Or use a shorthand.
|
36
|
-
|
37
|
-
res = req.search('George Orwell')
|
38
|
-
|
39
|
-
Customise your request.
|
40
|
-
|
41
|
-
res = req.search('Books', :response_group => 'ItemAttributes',
|
42
|
-
:power => 'George Orwell'
|
43
|
-
|
44
|
-
For a reference of available methods and syntax, [read here] [3].
|
45
|
-
|
46
|
-
Consume the entire response.
|
47
|
-
|
48
|
-
res.to_hash
|
49
|
-
|
50
|
-
Quickly drop down to a particular node.
|
51
|
-
|
52
|
-
res.each('Item') do |item|
|
53
|
-
puts item['ASIN']
|
54
|
-
end
|
55
|
-
|
56
|
-
[Please see the project page] [4] for further detail.
|
57
|
-
|
58
|
-
[1]: http://nokogiri.org/
|
59
|
-
[2]: https://affiliate-program.amazon.co.uk/gp/advertising/api/detail/main.html
|
60
|
-
[3]: https://github.com/hakanensari/vacuum/blob/master/lib/vacuum/operations.rb
|
61
|
-
[4]: http://code.papercavalier.com/vacuum/
|
39
|
+
[1]: https://github.com/hakanensari/vacuum/blob/master/vacuum.png?raw=true
|
40
|
+
[2]: https://secure.travis-ci.org/hakanensari/vacuum.png
|
41
|
+
[3]: http://travis-ci.org/hakanensari/vacuum
|
42
|
+
[4]: https://affiliate-program.amazon.co.uk/gp/advertising/api/detail/main.html
|
43
|
+
[5]: https://github.com/hakanensari/vacuum/blob/master/examples/
|
data/lib/vacuum/em.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'vacuum'
|
2
|
+
require 'em-http-request'
|
3
|
+
|
4
|
+
module Vacuum
|
5
|
+
class Request
|
6
|
+
# Performs an async request.
|
7
|
+
#
|
8
|
+
# @yield Passes response to given block
|
9
|
+
def aget(&blk)
|
10
|
+
http = EM::HttpRequest.new(url).get
|
11
|
+
http.callback { blk.call _response(http) }
|
12
|
+
http.errback { blk.call _response(http) }
|
13
|
+
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def _response(http)
|
20
|
+
Response.new(http.response, http.response_header.status)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,15 +1,9 @@
|
|
1
1
|
module Vacuum
|
2
|
-
module
|
3
|
-
# Builds a hash from a Nokogiri XML document
|
2
|
+
module HashBuilder
|
3
|
+
# Builds a hash from a Nokogiri XML document.
|
4
4
|
#
|
5
|
-
# @
|
6
|
-
#
|
7
|
-
# essentially accomplishes the same.
|
8
|
-
#
|
9
|
-
# @see Based on https://gist.github.com/335286
|
10
|
-
#
|
11
|
-
# @param [Nokogiri::XML::Document] xml an XML document
|
12
|
-
# @return [Hash] a hashified version of the XML document
|
5
|
+
# @param [Nokogiri::XML::Document] xml An XML document
|
6
|
+
# @return [Hash]
|
13
7
|
def self.from_xml(xml)
|
14
8
|
case xml
|
15
9
|
when Nokogiri::XML::Document
|
@@ -17,8 +11,8 @@ module Vacuum
|
|
17
11
|
when Nokogiri::XML::Element
|
18
12
|
hsh = {}
|
19
13
|
|
20
|
-
xml.attributes.each_pair do |key,
|
21
|
-
hsh[key] =
|
14
|
+
xml.attributes.each_pair do |key, attribute|
|
15
|
+
hsh[key] = attribute.value
|
22
16
|
end
|
23
17
|
|
24
18
|
xml.children.each do |child|
|
data/lib/vacuum/request.rb
CHANGED
@@ -1,131 +1,119 @@
|
|
1
1
|
module Vacuum
|
2
|
-
# A wrapper around the request to the Amazon Product Advertising API
|
2
|
+
# A wrapper around the request to the Amazon Product Advertising API.
|
3
3
|
class Request
|
4
|
-
|
5
|
-
include LookupOperations
|
6
|
-
include SearchOperations
|
7
|
-
|
8
|
-
# The latest Amazon API version
|
9
|
-
#
|
10
|
-
# @see http://aws.amazon.com/archives/Product%20Advertising%20API
|
11
|
-
#
|
12
|
-
# @note If you have a whitelisted access key, override this in your
|
13
|
-
# parameters with an earlier version.
|
4
|
+
# The latest Amazon API version.
|
14
5
|
CURRENT_API_VERSION = '2011-08-01'
|
15
6
|
|
16
|
-
#
|
7
|
+
# A list of Amazon endpoints.
|
8
|
+
HOSTS = {
|
9
|
+
:ca => 'ecs.amazonaws.ca',
|
10
|
+
:cn => 'webservices.amazon.cn',
|
11
|
+
:de => 'ecs.amazonaws.de',
|
12
|
+
:es => 'webservices.amazon.es',
|
13
|
+
:fr => 'ecs.amazonaws.fr',
|
14
|
+
:it => 'webservices.amazon.it',
|
15
|
+
:jp => 'ecs.amazonaws.jp',
|
16
|
+
:uk => 'ecs.amazonaws.co.uk',
|
17
|
+
:us => 'ecs.amazonaws.com'
|
18
|
+
}
|
19
|
+
|
20
|
+
# Creates a new request for given locale and credentials.
|
17
21
|
#
|
18
|
-
# @param [
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
+
# @param [Hash] options
|
23
|
+
# @option opts [#to_sym] :locale An Amazon locale
|
24
|
+
# @option opts [String] :key An Amazon AWS access key ID
|
25
|
+
# @option opts [String] :secret An Amazon AWS access secret key
|
26
|
+
# @option opts [String] :tag An Amazon Associate tag
|
27
|
+
# @raise [MissingKey] An Amazon AWS access key ID was not given
|
28
|
+
# @raise [MissingSecret] An Amazon AWS secret key was not given
|
29
|
+
# @raise [MissingTag] An Amazon Associate tag was not given
|
30
|
+
# @return [self]
|
31
|
+
def initialize(options)
|
32
|
+
_reset!
|
33
|
+
|
34
|
+
locale = (options[:locale] || :us).to_sym
|
35
|
+
@host = HOSTS[locale] or raise BadLocale
|
36
|
+
@key = options[:key] or raise MissingKey
|
37
|
+
@secret = options[:secret] or raise MissingSecret
|
38
|
+
@tag = options[:tag] or raise MissingTag
|
22
39
|
end
|
23
40
|
|
24
|
-
# Merges
|
25
|
-
#
|
26
|
-
# @param [Hash] hsh pairs of parameter keys and values
|
41
|
+
# Merges given parameters into the request query.
|
27
42
|
#
|
28
|
-
# @
|
29
|
-
#
|
30
|
-
|
31
|
-
def <<(hsh)
|
43
|
+
# @param [Hash] hsh Pairs of keys and values
|
44
|
+
# @return [self]
|
45
|
+
def build(hsh)
|
32
46
|
hsh.each do |k, v|
|
33
|
-
|
34
|
-
v = v.is_a?(Array) ? v.join(',') : v.to_s
|
35
|
-
|
36
|
-
# Remove whitespace after commas.
|
37
|
-
v.gsub!(/,\s+/, ',')
|
38
|
-
|
39
|
-
# Camelize key.
|
40
|
-
k = k.to_s.
|
41
|
-
split('_').
|
42
|
-
map { |w| w[0, 1] = w[0, 1].upcase; w }.
|
43
|
-
join
|
44
|
-
|
45
|
-
@params[k] = v
|
47
|
+
@params[k] = v.is_a?(Array) ? v.join(',') : v.to_s
|
46
48
|
end
|
49
|
+
|
50
|
+
self
|
47
51
|
end
|
48
52
|
|
49
|
-
#
|
50
|
-
#
|
51
|
-
# @yield passes locale to block for configuration
|
53
|
+
# Replaces the request query with given parameters.
|
52
54
|
#
|
53
|
-
#
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
# c.tag = YOUR_ASSOCIATE_TAG
|
58
|
-
# end
|
59
|
-
#
|
60
|
-
def configure(&block)
|
61
|
-
block.call(@locale)
|
55
|
+
# see(#build)
|
56
|
+
def build!(hsh = {})
|
57
|
+
_reset!
|
58
|
+
build hsh
|
62
59
|
end
|
63
60
|
|
64
|
-
# Performs a request
|
61
|
+
# Performs a request.
|
65
62
|
#
|
66
|
-
# @return [Vacuum::Response]
|
63
|
+
# @return [Vacuum::Response] A response
|
67
64
|
def get
|
68
|
-
|
69
|
-
|
70
|
-
Response.new(resp.body, resp.code)
|
65
|
+
res = Net::HTTP.get_response(url)
|
66
|
+
Response.new(res.body, res.code)
|
71
67
|
end
|
72
68
|
|
73
|
-
# @return [Hash]
|
74
|
-
# @raise [Vacuum::MissingKey] Amazon key is missing
|
75
|
-
# @raise [Vacuum::MissingTag] Amazon associate tag is
|
76
|
-
# missing
|
69
|
+
# @return [Hash] The parameters that make up the request query.
|
77
70
|
def params
|
78
|
-
|
79
|
-
raise MissingTag unless @locale.tag
|
80
|
-
|
81
|
-
{ 'AWSAccessKeyId' => @locale.key,
|
82
|
-
'AssociateTag' => @locale.tag,
|
83
|
-
'Service' => 'AWSECommerceService',
|
84
|
-
'Timestamp' => timestamp,
|
85
|
-
'Version' => CURRENT_API_VERSION }.merge(@params)
|
86
|
-
end
|
87
|
-
|
88
|
-
# Resets the request parameters
|
89
|
-
def reset!
|
90
|
-
@params = {}
|
71
|
+
_default_params.merge(@params)
|
91
72
|
end
|
92
73
|
|
93
|
-
# @
|
94
|
-
# @return [URI::HTTP] the Amazon URL
|
74
|
+
# @return [URI::HTTP] The URL for the API request
|
95
75
|
def url
|
96
|
-
|
97
|
-
|
98
|
-
URI::HTTP.build(:host => @locale.host,
|
76
|
+
URI::HTTP.build :host => @host,
|
99
77
|
:path => '/onca/xml',
|
100
|
-
:query =>
|
78
|
+
:query => _query_string
|
101
79
|
end
|
102
80
|
|
103
81
|
private
|
104
82
|
|
105
|
-
def
|
83
|
+
def _default_params
|
84
|
+
default = {
|
85
|
+
'AWSAccessKeyId' => @key,
|
86
|
+
'AssociateTag' => @tag,
|
87
|
+
'Service' => 'AWSECommerceService',
|
88
|
+
'Timestamp' => _timestamp,
|
89
|
+
'Version' => CURRENT_API_VERSION
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
def _escape(value)
|
106
94
|
value.gsub(/([^a-zA-Z0-9_.~-]+)/) do
|
107
95
|
'%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
|
108
96
|
end
|
109
97
|
end
|
110
98
|
|
111
|
-
def
|
112
|
-
params.sort.map { |k, v| "#{k}=" +
|
113
|
-
end
|
99
|
+
def _query_string
|
100
|
+
qs = params.sort.map { |k, v| "#{k}=" + _escape(v) }.join('&')
|
114
101
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
unsigned_query].join("\n")
|
121
|
-
hmac = OpenSSL::HMAC.digest(digest, @locale.secret, url_string)
|
122
|
-
signature = escape([hmac].pack('m').chomp)
|
102
|
+
# Sign query string.
|
103
|
+
dig = OpenSSL::Digest::Digest.new 'sha256'
|
104
|
+
req = ['GET', @host, '/onca/xml', qs]
|
105
|
+
hmac = OpenSSL::HMAC.digest dig, @secret, req.join("\n")
|
106
|
+
sig = _escape [hmac].pack('m').chomp
|
123
107
|
|
124
|
-
"#{
|
108
|
+
"#{qs}&Signature=#{sig}"
|
109
|
+
end
|
110
|
+
|
111
|
+
def _reset!
|
112
|
+
@params = {}
|
125
113
|
end
|
126
114
|
|
127
|
-
def
|
128
|
-
Time.now.utc.
|
115
|
+
def _timestamp
|
116
|
+
Time.now.utc.iso8601
|
129
117
|
end
|
130
118
|
end
|
131
119
|
end
|
data/lib/vacuum/response.rb
CHANGED
@@ -1,85 +1,63 @@
|
|
1
1
|
module Vacuum
|
2
|
-
# A wrapper around the API response
|
2
|
+
# A wrapper around the API response.
|
3
3
|
class Response
|
4
4
|
|
5
|
-
# @return [String] body
|
5
|
+
# @return [String] body The response body
|
6
6
|
attr_accessor :body
|
7
7
|
|
8
|
-
# @return [Integer] code
|
8
|
+
# @return [Integer] code The HTTP status code of the response
|
9
9
|
attr_accessor :code
|
10
10
|
|
11
|
-
# Creates a new response
|
11
|
+
# Creates a new response.
|
12
12
|
#
|
13
|
-
# @param [String] body
|
14
|
-
# @param [#to_i] code
|
13
|
+
# @param [String] body The response body
|
14
|
+
# @param [#to_i] code The HTTP status code of the response
|
15
15
|
def initialize(body, code)
|
16
16
|
@body = body
|
17
17
|
@code = code.to_i
|
18
18
|
end
|
19
19
|
|
20
|
-
#
|
21
|
-
# each matching document
|
22
|
-
#
|
23
|
-
# @param [String] query attribute to be queried
|
24
|
-
# @yield passes matching nodes to given block
|
25
|
-
#
|
26
|
-
# @example
|
27
|
-
# resp.each('Item') { |item| p item }
|
28
|
-
#
|
29
|
-
def each(query, &block)
|
30
|
-
find(query).each { |match| block.call(match) }
|
31
|
-
end
|
32
|
-
|
33
|
-
# @return [Array] errors in the response
|
20
|
+
# @return [Array] Errors in the response
|
34
21
|
def errors
|
35
|
-
find
|
22
|
+
find 'Error'
|
36
23
|
end
|
37
24
|
|
38
|
-
# Queries for a
|
25
|
+
# Queries for a given attribute, yielding matching nodes to a block if
|
26
|
+
# given one.
|
39
27
|
#
|
40
|
-
# @param [String]
|
41
|
-
# @
|
28
|
+
# @param [String] Query attribute to be queried
|
29
|
+
# @yield Optionally passes matching nodes to a given block
|
30
|
+
# @return [Array] Matches
|
42
31
|
#
|
43
32
|
# @example
|
44
|
-
# items =
|
33
|
+
# items = res.find('Item')
|
34
|
+
# asins = res.find('Item') { |item| item['ASIN'] }
|
45
35
|
#
|
46
36
|
def find(query)
|
47
|
-
xml.xpath("//xmlns:#{query}").map
|
37
|
+
xml.xpath("//xmlns:#{query}").map do |node|
|
38
|
+
hsh = HashBuilder.from_xml node
|
39
|
+
block_given? ? yield(hsh) : hsh
|
40
|
+
end
|
48
41
|
end
|
49
|
-
alias [] find
|
50
42
|
|
51
|
-
# @return [true, false]
|
43
|
+
# @return [true, false] Whether the response has errors
|
52
44
|
def has_errors?
|
53
45
|
errors.count > 0
|
54
46
|
end
|
55
47
|
|
56
|
-
#
|
57
|
-
# matching nodes, and collects final values.
|
58
|
-
#
|
59
|
-
# @param [String] query attribute to be queried
|
60
|
-
# @yield passes matching nodes to given block
|
61
|
-
# @return [Array] processed results
|
62
|
-
#
|
63
|
-
# @example
|
64
|
-
# asins = resp.map('Item') { |item| item['ASIN'] }
|
65
|
-
#
|
66
|
-
def map(path, &block)
|
67
|
-
find(path).map { |match| block.call(match) }
|
68
|
-
end
|
69
|
-
|
70
|
-
# @return [Hash] a hashified version of the response body
|
48
|
+
# @return [Hash] A hash representation of the entire response.
|
71
49
|
def to_hash
|
72
|
-
|
50
|
+
HashBuilder.from_xml xml
|
73
51
|
end
|
74
52
|
|
75
|
-
# @return [true, false]
|
53
|
+
# @return [true, false] Whether the HTTP response is OK
|
76
54
|
def valid?
|
77
55
|
code == 200
|
78
56
|
end
|
79
57
|
|
80
58
|
# @return [Nokogiri::XML] the XML document
|
81
59
|
def xml
|
82
|
-
@xml ||= Nokogiri::XML
|
60
|
+
@xml ||= Nokogiri::XML @body
|
83
61
|
end
|
84
62
|
end
|
85
63
|
end
|
data/lib/vacuum/version.rb
CHANGED
data/lib/vacuum.rb
CHANGED
@@ -1,23 +1,24 @@
|
|
1
|
+
require 'forwardable'
|
1
2
|
require 'net/http'
|
2
3
|
require 'nokogiri'
|
3
4
|
require 'openssl'
|
5
|
+
require 'time'
|
4
6
|
|
5
|
-
%w{
|
6
|
-
error locale request response}.each do |f|
|
7
|
+
%w{hash_builder request response}.each do |f|
|
7
8
|
require "vacuum/#{f}"
|
8
9
|
end
|
9
10
|
|
10
|
-
|
11
|
-
# API.
|
11
|
+
|
12
|
+
# Vacuum is a Ruby wrapper to the Amazon Product Advertising API.
|
12
13
|
module Vacuum
|
13
|
-
|
14
|
+
class BadLocale < ArgumentError; end
|
15
|
+
class MissingKey < ArgumentError; end
|
16
|
+
class MissingSecret < ArgumentError; end
|
17
|
+
class MissingTag < ArgumentError; end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
extend Forwardable
|
14
21
|
|
15
|
-
|
16
|
-
# @return [Vacuum::Request] a request
|
17
|
-
#
|
18
|
-
# @note The locale key may be any of the following: +ca+, +cn+, +de+,
|
19
|
-
# +es+, +fr+, +it+, +jp+, +uk+, or +us+.
|
20
|
-
def self.[](locale)
|
21
|
-
@requests[locale] ||= Request.new(locale)
|
22
|
+
def_delegators Request, :new
|
22
23
|
end
|
23
24
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
module Vacuum
|
4
|
-
describe
|
4
|
+
describe HashBuilder do
|
5
5
|
let(:xml) do
|
6
6
|
xml = <<-XML.gsub!(/>\s+</, '><').strip!
|
7
7
|
<?xml version=\"1.0\" ?>
|
@@ -17,19 +17,19 @@ module Vacuum
|
|
17
17
|
|
18
18
|
describe '.from_xml' do
|
19
19
|
it 'returns a hash' do
|
20
|
-
|
20
|
+
HashBuilder.from_xml(xml).should be_an_instance_of Hash
|
21
21
|
end
|
22
22
|
|
23
23
|
it 'handles only childs' do
|
24
|
-
|
24
|
+
HashBuilder.from_xml(xml)['Title'].should eql 'Anti-Oedipus'
|
25
25
|
end
|
26
26
|
|
27
27
|
it 'handles arrays' do
|
28
|
-
|
28
|
+
HashBuilder.from_xml(xml)['Author'].should be_a Array
|
29
29
|
end
|
30
30
|
|
31
31
|
it 'handles attributes' do
|
32
|
-
node =
|
32
|
+
node = HashBuilder.from_xml(xml)['Creator']
|
33
33
|
node['Role'].should eql 'Translator'
|
34
34
|
node['__content__'].should eql 'Robert Hurley'
|
35
35
|
end
|