vacuum 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2010 Hakan Ensari
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # Vacuum
2
+
3
+ Vacuum is a [Nokogiri][1]-backed Ruby wrapper to the [Amazon Product
4
+ Advertising API] [2].
5
+
6
+ [![travis](https://secure.travis-ci.org/hakanensari/vacuum.png)](http://travis-ci.org/hakanensari/vacuum)
7
+
8
+ ## Installation
9
+
10
+ Add to your Gemfile.
11
+
12
+ gem 'vacuum'
13
+
14
+ ## Usage
15
+
16
+ Set up a request.
17
+
18
+ require "vacuum"
19
+
20
+ req = Vacuum["us"]
21
+
22
+ req.configure do |c|
23
+ c.key = AMAZON_KEY
24
+ c.secret = AMAZON_SECRET
25
+ c.tag = AMAZON_ASSOCIATE_TAG
26
+ end
27
+
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/
data/lib/vacuum.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'net/http'
2
+ require 'nokogiri'
3
+ require 'openssl'
4
+
5
+ %w{cart_operations lookup_operations search_operations builder cart
6
+ error locale request response}.each do |f|
7
+ require "vacuum/#{f}"
8
+ end
9
+
10
+ # Vacuum is a Ruby wrapper to the Amazon Product Advertising
11
+ # API.
12
+ module Vacuum
13
+ @requests = Hash.new
14
+
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
+ end
23
+ end
@@ -0,0 +1,51 @@
1
+ module Vacuum
2
+ module Builder
3
+ # Builds a hash from a Nokogiri XML document
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
13
+ def self.from_xml(xml)
14
+ case xml
15
+ when Nokogiri::XML::Document
16
+ from_xml(xml.root)
17
+ when Nokogiri::XML::Element
18
+ hsh = {}
19
+
20
+ xml.attributes.each_pair do |key, attr|
21
+ hsh[key] = attr.value
22
+ end
23
+
24
+ xml.children.each do |child|
25
+ result = from_xml(child)
26
+
27
+ if child.name == 'text'
28
+ if hsh.empty?
29
+ return result
30
+ else
31
+ hsh['__content__'] = result
32
+ end
33
+ elsif hsh[child.name]
34
+ case hsh[child.name]
35
+ when Array
36
+ hsh[child.name] << result
37
+ else
38
+ hsh[child.name] = [hsh[child.name]] << result
39
+ end
40
+ else
41
+ hsh[child.name] = result
42
+ end
43
+ end
44
+
45
+ hsh
46
+ else
47
+ xml.content.to_s
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,90 @@
1
+ module Vacuum
2
+ class Cart
3
+ # @return [String] cart_id
4
+ attr :id
5
+
6
+ # @return [String] hmac
7
+ attr :hmac
8
+
9
+ attr :items
10
+
11
+ # @return [Vacuum::Response] last_response last response
12
+ # returned by the Amazon API
13
+ attr :last_response
14
+
15
+ # @return [String] purchase_url
16
+ attr :purchase_url
17
+
18
+ attr :sub_total
19
+
20
+ # Creates a new cart
21
+ #
22
+ # @param [Vacuum::Request] req an API request
23
+ # @param [Hash] params a hash of parameters
24
+ def initialize(req, params)
25
+ @req = req
26
+ get 'Create', params
27
+ end
28
+
29
+ # Clears the cart
30
+ #
31
+ # @param [Hash] params a hash of parameters
32
+ def clear(params = {})
33
+ get 'Clear', params
34
+ end
35
+
36
+ private
37
+
38
+ def get(operation, params)
39
+ @req.reset!
40
+
41
+ if id
42
+ @req << { 'CartId' => id,
43
+ 'HMAC' => hmac }
44
+ end
45
+
46
+ @req << { 'Operation' => "Cart#{operation}" }.merge(params)
47
+
48
+ @last_response = @req.get
49
+ @items = @last_response.find('CartItems')
50
+ @id = @last_response.find('CartId').first
51
+ @hmac = @last_response.find('HMAC').first
52
+ @purchase_url = @last_response.find('PurchaseURL').first
53
+ @sub_total = @last_response.find('SubTotal').first
54
+ end
55
+
56
+ # Add items to cart
57
+ #
58
+ # @param [String] cart_id
59
+ # @param [String] hmac
60
+ # @param [Hash] params
61
+ # @return [Vacuum::Cart] a response
62
+ # def add_to_cart(cart_id, hmac, params)
63
+ # cartify 'Add', { 'CartId' => cart_id,
64
+ # 'HMAC' => hmac }.merge(params)
65
+ # end
66
+
67
+
68
+ # Gets an existing cart
69
+ #
70
+ # @param [String] cart_id
71
+ # @param [String] hmac
72
+ # @param [Hash] params
73
+ # @return [Vacuum::Cart] a response
74
+ # def get_cart(cart_id, hmac, params)
75
+ # cartify 'Get', { 'CartId' => cart_id,
76
+ # 'HMAC' => hmac }.merge(params)
77
+ # end
78
+
79
+ # Modifies an existing cart
80
+ #
81
+ # @param [String] cart_id
82
+ # @param [String] hmac
83
+ # @param [Hash] params
84
+ # @return [Vacuum::Cart] a response
85
+ # def modify_cart(cart_id, hmac, params)
86
+ # cartify 'Modify', { 'CartId' => cart_id,
87
+ # 'HMAC' => hmac }.merge(params)
88
+ # end
89
+ end
90
+ end
@@ -0,0 +1,12 @@
1
+ module Vacuum
2
+ # Cart operations
3
+ module CartOperations
4
+ # Creates a cart
5
+ #
6
+ # @param [Hash] params
7
+ # @return [Vacuum::Cart] a cart
8
+ def create_cart(params)
9
+ Cart.new(self, params)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module Vacuum
2
+ # Raised when a bad locale is specified
3
+ class BadLocale < ArgumentError; end
4
+
5
+ # Raised when the Amazon key is not specified
6
+ class MissingKey < ArgumentError; end
7
+
8
+ # Raised when the Amazon secret is not specified
9
+ class MissingSecret < ArgumentError; end
10
+
11
+ # Raised when the Amazon associate tag is not specified
12
+ class MissingTag < ArgumentError; end
13
+ end
@@ -0,0 +1,39 @@
1
+ module Vacuum
2
+ # An Amazon locale
3
+ class Locale
4
+ # Amazon hosts
5
+ HOSTS = { :ca => 'ecs.amazonaws.ca',
6
+ :cn => 'webservices.amazon.cn',
7
+ :de => 'ecs.amazonaws.de',
8
+ :es => 'webservices.amazon.es',
9
+ :fr => 'ecs.amazonaws.fr',
10
+ :it => 'webservices.amazon.it',
11
+ :jp => 'ecs.amazonaws.jp',
12
+ :us => 'ecs.amazonaws.com',
13
+ :uk => 'ecs.amazonaws.co.uk' }
14
+
15
+ # Country codes for Amazon locales
16
+ LOCALES = HOSTS.keys
17
+
18
+ # @return [String] the Amazon Web Services access key
19
+ attr_accessor :key
20
+
21
+ # @return [String] the Amazon Web Services secret
22
+ attr_accessor :secret
23
+
24
+ # @return [String] the Amazon associate tag
25
+ attr_accessor :tag
26
+
27
+ # @param [Symbol] locale the locale key
28
+ # @raise [Vacuum::BadLocale] locale is bad
29
+ def initialize(locale)
30
+ raise BadLocale unless LOCALES.include?(locale)
31
+ @locale = locale
32
+ end
33
+
34
+ # @return [String] the Amazon host
35
+ def host
36
+ HOSTS[@locale]
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,62 @@
1
+ module Vacuum
2
+ # Lookup operations
3
+ module LookupOperations
4
+ # Given up to ten item ids, returns some or all of the item
5
+ # attributes, depending on the response group specified in the
6
+ # request.
7
+ #
8
+ # @param [Array] item_ids splat of item IDs and an optional hash of
9
+ # parameters
10
+ # @return [Vacuum::Response] a response
11
+ #
12
+ # Id Type defaults to ASIN.
13
+ #
14
+ # @example The following returns some basic information for the
15
+ # ASIN 0679753354.
16
+ #
17
+ # req.find('0679753354')
18
+ #
19
+ # @example The following request returns cover art for the same
20
+ # ASIN.
21
+ #
22
+ # req.find('0679753354', :response_group => 'Images')
23
+ #
24
+ def find(*item_ids)
25
+ reset!
26
+ params = item_ids.last.is_a?(Hash) ? item_ids.pop : {}
27
+ self.<<({ 'Operation' => 'ItemLookup',
28
+ 'ItemId' => item_ids }.merge(params))
29
+
30
+ get
31
+ end
32
+
33
+ # Given a browse node ID, returns the specified browse node’s name,
34
+ # children, and ancestors.
35
+ #
36
+ # @param [String] browse_node_id browse node ID
37
+ # @params [Hash] params hash of parameters
38
+ # @return [Vacuum::Response] a response
39
+ def find_browse_node(browse_node_id, params = {})
40
+ reset!
41
+ self.<<({ 'Operation' => 'BrowseNodeLookup',
42
+ 'BrowseNodeId' => browse_node_id }.merge(params))
43
+
44
+ get
45
+ end
46
+
47
+ # Given up to ten item ids, returns up to ten products per page
48
+ # that are similar to those items
49
+ #
50
+ # @param [Array] item_ids splat of item IDs and an optional hash of
51
+ # parameters
52
+ # @return [Vacuum::Response] a response
53
+ def find_similar(*item_ids)
54
+ reset!
55
+ params = item_ids.last.is_a?(Hash) ? item_ids.pop : {}
56
+ self.<<({ 'Operation' => 'SimilarityLookup',
57
+ 'ItemId' => item_ids }.merge(params))
58
+
59
+ get
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,131 @@
1
+ module Vacuum
2
+ # A wrapper around the request to the Amazon Product Advertising API
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.
14
+ CURRENT_API_VERSION = '2011-08-01'
15
+
16
+ # Creates a new request for specified locale
17
+ #
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
+ end
23
+
24
+ # Merges a hash of request parameters into the query
25
+ #
26
+ # @param [Hash] hsh pairs of parameter keys and values
27
+ #
28
+ # @example
29
+ # request << { :key => 'value' }
30
+ #
31
+ def <<(hsh)
32
+ 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
46
+ end
47
+ end
48
+
49
+ # Configures the Amazon locale
50
+ #
51
+ # @yield passes locale to block for configuration
52
+ #
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)
62
+ end
63
+
64
+ # Performs a request
65
+ #
66
+ # @return [Vacuum::Response] a response
67
+ def get
68
+ resp = Net::HTTP.get_response(url)
69
+
70
+ Response.new(resp.body, resp.code)
71
+ end
72
+
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
77
+ 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 = {}
91
+ end
92
+
93
+ # @raise [Vacuum::MissingSecret] Amazon secret is missing
94
+ # @return [URI::HTTP] the Amazon URL
95
+ def url
96
+ raise MissingSecret unless @locale.secret
97
+
98
+ URI::HTTP.build(:host => @locale.host,
99
+ :path => '/onca/xml',
100
+ :query => sign(query))
101
+ end
102
+
103
+ private
104
+
105
+ def escape(value)
106
+ value.gsub(/([^a-zA-Z0-9_.~-]+)/) do
107
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
108
+ end
109
+ end
110
+
111
+ def query
112
+ params.sort.map { |k, v| "#{k}=" + escape(v) }.join('&')
113
+ end
114
+
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)
123
+
124
+ "#{unsigned_query}&Signature=#{signature}"
125
+ end
126
+
127
+ def timestamp
128
+ Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,85 @@
1
+ module Vacuum
2
+ # A wrapper around the API response
3
+ class Response
4
+
5
+ # @return [String] body the response body
6
+ attr_accessor :body
7
+
8
+ # @return [Integer] code the HTTP status code of the response
9
+ attr_accessor :code
10
+
11
+ # Creates a new response
12
+ #
13
+ # @param [String] body the response body
14
+ # @param [#to_i] code the HTTP status code of the response
15
+ def initialize(body, code)
16
+ @body = body
17
+ @code = code.to_i
18
+ end
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
34
+ def errors
35
+ find('Error')
36
+ end
37
+
38
+ # Queries for a specified attribute and returns matching nodes
39
+ #
40
+ # @param [String] query attribute to be queried
41
+ # @return [Array] matching nodes
42
+ #
43
+ # @example
44
+ # items = resp.find('Item')
45
+ #
46
+ def find(query)
47
+ xml.xpath("//xmlns:#{query}").map { |e| Builder.from_xml(e) }
48
+ end
49
+ alias [] find
50
+
51
+ # @return [true, false] checks if the response has errors
52
+ def has_errors?
53
+ errors.count > 0
54
+ end
55
+
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
71
+ def to_hash
72
+ Builder.from_xml(xml)
73
+ end
74
+
75
+ # @return [true, false] checks if the HTTP response is OK
76
+ def valid?
77
+ code == 200
78
+ end
79
+
80
+ # @return [Nokogiri::XML] the XML document
81
+ def xml
82
+ @xml ||= Nokogiri::XML(@body)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,43 @@
1
+ module Vacuum
2
+ # Search operations
3
+ module SearchOperations
4
+ # Returns up to ten items that satisfy the search criteria,
5
+ # including one or more search indices.
6
+ #
7
+ # @param [String, nil] search_index search index or keyword query
8
+ # @param [String, Hash] params keyword query or hash of parameters
9
+ # @return [Vacuum::Response] a reponse
10
+ #
11
+ # @example The following searches the entire Amazon catalog for the
12
+ # keyword 'book'.
13
+ #
14
+ # req.search('book')
15
+ #
16
+ # @example The following searches the books search index for the
17
+ # keyword 'lacan'.
18
+ #
19
+ # req.search('Books', 'lacan')
20
+ #
21
+ # @example The following runs a power search on the books search
22
+ # index for non-fiction titles authored by Lacan and sorts results
23
+ # by Amazon's relevance ranking.
24
+ #
25
+ # req.search('Books', :power => 'author:lacan and not fiction',
26
+ # :sort => 'relevancerank')
27
+ #
28
+ def search(search_index, params = nil)
29
+ reset!
30
+ if params.nil?
31
+ params = { 'Keywords' => search_index }
32
+ search_index = 'All'
33
+ end
34
+ if params.is_a? String
35
+ params = { 'Keywords' => params }
36
+ end
37
+ self.<<({ 'Operation' => 'ItemSearch',
38
+ 'SearchIndex' => search_index }.merge(params))
39
+
40
+ get
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,3 @@
1
+ module Vacuum
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1 @@
1
+ <?xml version="1.0" ?><ItemLookupResponse xmlns="http://webservices.amazon.com/AWSECommerceService/2011-08-01"><OperationRequest><RequestId>86b89a15-b717-4d95-99aa-fe531b4ca762</RequestId><Arguments><Argument Name="Operation" Value="ItemLookup"></Argument><Argument Name="Service" Value="AWSECommerceService"></Argument><Argument Name="AssociateTag" Value="theorydot08-20"></Argument><Argument Name="Version" Value="2011-08-01"></Argument><Argument Name="Signature" Value="vOT9O1NW8PYLvrUX6KI3jrZ4Fg7LdtEYTrlsWzhbm1k="></Argument><Argument Name="ItemId" Value="0816614024,0143105825"></Argument><Argument Name="IdType" Value="ASIN"></Argument><Argument Name="AWSAccessKeyId" Value="0ZVSQ33MDFPQS8H2PM02"></Argument><Argument Name="Timestamp" Value="2011-07-29T17:52:34Z"></Argument></Arguments><RequestProcessingTime>0.0111990000000000</RequestProcessingTime></OperationRequest><Items><Request><IsValid>True</IsValid><ItemLookupRequest><IdType>ASIN</IdType><ItemId>0816614024</ItemId><ItemId>0143105825</ItemId><ResponseGroup>Small</ResponseGroup><VariationPage>All</VariationPage></ItemLookupRequest></Request><Item><ASIN>0816614024</ASIN><DetailPageURL>http://www.amazon.com/Thousand-Plateaus-Schizophrenia-Gilles-Deleuze/dp/0816614024%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D165953%26creativeASIN%3D0816614024</DetailPageURL><ItemLinks><ItemLink><Description>Technical Details</Description><URL>http://www.amazon.com/Thousand-Plateaus-Schizophrenia-Gilles-Deleuze/dp/tech-data/0816614024%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0816614024</URL></ItemLink><ItemLink><Description>Add To Baby Registry</Description><URL>http://www.amazon.com/gp/registry/baby/add-item.html%3Fasin.0%3D0816614024%26SubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0816614024</URL></ItemLink><ItemLink><Description>Add To Wedding Registry</Description><URL>http://www.amazon.com/gp/registry/wedding/add-item.html%3Fasin.0%3D0816614024%26SubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0816614024</URL></ItemLink><ItemLink><Description>Add To Wishlist</Description><URL>http://www.amazon.com/gp/registry/wishlist/add-item.html%3Fasin.0%3D0816614024%26SubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0816614024</URL></ItemLink><ItemLink><Description>Tell A Friend</Description><URL>http://www.amazon.com/gp/pdp/taf/0816614024%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0816614024</URL></ItemLink><ItemLink><Description>All Customer Reviews</Description><URL>http://www.amazon.com/review/product/0816614024%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0816614024</URL></ItemLink><ItemLink><Description>All Offers</Description><URL>http://www.amazon.com/gp/offer-listing/0816614024%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0816614024</URL></ItemLink></ItemLinks><ItemAttributes><Author>Gilles Deleuze</Author><Creator Role="Contributor">Felix Guattari</Creator><Manufacturer>Univ Of Minnesota Press</Manufacturer><ProductGroup>Book</ProductGroup><Title>Thousand Plateaus: Capitalism and Schizophrenia</Title></ItemAttributes></Item><Item><ASIN>0143105825</ASIN><DetailPageURL>http://www.amazon.com/Anti-Oedipus-Capitalism-Schizophrenia-Penguin-Classics/dp/0143105825%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D165953%26creativeASIN%3D0143105825</DetailPageURL><ItemLinks><ItemLink><Description>Technical Details</Description><URL>http://www.amazon.com/Anti-Oedipus-Capitalism-Schizophrenia-Penguin-Classics/dp/tech-data/0143105825%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0143105825</URL></ItemLink><ItemLink><Description>Add To Baby Registry</Description><URL>http://www.amazon.com/gp/registry/baby/add-item.html%3Fasin.0%3D0143105825%26SubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0143105825</URL></ItemLink><ItemLink><Description>Add To Wedding Registry</Description><URL>http://www.amazon.com/gp/registry/wedding/add-item.html%3Fasin.0%3D0143105825%26SubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0143105825</URL></ItemLink><ItemLink><Description>Add To Wishlist</Description><URL>http://www.amazon.com/gp/registry/wishlist/add-item.html%3Fasin.0%3D0143105825%26SubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0143105825</URL></ItemLink><ItemLink><Description>Tell A Friend</Description><URL>http://www.amazon.com/gp/pdp/taf/0143105825%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0143105825</URL></ItemLink><ItemLink><Description>All Customer Reviews</Description><URL>http://www.amazon.com/review/product/0143105825%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0143105825</URL></ItemLink><ItemLink><Description>All Offers</Description><URL>http://www.amazon.com/gp/offer-listing/0143105825%3FSubscriptionId%3D0ZVSQ33MDFPQS8H2PM02%26tag%3Dtheorydot08-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0143105825</URL></ItemLink></ItemLinks><ItemAttributes><Author>Gilles Deleuze</Author><Author>Felix Guattari</Author><Creator Role="Translator">Robert Hurley</Creator><Creator Role="Translator">Mark Seem</Creator><Creator Role="Introduction">Mark Seem</Creator><Creator Role="Translator">Helen Lane</Creator><Creator Role="Preface">Michel Foucault</Creator><Manufacturer>Penguin Classics</Manufacturer><ProductGroup>Book</ProductGroup><Title>Anti-Oedipus: Capitalism and Schizophrenia (Penguin Classics)</Title></ItemAttributes></Item></Items></ItemLookupResponse>
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'rspec'
4
+
5
+ begin
6
+ require 'pry'
7
+ rescue LoadError
8
+ end
9
+
10
+ require File.expand_path('../../lib/vacuum', __FILE__)
11
+
12
+ RSpec.configure do |c|
13
+ c.treat_symbols_as_metadata_keys_with_true_values = true
14
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ module Vacuum
4
+ describe Builder do
5
+ let(:xml) do
6
+ xml = <<-XML.gsub!(/>\s+</, '><').strip!
7
+ <?xml version=\"1.0\" ?>
8
+ <ItemAttributes>
9
+ <Title>Anti-Oedipus</Title>
10
+ <Author>Gilles Deleuze</Author>
11
+ <Author>Felix Guattari</Author>
12
+ <Creator Role="Translator">Robert Hurley</Creator>
13
+ </ItemAttributes>
14
+ XML
15
+ Nokogiri::XML(xml)
16
+ end
17
+
18
+ describe '.from_xml' do
19
+ it 'returns a hash' do
20
+ Builder.from_xml(xml).should be_an_instance_of Hash
21
+ end
22
+
23
+ it 'handles only childs' do
24
+ Builder.from_xml(xml)['Title'].should eql 'Anti-Oedipus'
25
+ end
26
+
27
+ it 'handles arrays' do
28
+ Builder.from_xml(xml)['Author'].should be_a Array
29
+ end
30
+
31
+ it 'handles attributes' do
32
+ node = Builder.from_xml(xml)['Creator']
33
+ node['Role'].should eql 'Translator'
34
+ node['__content__'].should eql 'Robert Hurley'
35
+ end
36
+ end
37
+ end
38
+ end
File without changes
File without changes
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+
3
+ module Vacuum
4
+ describe LookupOperations do
5
+ let(:req) { Request.new('us') }
6
+
7
+ before do
8
+ req.configure do |c|
9
+ c.key = 'foo'
10
+ c.tag = 'bar'
11
+ end
12
+ req.stub!(:get)
13
+ end
14
+
15
+ describe "#find" do
16
+ before do
17
+ req.find('1', '2', :foo => 'bar')
18
+ end
19
+
20
+ it 'merges item ids' do
21
+ req.params['ItemId'].should eql '1,2'
22
+ end
23
+
24
+ it 'merges additional parameters' do
25
+ req.params['Foo'].should eql 'bar'
26
+ end
27
+ end
28
+
29
+ describe "#find_browse_node" do
30
+ before do
31
+ req.find_browse_node('123', :foo => 'bar')
32
+ end
33
+
34
+ it 'merges item ids' do
35
+ req.params['BrowseNodeId'].should eql '123'
36
+ end
37
+
38
+ it 'merges additional parameters' do
39
+ req.params['Foo'].should eql 'bar'
40
+ end
41
+ end
42
+
43
+ describe "#find_similar" do
44
+ before do
45
+ req.find_similar('1', '2')
46
+ end
47
+
48
+ it 'merges item ids' do
49
+ req.params['ItemId'].should eql '1,2'
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,183 @@
1
+ require 'spec_helper'
2
+
3
+ module Vacuum
4
+ describe Request do
5
+ let(:req) { Request.new('us') }
6
+
7
+ describe '#<<' do
8
+ before do
9
+ req.configure do |c|
10
+ c.key = 'foo'
11
+ c.tag = 'bar'
12
+ end
13
+
14
+ req.reset!
15
+ end
16
+
17
+ it 'merges parameters into the query' do
18
+ req << { 'Key' => 'value' }
19
+
20
+ req.params['Key'].should eql 'value'
21
+ end
22
+
23
+ it 'camelizes keys' do
24
+ req << { :some_key => 'value' }
25
+
26
+ req.params.should have_key 'SomeKey'
27
+ end
28
+
29
+ it 'leaves camelized keys as is' do
30
+ req << { 'SomeKey' => 'value' }
31
+
32
+ req.params.should have_key 'SomeKey'
33
+ end
34
+
35
+ it 'casts numeric values to string' do
36
+ req << { 'Key' => 1 }
37
+
38
+ req.params['Key'].should eql '1'
39
+ end
40
+
41
+ it 'converts array values to string' do
42
+ req << { 'Key' => ['foo', 'bar'] }
43
+
44
+ req.params['Key'].should eql 'foo,bar'
45
+ end
46
+
47
+ it 'removes whitespace after commas in values' do
48
+ req << { 'Key' => 'foo, bar' }
49
+
50
+ req.params['Key'].should eql 'foo,bar'
51
+ end
52
+ end
53
+
54
+ describe '#configure' do
55
+ it 'yields the locale' do
56
+ req.configure(&:class).should eql Locale
57
+ end
58
+ end
59
+
60
+ describe '#get' do
61
+ before do
62
+ req.configure do |c|
63
+ c.key = 'foo'
64
+ c.secret = 'bar'
65
+ c.tag = 'baz'
66
+ end
67
+ end
68
+
69
+ it 'returns a response' do
70
+ req.get.should be_a Response
71
+ end
72
+
73
+ it 'raises an error if secret is missing' do
74
+ req.configure { |c| c.secret = nil }
75
+
76
+ expect { req.get }.to raise_error MissingSecret
77
+ end
78
+ end
79
+
80
+ describe '#params' do
81
+ before do
82
+ req.configure do |c|
83
+ c.key = 'foo'
84
+ c.tag = 'bar'
85
+ end
86
+ end
87
+
88
+ it 'raises an error if key is missing' do
89
+ req.configure { |c| c.key = nil }
90
+
91
+ expect { req.params }.to raise_error MissingKey
92
+ end
93
+
94
+ it 'raises an error if tag is missing' do
95
+ req.configure { |c| c.tag = nil }
96
+
97
+ expect { req.params }.to raise_error MissingTag
98
+ end
99
+
100
+ it 'includes common request parameters' do
101
+ req.params['Service'].should eql 'AWSECommerceService'
102
+ end
103
+
104
+ it 'includes credentials' do
105
+ req.params.should have_key 'AWSAccessKeyId'
106
+ req.params.should have_key 'AssociateTag'
107
+ end
108
+
109
+ it 'includes a timestamp' do
110
+ req.params['Timestamp'].should =~ /^\d+-\d+-\d+T\d+:\d+:\d+Z$/
111
+ end
112
+
113
+ context 'when no API version is specified' do
114
+ it 'includes the current API version' do
115
+ req.params['Version'].should eql Request::CURRENT_API_VERSION
116
+ end
117
+ end
118
+
119
+ context 'when an API version is specified' do
120
+ it 'includes the specified API version' do
121
+ req << { 'Version' => '1' }
122
+ req.params['Version'].should eql '1'
123
+ end
124
+ end
125
+ end
126
+
127
+ describe '#reset!' do
128
+ before do
129
+ req.configure do |c|
130
+ c.key = 'foo'
131
+ c.tag = 'bar'
132
+ end
133
+ end
134
+
135
+ it 'resets the request parameters' do
136
+ req << { 'Key' => 'value' }
137
+ req.params.should have_key 'Key'
138
+
139
+ req.reset!
140
+ req.params.should_not have_key 'Key'
141
+ end
142
+ end
143
+
144
+ describe '#url' do
145
+ before do
146
+ req.configure do |c|
147
+ c.key = 'foo'
148
+ c.secret = 'bar'
149
+ c.tag = 'baz'
150
+ end
151
+ end
152
+
153
+ it 'builds a URL' do
154
+ req.url.should be_a URI::HTTP
155
+ end
156
+
157
+ it 'canonicalizes the request parameters' do
158
+ req.url.query.should match /\w+=\w+&/
159
+ end
160
+
161
+ it 'sorts the request parameters' do
162
+ req << { 'A' => 1 }
163
+ req.url.query.should match /^A=1&/
164
+ end
165
+
166
+ it 'URL-encodes values' do
167
+ req << { :key => 'foo,bar' }
168
+ req.url.query.should match /foo%2Cbar/
169
+ end
170
+
171
+ it 'signs the query' do
172
+ req.url.query.should match /&Signature=/
173
+ end
174
+
175
+ it 'raises an error if no secret is specified' do
176
+ expect do
177
+ req.configure { |c| c.secret = nil }
178
+ req.url
179
+ end.to raise_error MissingSecret
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+
3
+ module Vacuum
4
+ describe Response do
5
+ let(:resp) do
6
+ body = File.read(File.expand_path('../../fixtures/http_response', __FILE__))
7
+ code = '200'
8
+ Response.new(body, code)
9
+ end
10
+
11
+ describe '#each' do
12
+ it 'yields matches to given block' do
13
+ yielded = false
14
+ resp.each('Item') do |item|
15
+ yielded = true
16
+ end
17
+
18
+ yielded.should be_true
19
+ end
20
+ end
21
+
22
+ describe '#errors' do
23
+ it 'returns an array of errors' do
24
+ resp.body = <<-EOF.gsub!(/>\s+</, '><').strip!
25
+ <?xml version=\"1.0\" ?>
26
+ <resp xmlns="http://example.com">
27
+ <Errors>
28
+ <Error>foo</Error>
29
+ </Errors>
30
+ </resp>
31
+ EOF
32
+
33
+ resp.errors.should =~ ['foo']
34
+ end
35
+ end
36
+
37
+ describe '#has_errors?' do
38
+ context 'when a resp does not contain any errors' do
39
+ it 'returns false' do
40
+ resp.stub!(:errors).and_return([])
41
+
42
+ resp.should_not have_errors
43
+ end
44
+ end
45
+
46
+ context 'when a resp contains errors' do
47
+ it 'returns true' do
48
+ resp.stub!(:errors).and_return([1])
49
+
50
+ resp.should have_errors
51
+ end
52
+ end
53
+ end
54
+
55
+ describe '#find' do
56
+ it 'returns an array of matching nodes' do
57
+ resp.find('ASIN').should_not be_empty
58
+ end
59
+ end
60
+
61
+ describe "#map" do
62
+ it "yields each match to a block and maps returned values" do
63
+ titles = resp.map('Item') { |i| i['ItemAttributes']['Title'] }
64
+
65
+ titles.count.should eql 2
66
+ end
67
+ end
68
+
69
+ describe '#to_hash' do
70
+ it 'casts resp to a hash' do
71
+ resp.to_hash.should be_a Hash
72
+ end
73
+ end
74
+
75
+ describe '#valid?' do
76
+ context 'when HTTP status is OK' do
77
+ it 'returns true' do
78
+ resp.should be_valid
79
+ end
80
+ end
81
+
82
+ context 'when HTTP status is not OK' do
83
+ it 'returns false' do
84
+ resp.code = 403
85
+ resp.should_not be_valid
86
+ end
87
+ end
88
+ end
89
+
90
+ describe '#xml' do
91
+ it 'returns a Nokogiri document' do
92
+ resp.xml.should be_an_instance_of Nokogiri::XML::Document
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ module Vacuum
4
+ describe SearchOperations do
5
+ let(:req) { Request.new('us') }
6
+
7
+ before do
8
+ req.configure do |c|
9
+ c.key = 'foo'
10
+ c.tag = 'bar'
11
+ end
12
+ req.stub!(:get)
13
+ end
14
+
15
+ describe "#search" do
16
+ context "when given a keyword" do
17
+ before do
18
+ req.search('foo')
19
+ end
20
+
21
+ it "does a keyword search" do
22
+ req.params['Keywords'].should eql 'foo'
23
+ end
24
+
25
+ it "searches all products" do
26
+ req.params["SearchIndex"].should eql 'All'
27
+ end
28
+ end
29
+
30
+ context "when given a search index and a keyword" do
31
+ before do
32
+ req.search('foo', 'bar')
33
+ end
34
+
35
+ it "does a keyword search" do
36
+ req.params['Keywords'].should eql 'bar'
37
+ end
38
+
39
+ it "sets the search index" do
40
+ req.params["SearchIndex"].should eql 'foo'
41
+ end
42
+ end
43
+
44
+ context "when given a search index and parameters" do
45
+ before do
46
+ req.search('foo', :bar => 'baz')
47
+ end
48
+
49
+ it "sets the parameters" do
50
+ req.params['Bar'].should eql 'baz'
51
+ end
52
+
53
+ it "sets the search index" do
54
+ req.params["SearchIndex"].should eql 'foo'
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vacuum
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Hakan Ensari
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: nokogiri
16
+ requirement: &70206980398600 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.4'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70206980398600
25
+ description: Vacuum is a Ruby wrapper to the Amazon Product Advertising API.
26
+ email:
27
+ - code@papercavalier.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - lib/vacuum/builder.rb
33
+ - lib/vacuum/cart.rb
34
+ - lib/vacuum/cart_operations.rb
35
+ - lib/vacuum/error.rb
36
+ - lib/vacuum/locale.rb
37
+ - lib/vacuum/lookup_operations.rb
38
+ - lib/vacuum/request.rb
39
+ - lib/vacuum/response.rb
40
+ - lib/vacuum/search_operations.rb
41
+ - lib/vacuum/version.rb
42
+ - lib/vacuum.rb
43
+ - LICENSE
44
+ - README.md
45
+ - spec/fixtures/http_response
46
+ - spec/spec_helper.rb
47
+ - spec/vacuum/builder_spec.rb
48
+ - spec/vacuum/cart_operations_spec.rb
49
+ - spec/vacuum/cart_spec.rb
50
+ - spec/vacuum/lookup_operations_spec.rb
51
+ - spec/vacuum/request_spec.rb
52
+ - spec/vacuum/response_spec.rb
53
+ - spec/vacuum/search_operations_spec.rb
54
+ homepage: http://code.papercavalier.com/vacuum/
55
+ licenses: []
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ! '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubyforge_project:
74
+ rubygems_version: 1.8.10
75
+ signing_key:
76
+ specification_version: 3
77
+ summary: A Ruby wrapper to the Amazon Product Advertising API
78
+ test_files:
79
+ - spec/fixtures/http_response
80
+ - spec/spec_helper.rb
81
+ - spec/vacuum/builder_spec.rb
82
+ - spec/vacuum/cart_operations_spec.rb
83
+ - spec/vacuum/cart_spec.rb
84
+ - spec/vacuum/lookup_operations_spec.rb
85
+ - spec/vacuum/request_spec.rb
86
+ - spec/vacuum/response_spec.rb
87
+ - spec/vacuum/search_operations_spec.rb