vacuum 0.0.1

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/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