vacuum 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,61 +1,43 @@
1
1
  # Vacuum
2
2
 
3
- Vacuum is a [Nokogiri][1]-backed Ruby wrapper to the [Amazon Product
4
- Advertising API] [2].
3
+ [![travis] [2]] [3]
5
4
 
6
- [![travis](https://secure.travis-ci.org/hakanensari/vacuum.png)](http://travis-ci.org/hakanensari/vacuum)
5
+ ![vacuum] [1]
7
6
 
8
- ## Installation
7
+ Vacuum is a minimal Ruby wrapper to the [Amazon Product Advertising API] [4].
9
8
 
10
- Add to your Gemfile.
9
+ # Usage
11
10
 
12
- gem 'vacuum'
11
+ ```ruby
12
+ require 'vacuum'
13
13
 
14
- ## Usage
14
+ # Create a request.
15
+ req = Vacuum.new key: 'key',
16
+ secret: 'secret',
17
+ tag: 'tag',
18
+ locale: 'us'
15
19
 
16
- Set up a request.
20
+ # Build query.
21
+ req.build 'Operation' => 'ItemSearch',
22
+ 'SearchIndex' => 'All',
23
+ 'Keywords' => 'Gilles Deleuze'
17
24
 
18
- require "vacuum"
25
+ # Execute.
26
+ res = request.get
19
27
 
20
- req = Vacuum["us"]
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
- req.configure do |c|
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
- Search for something.
29
-
30
- req << { :operation => 'ItemSearch',
31
- :search_index => 'All',
32
- :keywords => 'George Orwell' }
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 Builder
3
- # Builds a hash from a Nokogiri XML document
2
+ module HashBuilder
3
+ # Builds a hash from a Nokogiri XML document.
4
4
  #
5
- # @note In earlier versions of the library, I was relying on the
6
- # XML Mini Nokogiri module in Active Support. This method
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, attr|
21
- hsh[key] = attr.value
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|
@@ -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
- include CartOperations
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
- # Creates a new request for specified locale
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 [#to_sym] locale two-letter abbreviation for locale
19
- def initialize(locale)
20
- @locale = Locale.new(locale.to_sym)
21
- @params = Hash.new
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 a hash of request parameters into the query
25
- #
26
- # @param [Hash] hsh pairs of parameter keys and values
41
+ # Merges given parameters into the request query.
27
42
  #
28
- # @example
29
- # request << { :key => 'value' }
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
- # Cast value to string.
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
- # Configures the Amazon locale
50
- #
51
- # @yield passes locale to block for configuration
53
+ # Replaces the request query with given parameters.
52
54
  #
53
- # @example
54
- # request.configure do |c|
55
- # c.key = YOUR_KEY
56
- # c.secret = YOUR_SECRET
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] a response
63
+ # @return [Vacuum::Response] A response
67
64
  def get
68
- resp = Net::HTTP.get_response(url)
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] params The request parameters
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
- raise MissingKey unless @locale.key
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
- # @raise [Vacuum::MissingSecret] Amazon secret is missing
94
- # @return [URI::HTTP] the Amazon URL
74
+ # @return [URI::HTTP] The URL for the API request
95
75
  def url
96
- raise MissingSecret unless @locale.secret
97
-
98
- URI::HTTP.build(:host => @locale.host,
76
+ URI::HTTP.build :host => @host,
99
77
  :path => '/onca/xml',
100
- :query => sign(query))
78
+ :query => _query_string
101
79
  end
102
80
 
103
81
  private
104
82
 
105
- def escape(value)
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 query
112
- params.sort.map { |k, v| "#{k}=" + escape(v) }.join('&')
113
- end
99
+ def _query_string
100
+ qs = params.sort.map { |k, v| "#{k}=" + _escape(v) }.join('&')
114
101
 
115
- def sign(unsigned_query)
116
- digest = OpenSSL::Digest::Digest.new('sha256')
117
- url_string = ['GET',
118
- @locale.host,
119
- '/onca/xml',
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
- "#{unsigned_query}&Signature=#{signature}"
108
+ "#{qs}&Signature=#{sig}"
109
+ end
110
+
111
+ def _reset!
112
+ @params = {}
125
113
  end
126
114
 
127
- def timestamp
128
- Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
115
+ def _timestamp
116
+ Time.now.utc.iso8601
129
117
  end
130
118
  end
131
119
  end
@@ -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 the response body
5
+ # @return [String] body The response body
6
6
  attr_accessor :body
7
7
 
8
- # @return [Integer] code the HTTP status code of the response
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 the response body
14
- # @param [#to_i] code the HTTP status code of the response
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
- # Queries for a specified attribute and yields to a given block
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('Error')
22
+ find 'Error'
36
23
  end
37
24
 
38
- # Queries for a specified attribute and returns matching nodes
25
+ # Queries for a given attribute, yielding matching nodes to a block if
26
+ # given one.
39
27
  #
40
- # @param [String] query attribute to be queried
41
- # @return [Array] matching nodes
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 = resp.find('Item')
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 { |e| Builder.from_xml(e) }
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] checks if the response has errors
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
- # Queries for a specifed attribute, yields to a given block
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
- Builder.from_xml(xml)
50
+ HashBuilder.from_xml xml
73
51
  end
74
52
 
75
- # @return [true, false] checks if the HTTP response is OK
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(@body)
60
+ @xml ||= Nokogiri::XML @body
83
61
  end
84
62
  end
85
63
  end
@@ -1,3 +1,3 @@
1
1
  module Vacuum
2
- VERSION = '0.0.1'
2
+ VERSION = '0.1.0'
3
3
  end
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{cart_operations lookup_operations search_operations builder cart
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
- # Vacuum is a Ruby wrapper to the Amazon Product Advertising
11
- # API.
11
+
12
+ # Vacuum is a Ruby wrapper to the Amazon Product Advertising API.
12
13
  module Vacuum
13
- @requests = Hash.new
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
- # @param [#to_sym] locale a locale key
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 Builder do
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
- Builder.from_xml(xml).should be_an_instance_of Hash
20
+ HashBuilder.from_xml(xml).should be_an_instance_of Hash
21
21
  end
22
22
 
23
23
  it 'handles only childs' do
24
- Builder.from_xml(xml)['Title'].should eql 'Anti-Oedipus'
24
+ HashBuilder.from_xml(xml)['Title'].should eql 'Anti-Oedipus'
25
25
  end
26
26
 
27
27
  it 'handles arrays' do
28
- Builder.from_xml(xml)['Author'].should be_a Array
28
+ HashBuilder.from_xml(xml)['Author'].should be_a Array
29
29
  end
30
30
 
31
31
  it 'handles attributes' do
32
- node = Builder.from_xml(xml)['Creator']
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