dpickett-amazon_associate 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,34 @@
1
+ 0.6.1 2008-11-10
2
+ * renamed to amazon_associate
3
+
4
+ 0.6 2008-11-09
5
+ * separated the classes in ecs.rb
6
+ * filesystem caching available
7
+ * more test coverage
8
+
9
+ 0.5.4 2008-10-21
10
+ * include Chris Martin's patches
11
+ * rename Gem to match Amazon's new name for ECS - Amazon Associates API
12
+ * update to git gemspec format
13
+
14
+ 0.5.3 2007-09-12
15
+ ----------------
16
+ * send_request to use default options.
17
+
18
+ 0.5.2 2007-09-08
19
+ ----------------
20
+ * Fixed AmazonAssociate::Element.get_unescaped error when result returned for given element path is nil
21
+
22
+ 0.5.1 2007-02-08
23
+ ----------------
24
+ * Fixed Amazon Japan and France URL error
25
+ * Removed opts.delete(:search_index) from item_lookup, SearchIndex param is allowed
26
+ when looking for a book with IdType other than the ASIN.
27
+ * Check for defined? RAILS_DEFAULT_LOGGER to avoid exception for non-rails ruby app
28
+ * Added check for LOGGER constant if RAILS_DEFAULT_LOGGER is not defined
29
+ * Added Ecs.configure(&proc) method for easier configuration of default options
30
+ * Added Element#search_and_convert method
31
+
32
+ 0.5.0 2006-09-12
33
+ ----------------
34
+ Initial Release
data/README ADDED
@@ -0,0 +1,108 @@
1
+ == amazon-associate
2
+
3
+ Generic Amazon E-commerce REST API using Hpricot with configurable
4
+ default options and method call options. Uses Response and
5
+ Element wrapper classes for easy access to REST XML output. It supports ECS 4.0.
6
+
7
+ It is generic, so you can easily extend <tt>AmazonAssociate::Request</tt> to support
8
+ other not implemented REST operations; and it is also generic because it just wraps around
9
+ Hpricot element object, instead of providing one-to-one object/attributes to XML elements map.
10
+
11
+ If in the future, there is a change in REST XML output structure,
12
+ no changes will be required on <tt>amazon-ecs</tt> library,
13
+ instead you just need to change the element path.
14
+
15
+ Version: 0.6.1
16
+
17
+ == WANTS
18
+ * instance based refactoring (singletons are not ideal here)
19
+
20
+ == INSTALLATION
21
+
22
+ $ gem install dpickett-amazon_associate
23
+
24
+ == EXAMPLE
25
+
26
+ require 'amazon_associate'
27
+
28
+ # set the default options; options will be camelized and converted to REST request parameters.
29
+ AmazonAssociate::Request.options = {:aWS_access_key_id => [your developer token]}
30
+
31
+ # options provided on method call will merge with the default options
32
+ res = AmazonAssociate::Request.item_search('ruby', {:response_group => 'Medium', :sort => 'salesrank'})
33
+
34
+ # some common response object methods
35
+ res.is_valid_request? # return true if request is valid
36
+ res.has_error? # return true if there is an error
37
+ res.error # return error message if there is any
38
+ res.total_pages # return total pages
39
+ res.total_results # return total results
40
+ res.item_page # return current page no if :item_page option is provided
41
+
42
+ # traverse through each item (AmazonAssociate::Element)
43
+ res.items.each do |item|
44
+ # retrieve string value using XML path
45
+ item.get('asin')
46
+ item.get('itemattributes/title')
47
+
48
+ # or return AmazonAssociate::Element instance
49
+ atts = item.search_and_convert('itemattributes')
50
+ atts.get('title')
51
+
52
+ # return first author or a string array of authors
53
+ atts.get('author') # 'Author 1'
54
+ atts.get_array('author') # ['Author 1', 'Author 2', ...]
55
+
56
+ # return an hash of children text values with the element names as the keys
57
+ item.get_hash('smallimage') # {:url => ..., :width => ..., :height => ...}
58
+
59
+ # note that '/' returns Hpricot::Elements array object, nil if not found
60
+ reviews = item/'editorialreview'
61
+
62
+ # traverse through Hpricot elements
63
+ reviews.each do |review|
64
+ # Getting hash value out of Hpricot element
65
+ AmazonAssociate::Element.get_hash(review) # [:source => ..., :content ==> ...]
66
+
67
+ # Or to get unescaped HTML values
68
+ AmazonAssociate::Element.get_unescaped(review, 'source')
69
+ AmazonAssociate::Element.get_unescaped(review, 'content')
70
+
71
+ # Or this way
72
+ el = AmazonAssociate::Element.new(review)
73
+ el.get_unescaped('source')
74
+ el.get_unescaped('content')
75
+ end
76
+
77
+ # returns AmazonAssociate::Element instead of string
78
+ item.search_and_convert('itemattributes').
79
+ end
80
+
81
+ Refer to Amazon Associate's documentation for more information on Amazon REST request parameters and XML output:
82
+ http://docs.amazonwebservices.com/AWSEcommerceService/2006-09-13/
83
+
84
+ To get a sample of Amazon REST response XML output, use AWSZone.com scratch pad:
85
+ http://www.awszone.com/scratchpads/aws/ecs.us/index.aws
86
+
87
+ == CACHING
88
+
89
+ Filesystem caching is now available.
90
+
91
+ AmazonAssociate::Request.options = {:aWS_access_key_id => [your developer token], :caching_strategy => :filesystem,
92
+ :caching_options => {:disk_quota => 200, :cache_path => <path where you want to store requests>, :sweep_frequency => 4}
93
+ }
94
+
95
+ The above command will cache up to 200MB of requests. It will purge the cache every 4 hours or when the disk quota has been exceeded.
96
+
97
+ Every request will be stored in the cache path. On every request, AmazonAssociate::Request will check for the presence of the cached file before querying Amazon directly.
98
+
99
+ == LINKS
100
+
101
+ * http://amazon-ecs.rubyforge.org
102
+ * http://www.pluitsolutions.com/amazon-ecs
103
+
104
+ == LICENSE
105
+
106
+ (The MIT License)
107
+
108
+ Copyright (c) 2008 Dan Pickett, Enlight Solutions, Inc.
@@ -0,0 +1,9 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ require "amazon_associate/request"
4
+ require "amazon_associate/element"
5
+ require "amazon_associate/response"
6
+ require "amazon_associate/cache_factory"
7
+ require "amazon_associate/caching_strategy"
8
+ require "amazon_associate/configuration_error"
9
+ require "amazon_associate/request_error"
@@ -0,0 +1,30 @@
1
+ module AmazonAssociate
2
+ class CacheFactory
3
+ def self.cache(request, response, strategy)
4
+ strategy_class_hash[strategy].cache(request, response)
5
+ end
6
+
7
+ def self.initialize_options(options)
8
+ #check for a valid caching strategy
9
+ unless self.strategy_class_hash.keys.include?(options[:caching_strategy])
10
+ raise AmazonAssociate::ConfigurationError, "Invalid caching strategy"
11
+ end
12
+ strategy_class_hash[options[:caching_strategy]].initialize_options(options)
13
+ end
14
+
15
+ def self.get(request, strategy)
16
+ strategy_class_hash[strategy].get(request)
17
+ end
18
+
19
+ def self.sweep(strategy)
20
+ strategy_class_hash[strategy].sweep
21
+ end
22
+
23
+ private
24
+ def self.strategy_class_hash
25
+ {
26
+ :filesystem => AmazonAssociate::CachingStrategy::Filesystem
27
+ }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,2 @@
1
+ require File.join(File.dirname(__FILE__), "caching_strategy/base")
2
+ require File.join(File.dirname(__FILE__), "caching_strategy/filesystem")
@@ -0,0 +1,22 @@
1
+ #abstract class
2
+ module AmazonAssociate
3
+ module CachingStrategy
4
+ class Base
5
+ def self.cache(request, response)
6
+ raise "This method must be overwritten by a caching strategy"
7
+ end
8
+
9
+ def self.initialize_options(options)
10
+ raise "This method must be overwritten by a caching strategy"
11
+ end
12
+
13
+ def self.get(request)
14
+ raise "This method must be overwritten by a caching strategy"
15
+ end
16
+
17
+ def self.sweep
18
+ raise "This method must be overwritten by a caching strategy"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,114 @@
1
+ require "fileutils"
2
+ require "find"
3
+
4
+ module AmazonAssociate
5
+ module CachingStrategy
6
+ class Filesystem < AmazonAssociate::CachingStrategy::Base
7
+ #disk quota in megabytes
8
+ DEFAULT_DISK_QUOTA = 200
9
+
10
+ #frequency of sweeping in hours
11
+ DEFAULT_SWEEP_FREQUENCY = 2
12
+
13
+ def self.cache(request, response)
14
+ path = self.cache_path
15
+ cached_filename = Digest::SHA1.hexdigest(response.request_url)
16
+ cached_folder = cached_filename[0..2]
17
+
18
+ FileUtils.mkdir_p(File.join(path, cached_folder, cached_folder))
19
+
20
+ cached_file = File.open(File.join(path, cached_folder, cached_filename), "w")
21
+ cached_file.puts response.doc.to_s
22
+ cached_file.close
23
+ end
24
+
25
+ def self.get(request)
26
+ path = self.cache_path
27
+ cached_filename = Digest::SHA1.hexdigest(request)
28
+ file_path = File.join(path, cached_filename[0..2], cached_filename)
29
+ if FileTest.exists?(file_path)
30
+ File.read(file_path).chomp
31
+ else
32
+ nil
33
+ end
34
+ end
35
+
36
+ def self.initialize_options(options)
37
+ #check for required options
38
+ if options[:caching_options].nil?
39
+ raise AmazonAssociate::ConfigurationError, "You must specify caching options for filesystem caching: :cache_path is required"
40
+ end
41
+
42
+ #default disk quota to 200MB
43
+ @@disk_quota = options[:caching_options][:disk_quota] || DEFAULT_DISK_QUOTA
44
+
45
+ @@sweep_frequency = options[:caching_options][:sweep_frequency] || DEFAULT_SWEEP_FREQUENCY
46
+
47
+ @@cache_path = options[:caching_options][:cache_path]
48
+
49
+ if @@cache_path.nil? || !File.directory?(@@cache_path)
50
+ raise AmazonAssociate::ConfigurationError, "You must specify a cache path for filesystem caching"
51
+ end
52
+
53
+ return options
54
+ end
55
+
56
+ def self.sweep
57
+ self.perform_sweep if must_sweep?
58
+ end
59
+
60
+ def self.disk_quota
61
+ @@disk_quota
62
+ end
63
+
64
+ def self.sweep_frequency
65
+ @@sweep_frequency
66
+ end
67
+
68
+ def self.cache_path
69
+ @@cache_path
70
+ end
71
+
72
+ private
73
+ def self.perform_sweep
74
+ FileUtils.rm_rf(Dir.glob("#{@@cache_path}/*"))
75
+
76
+ self.timestamp_sweep_performance
77
+ end
78
+
79
+ def self.timestamp_sweep_performance
80
+ #remove the timestamp
81
+ FileUtils.rm_rf(self.timestamp_filename)
82
+
83
+ #create a new one its place
84
+ timestamp = File.open(self.timestamp_filename, "w")
85
+ timestamp.puts(Time.now)
86
+ timestamp.close
87
+ end
88
+
89
+ def self.must_sweep?
90
+ sweep_time_expired? || disk_quota_exceeded?
91
+ end
92
+
93
+ def self.sweep_time_expired?
94
+ FileTest.exists?(timestamp_filename) && Time.parse(File.read(timestamp_filename).chomp) < Time.now - (sweep_frequency * 3600)
95
+ end
96
+
97
+ def self.disk_quota_exceeded?
98
+ cache_size > @@disk_quota
99
+ end
100
+
101
+ def self.timestamp_filename
102
+ File.join(self.cache_path, ".amz_timestamp")
103
+ end
104
+
105
+ def self.cache_size
106
+ size = 0
107
+ Find.find(@@cache_path) do|f|
108
+ size += File.size(f) if File.file?(f)
109
+ end
110
+ size / 1000000
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,4 @@
1
+ module AmazonAssociate
2
+ class ConfigurationError < RuntimeError
3
+ end
4
+ end
@@ -0,0 +1,100 @@
1
+ # Internal wrapper class to provide convenient method to access Hpricot element value.
2
+ module AmazonAssociate
3
+ class Element
4
+ # Pass Hpricot::Elements object
5
+ def initialize(element)
6
+ @element = element
7
+ end
8
+
9
+ # Returns Hpricot::Elments object
10
+ def elem
11
+ @element
12
+ end
13
+
14
+ # Find Hpricot::Elements matching the given path. Example: element/"author".
15
+ def /(path)
16
+ elements = @element/path
17
+ return nil if elements.size == 0
18
+ elements
19
+ end
20
+
21
+ # Find Hpricot::Elements matching the given path, and convert to AmazonAssociate::Element.
22
+ # Returns an array AmazonAssociate::Elements if more than Hpricot::Elements size is greater than 1.
23
+ def search_and_convert(path)
24
+ elements = self./(path)
25
+ return unless elements
26
+ elements = elements.map{|element| Element.new(element)}
27
+ return elements.first if elements.size == 1
28
+ elements
29
+ end
30
+
31
+ # Get the text value of the given path, leave empty to retrieve current element value.
32
+ def get(path="")
33
+ Element.get(@element, path)
34
+ end
35
+
36
+ # Get the unescaped HTML text of the given path.
37
+ def get_unescaped(path="")
38
+ Element.get_unescaped(@element, path)
39
+ end
40
+
41
+ # Get the array values of the given path.
42
+ def get_array(path="")
43
+ Element.get_array(@element, path)
44
+ end
45
+
46
+ # Get the children element text values in hash format with the element names as the hash keys.
47
+ def get_hash(path="")
48
+ Element.get_hash(@element, path)
49
+ end
50
+
51
+ # Similar to #get, except an element object must be passed-in.
52
+ def self.get(element, path="")
53
+ return unless element
54
+ result = element.at(path)
55
+ result = result.inner_html if result
56
+ result
57
+ end
58
+
59
+ # Similar to #get_unescaped, except an element object must be passed-in.
60
+ def self.get_unescaped(element, path="")
61
+ result = get(element, path)
62
+ CGI::unescapeHTML(result) if result
63
+ end
64
+
65
+ # Similar to #get_array, except an element object must be passed-in.
66
+ def self.get_array(element, path="")
67
+ return unless element
68
+
69
+ result = element/path
70
+ if (result.is_a? Hpricot::Elements) || (result.is_a? Array)
71
+ parsed_result = []
72
+ result.each {|item|
73
+ parsed_result << Element.get(item)
74
+ }
75
+ parsed_result
76
+ else
77
+ [Element.get(result)]
78
+ end
79
+ end
80
+
81
+ # Similar to #get_hash, except an element object must be passed-in.
82
+ def self.get_hash(element, path="")
83
+ return unless element
84
+
85
+ result = element.at(path)
86
+ if result
87
+ hash = {}
88
+ result = result.children
89
+ result.each do |item|
90
+ hash[item.name.to_sym] = item.inner_html
91
+ end
92
+ hash
93
+ end
94
+ end
95
+
96
+ def to_s
97
+ elem.to_s if elem
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,277 @@
1
+ require "net/http"
2
+ require "hpricot"
3
+ require "cgi"
4
+
5
+ begin
6
+ require 'md5'
7
+ rescue LoadError
8
+ require 'digest/md5'
9
+ end
10
+
11
+ #--
12
+ # Copyright (c) 2006 Herryanto Siatono, Pluit Solutions
13
+ #
14
+ # Permission is hereby granted, free of charge, to any person obtaining
15
+ # a copy of this software and associated documentation files (the
16
+ # "Software"), to deal in the Software without restriction, including
17
+ # without limitation the rights to use, copy, modify, merge, publish,
18
+ # distribute, sublicense, and/or sell copies of the Software, and to
19
+ # permit persons to whom the Software is furnished to do so, subject to
20
+ # the following conditions:
21
+ #
22
+ # The above copyright notice and this permission notice shall be
23
+ # included in all copies or substantial portions of the Software.
24
+ #
25
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
28
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
29
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
30
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
31
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32
+ #++
33
+ module AmazonAssociate
34
+ class Request
35
+
36
+ SERVICE_URLS = {:us => "http://webservices.amazon.com/onca/xml?Service=AWSECommerceService",
37
+ :uk => "http://webservices.amazon.co.uk/onca/xml?Service=AWSECommerceService",
38
+ :ca => "http://webservices.amazon.ca/onca/xml?Service=AWSECommerceService",
39
+ :de => "http://webservices.amazon.de/onca/xml?Service=AWSECommerceService",
40
+ :jp => "http://webservices.amazon.co.jp/onca/xml?Service=AWSECommerceService",
41
+ :fr => "http://webservices.amazon.fr/onca/xml?Service=AWSECommerceService"
42
+ }
43
+
44
+ # The sort types available to each product search index.
45
+ SORT_TYPES = {
46
+ "Apparel" => %w[relevancerank salesrank pricerank inverseprice -launch-date sale-flag],
47
+ "Automotive" => %w[salesrank price -price titlerank -titlerank],
48
+ "Baby" => %w[psrank salesrank price -price titlerank],
49
+ "Beauty" => %w[pmrank salesrank price -price -launch-date sale-flag],
50
+ "Books" => %w[relevancerank salesrank reviewrank pricerank inverse-pricerank daterank titlerank -titlerank],
51
+ "Classical" => %w[psrank salesrank price -price titlerank -titlerank orig-rel-date],
52
+ "DigitalMusic" => %w[songtitlerank uploaddaterank],
53
+ "DVD" => %w[relevancerank salesrank price -price titlerank -video-release-date],
54
+ "Electronics" => %w[pmrank salesrank reviewrank price -price titlerank],
55
+ "GourmetFood" => %w[relevancerank salesrank pricerank inverseprice launch-date sale-flag],
56
+ "HealthPersonalCare" => %w[pmrank salesrank pricerank inverseprice launch-date sale-flag],
57
+ "Jewelry" => %w[pmrank salesrank pricerank inverseprice launch-date],
58
+ "Kitchen" => %w[pmrank salesrank price -price titlerank -titlerank],
59
+ "Magazines" => %w[subslot-salesrank reviewrank price -price daterank titlerank -titlerank],
60
+ "Merchants" => %w[relevancerank salesrank pricerank inverseprice launch-date sale-flag],
61
+ "Miscellaneous" => %w[pmrank salesrank price -price titlerank -titlerank],
62
+ "Music" => %w[psrank salesrank price -price titlerank -titlerank artistrank orig-rel-date release-date],
63
+ "MusicalInstruments" => %w[pmrank salesrank price -price -launch-date sale-flag],
64
+ "MusicTracks" => %w[titlerank -titlerank],
65
+ "OfficeProducts" => %w[pmrank salesrank reviewrank price -price titlerank],
66
+ "OutdoorLiving" => %w[psrank salesrank price -price titlerank -titlerank],
67
+ "PCHardware" => %w[psrank salesrank price -price titlerank],
68
+ "PetSupplies" => %w[+pmrank salesrank price -price titlerank -titlerank],
69
+ "Photo" => %w[pmrank salesrank titlerank -titlerank],
70
+ "Restaurants" => %w[relevancerank titlerank],
71
+ "Software" => %w[pmrank salesrank titlerank price -price],
72
+ "SportingGoods" => %w[relevancerank salesrank pricerank inverseprice launch-date sale-flag],
73
+ "Tools" => %w[pmrank salesrank titlerank -titlerank price -price],
74
+ "Toys" => %w[pmrank salesrank price -price titlerank -age-min],
75
+ "VHS" => %w[relevancerank salesrank price -price titlerank -video-release-date],
76
+ "Video" => %w[relevancerank salesrank price -price titlerank -video-release-date],
77
+ "VideoGames" => %w[pmrank salesrank price -price titlerank],
78
+ "Wireless" => %w[daterank pricerank invers-pricerank reviewrank salesrank titlerank -titlerank],
79
+ "WirelessAccessories" => %w[psrank salesrank titlerank -titlerank]
80
+ }
81
+
82
+ # Returns an Array of valid sort types for _search_index_, or +nil+ if _search_index_ is invalid.
83
+ def self.sort_types(search_index)
84
+ SORT_TYPES.has_key?(search_index) ? SORT_TYPES[search_index] : nil
85
+ end
86
+
87
+ # Performs BrowseNodeLookup request, defaults to TopSellers ResponseGroup
88
+ def self.browse_node_lookup(browse_node_id, opts = {})
89
+ opts = self.options.merge(opts) if self.options
90
+ opts[:response_group] = opts[:response_group] || "TopSellers"
91
+ opts[:operation] = "BrowseNodeLookup"
92
+ opts[:browse_node_id] = browse_node_id
93
+
94
+ self.send_request(opts)
95
+ end
96
+
97
+ # Cart operations build the Item tags from the ASIN
98
+ # Item.ASIN.Quantity defaults to 1, unless otherwise specified in _opts_
99
+
100
+ # Creates remote shopping cart containing _asin_
101
+ def self.cart_create(asin, opts = {})
102
+ opts = self.options.merge(opts) if self.options
103
+ opts[:operation] = "CartCreate"
104
+ opts["Item.#{asin}.Quantity"] = opts[:quantity] || 1
105
+ opts["Item.#{asin}.ASIN"] = asin
106
+
107
+ self.send_request(opts)
108
+ end
109
+
110
+ # Adds item to remote shopping cart
111
+ def self.cart_add(asin, cart_id, hmac, opts = {})
112
+ opts = self.options.merge(opts) if self.options
113
+ opts[:operation] = "CartAdd"
114
+ opts["Item.#{asin}.Quantity"] = opts[:quantity] || 1
115
+ opts["Item.#{asin}.ASIN"] = asin
116
+ opts[:cart_id] = cart_id
117
+ opts[:hMAC] = hmac
118
+
119
+ self.send_request(opts)
120
+ end
121
+
122
+ # Adds item to remote shopping cart
123
+ def self.cart_get(cart_id, hmac, opts = {})
124
+ opts = self.options.merge(opts) if self.options
125
+ opts[:operation] = "CartGet"
126
+ opts[:cart_id] = cart_id
127
+ opts[:hMAC] = hmac
128
+
129
+ self.send_request(opts)
130
+ end
131
+
132
+ # modifies _cart_item_id_ in remote shopping cart
133
+ # _quantity_ defaults to 0 to remove the given _cart_item_id_
134
+ # specify _quantity_ to update cart contents
135
+ def self.cart_modify(cart_item_id, cart_id, hmac, quantity=0, opts = {})
136
+ opts = self.options.merge(opts) if self.options
137
+ opts[:operation] = "CartModify"
138
+ opts["Item.1.CartItemId"] = cart_item_id
139
+ opts["Item.1.Quantity"] = quantity
140
+ opts[:cart_id] = cart_id
141
+ opts[:hMAC] = hmac
142
+
143
+ self.send_request(opts)
144
+ end
145
+
146
+ # clears contents of remote shopping cart
147
+ def self.cart_clear(cart_id, hmac, opts = {})
148
+ opts = self.options.merge(opts) if self.options
149
+ opts[:operation] = "CartClear"
150
+ opts[:cart_id] = cart_id
151
+ opts[:hMAC] = hmac
152
+
153
+ self.send_request(opts)
154
+ end
155
+ @@options = {}
156
+ @@debug = false
157
+
158
+ # Default search options
159
+ def self.options
160
+ @@options
161
+ end
162
+
163
+ # Set default search options
164
+ def self.options=(opts)
165
+ @@options = opts
166
+ end
167
+
168
+ # Get debug flag.
169
+ def self.debug
170
+ @@debug
171
+ end
172
+
173
+ # Set debug flag to true or false.
174
+ def self.debug=(dbg)
175
+ @@debug = dbg
176
+ end
177
+
178
+ def self.configure(&proc)
179
+ raise ArgumentError, "Block is required." unless block_given?
180
+
181
+ yield @@options
182
+ if !@@options[:caching_strategy].nil?
183
+ @@options.merge!(CacheFactory.initialize_options(@@options))
184
+ end
185
+ end
186
+
187
+ # Search amazon items with search terms. Default search index option is "Books".
188
+ # For other search type other than keywords, please specify :type => [search type param name].
189
+ def self.item_search(terms, opts = {})
190
+ opts[:operation] = "ItemSearch"
191
+ opts[:search_index] = opts[:search_index] || "Books"
192
+
193
+ type = opts.delete(:type)
194
+ if type
195
+ opts[type.to_sym] = terms
196
+ else
197
+ opts[:keywords] = terms
198
+ end
199
+
200
+ self.send_request(opts)
201
+ end
202
+
203
+ # Search an item by ASIN no.
204
+ def self.item_lookup(item_id, opts = {})
205
+ opts[:operation] = "ItemLookup"
206
+ opts[:item_id] = item_id
207
+
208
+ self.send_request(opts)
209
+ end
210
+
211
+ # Generic send request to ECS REST service. You have to specify the :operation parameter.
212
+ def self.send_request(opts)
213
+ opts = self.options.merge(opts) if self.options
214
+ request_url = prepare_url(opts)
215
+ response = nil
216
+
217
+ if caching_enabled?
218
+ AmazonAssociate::CacheFactory.sweep(self.options[:caching_strategy])
219
+
220
+ res = AmazonAssociate::CacheFactory.get(request_url, self.options[:caching_strategy])
221
+ response = Response.new(res, request_url) unless res.nil?
222
+ end
223
+
224
+ if !caching_enabled? || response.nil?
225
+ log "Request URL: #{request_url}"
226
+ res = Net::HTTP.get_response(URI::parse(request_url))
227
+ unless res.kind_of? Net::HTTPSuccess
228
+ raise AmazonAssociate::RequestError, "HTTP Response: #{res.code} #{res.message}"
229
+ end
230
+ response = Response.new(res.body, request_url)
231
+ cache_response(request_url, response, self.options[:caching_strategy]) if caching_enabled?
232
+ end
233
+
234
+ response
235
+ end
236
+
237
+ protected
238
+ def self.log(s)
239
+ return unless self.debug
240
+ if defined? RAILS_DEFAULT_LOGGER
241
+ RAILS_DEFAULT_LOGGER.error(s)
242
+ elsif defined? LOGGER
243
+ LOGGER.error(s)
244
+ else
245
+ puts s
246
+ end
247
+ end
248
+
249
+ private
250
+ def self.prepare_url(opts)
251
+ country = opts.delete(:country)
252
+ country = (country.nil?) ? "us" : country
253
+ request_url = SERVICE_URLS[country.to_sym]
254
+ raise AmazonAssociate::RequestError, "Invalid country \"#{country}\"" unless request_url
255
+
256
+ qs = ""
257
+ opts.each {|k,v|
258
+ next unless v
259
+ v = v.join(",") if v.is_a? Array
260
+ qs << "&#{camelize(k.to_s)}=#{URI.encode(v.to_s)}"
261
+ }
262
+ "#{request_url}#{qs}"
263
+ end
264
+
265
+ def self.camelize(s)
266
+ s.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
267
+ end
268
+
269
+ def self.caching_enabled?
270
+ !self.options[:caching_strategy].nil?
271
+ end
272
+
273
+ def self.cache_response(request, response, options)
274
+ AmazonAssociate::CacheFactory.cache(request, response, options)
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,4 @@
1
+ module AmazonAssociate
2
+ class RequestError < RuntimeError
3
+ end
4
+ end
@@ -0,0 +1,74 @@
1
+ module AmazonAssociate
2
+ # Response object returned after a REST call to Amazon service.
3
+ class Response
4
+
5
+ attr_accessor :request_url
6
+ # XML input is in string format
7
+ def initialize(xml, request_url)
8
+ @doc = Hpricot(xml)
9
+ @items = nil
10
+ @item_page = nil
11
+ @total_results = nil
12
+ @total_pages = nil
13
+
14
+ self.request_url = request_url
15
+ end
16
+
17
+ # Return Hpricot object.
18
+ def doc
19
+ @doc
20
+ end
21
+
22
+ # Return true if request is valid.
23
+ def is_valid_request?
24
+ (@doc/"isvalid").inner_html == "True"
25
+ end
26
+
27
+ # Return true if response has an error.
28
+ def has_error?
29
+ !(error.nil? || error.empty?)
30
+ end
31
+
32
+ # Return error message.
33
+ def error
34
+ Element.get(@doc, "error/message")
35
+ end
36
+
37
+ # Return an array of AmazonAssociate::Element item objects.
38
+ def items
39
+ unless @items
40
+ @items = (@doc/"item").collect {|item| Element.new(item)}
41
+ end
42
+ @items
43
+ end
44
+
45
+ # Return the first item (AmazonAssociate::Element)
46
+ def first_item
47
+ items.first
48
+ end
49
+
50
+ # Return current page no if :item_page option is when initiating the request.
51
+ def item_page
52
+ unless @item_page
53
+ @item_page = (@doc/"itemsearchrequest/itempage").inner_html.to_i
54
+ end
55
+ @item_page
56
+ end
57
+
58
+ # Return total results.
59
+ def total_results
60
+ unless @total_results
61
+ @total_results = (@doc/"totalresults").inner_html.to_i
62
+ end
63
+ @total_results
64
+ end
65
+
66
+ # Return total pages.
67
+ def total_pages
68
+ unless @total_pages
69
+ @total_pages = (@doc/"totalpages").inner_html.to_i
70
+ end
71
+ @total_pages
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,38 @@
1
+ require File.dirname(__FILE__) + "/../test_helper"
2
+
3
+ class AmazonAssociate::BrowseNodeLookupTest < Test::Unit::TestCase
4
+
5
+ ## Test browse_node_lookup
6
+ def test_browse_node_lookup
7
+ resp = AmazonAssociate::Request.browse_node_lookup("5")
8
+ assert resp.is_valid_request?
9
+ browse_node_tags = resp.doc.get_elements_by_tag_name("browsenodeid")
10
+ browse_node_tags.each { |node| assert_equal("5", node.inner_text) }
11
+ assert_equal "TopSellers", resp.doc.get_elements_by_tag_name("responsegroup").inner_text
12
+ end
13
+
14
+ def test_browse_node_lookup_with_browse_node_info_response
15
+ resp = AmazonAssociate::Request.browse_node_lookup("5", :response_group => "BrowseNodeInfo")
16
+ assert resp.is_valid_request?
17
+ assert_equal "BrowseNodeInfo", resp.doc.get_elements_by_tag_name("responsegroup").inner_text
18
+ end
19
+
20
+ def test_browse_node_lookup_with_new_releases_response
21
+ resp = AmazonAssociate::Request.browse_node_lookup("5", :response_group => "NewReleases")
22
+ assert resp.is_valid_request?
23
+ assert_equal "NewReleases", resp.doc.get_elements_by_tag_name("responsegroup").inner_text
24
+ end
25
+
26
+ def test_browse_node_lookup_with_invalid_request
27
+ resp = AmazonAssociate::Request.browse_node_lookup(nil)
28
+ assert resp.has_error?
29
+ assert resp.error
30
+ end
31
+
32
+ def test_browse_node_lookup_with_no_result
33
+ resp = AmazonAssociate::Request.browse_node_lookup("abc")
34
+
35
+ assert resp.is_valid_request?
36
+ assert_match(/abc is not a valid value for BrowseNodeId/, resp.error)
37
+ end
38
+ end
@@ -0,0 +1,34 @@
1
+ require File.dirname(__FILE__) + "/../test_helper"
2
+
3
+ class AmazonAssociate::CacheTest < Test::Unit::TestCase
4
+ include FilesystemTestHelper
5
+ context "caching get" do
6
+ setup do
7
+ get_cache_directory
8
+ get_valid_caching_options
9
+ end
10
+
11
+ teardown do
12
+ destroy_cache_directory
13
+ destroy_caching_options
14
+ end
15
+
16
+ should "optionally allow for a caching strategy in configuration" do
17
+ assert_nothing_raised do
18
+ AmazonAssociate::Request.configure do |options|
19
+ options[:caching_strategy] = :filesystem
20
+ end
21
+ end
22
+ end
23
+
24
+ should "raise an exception if a caching strategy is specified that is not found" do
25
+ assert_raises(AmazonAssociate::ConfigurationError) do
26
+ AmazonAssociate::Request.configure do |options|
27
+ options[:caching_strategy] = "foo"
28
+ end
29
+ end
30
+ end
31
+
32
+
33
+ end
34
+ end
@@ -0,0 +1,183 @@
1
+ require File.dirname(__FILE__) + "/../../test_helper"
2
+
3
+ class AmazonAssociate::CachingStrategy::FilesystemTest < Test::Unit::TestCase
4
+ include FilesystemTestHelper
5
+ context "setting up filesystem caching" do
6
+ teardown do
7
+ AmazonAssociate::Request.configure do |options|
8
+ options[:caching_strategy] = nil
9
+ options[:caching_options] = nil
10
+ end
11
+ end
12
+
13
+ should "require a caching options hash with a cache_path key" do
14
+ assert_raises(AmazonAssociate::ConfigurationError) do
15
+ AmazonAssociate::Request.configure do |options|
16
+ options[:caching_strategy] = :filesystem
17
+ options[:caching_options] = nil
18
+ end
19
+ end
20
+ end
21
+
22
+ should "raise an exception when a cache_path is specified that doesn't exist" do
23
+ assert_raises(AmazonAssociate::ConfigurationError) do
24
+ AmazonAssociate::Request.configure do |options|
25
+ options[:caching_strategy] = :filesystem
26
+ options[:caching_options] = {:cache_path => "foo123"}
27
+ end
28
+ end
29
+ end
30
+
31
+ should "set default values for disk_quota and sweep_frequency" do
32
+ AmazonAssociate::Request.configure do |options|
33
+ options[:caching_strategy] = :filesystem
34
+ options[:caching_options] = {:cache_path => "."}
35
+ end
36
+
37
+ assert_equal AmazonAssociate::CachingStrategy::Filesystem.disk_quota, AmazonAssociate::CachingStrategy::Filesystem.disk_quota
38
+ assert_equal AmazonAssociate::CachingStrategy::Filesystem.sweep_frequency, AmazonAssociate::CachingStrategy::Filesystem.sweep_frequency
39
+ end
40
+
41
+ should "override the default value for disk quota if I specify one" do
42
+ quota = 400
43
+ AmazonAssociate::Request.configure do |options|
44
+ options[:caching_strategy] = :filesystem
45
+ options[:caching_options] = {:cache_path => ".", :disk_quota => quota}
46
+ end
47
+
48
+ assert_equal quota, AmazonAssociate::CachingStrategy::Filesystem.disk_quota
49
+ end
50
+
51
+ should "override the default value for cache_frequency if I specify one" do
52
+ frequency = 4
53
+ AmazonAssociate::Request.configure do |options|
54
+ options[:caching_strategy] = :filesystem
55
+ options[:caching_options] = {:cache_path => ".", :sweep_frequency => frequency}
56
+ end
57
+
58
+ assert_equal frequency, AmazonAssociate::CachingStrategy::Filesystem.sweep_frequency
59
+ end
60
+ end
61
+
62
+ context "caching a request" do
63
+
64
+ setup do
65
+ get_cache_directory
66
+ get_valid_caching_options
67
+ @resp = AmazonAssociate::Request.item_lookup("0974514055")
68
+ @filename = Digest::SHA1.hexdigest(@resp.request_url)
69
+ end
70
+
71
+ teardown do
72
+ destroy_cache_directory
73
+ destroy_caching_options
74
+ end
75
+
76
+ should "create a folder in the cache path with the first three letters of the digested filename" do
77
+ filename = Digest::SHA1.hexdigest(@resp.request_url)
78
+ FileTest.exists?(File.join(@@cache_path, @filename[0..2]))
79
+ end
80
+
81
+ should "create a file in the cache path with a digested version of the url " do
82
+
83
+ filename = Digest::SHA1.hexdigest(@resp.request_url)
84
+ assert FileTest.exists?(File.join(@@cache_path, @filename[0..2], @filename))
85
+ end
86
+
87
+ should "create a file in the cache path with the response inside it" do
88
+ assert FileTest.exists?(File.join(@@cache_path + @filename[0..2], @filename))
89
+ assert_equal @resp.doc.to_s, File.read(File.join(@@cache_path + @filename[0..2], @filename)).chomp
90
+ end
91
+ end
92
+
93
+ context "getting a cached request" do
94
+ setup do
95
+ get_cache_directory
96
+ get_valid_caching_options
97
+ do_request
98
+ end
99
+
100
+ teardown do
101
+ destroy_cache_directory
102
+ destroy_caching_options
103
+ end
104
+
105
+ should "not do an http request the second time the lookup is performed due a cached copy" do
106
+ Net::HTTP.expects(:get_response).never
107
+ do_request
108
+ end
109
+
110
+ should "return the same response as the original request" do
111
+ original = @resp.doc.to_s
112
+ do_request
113
+ assert_equal(original, @resp.doc.to_s)
114
+ end
115
+ end
116
+
117
+ context "sweeping cached requests" do
118
+ setup do
119
+ get_cache_directory
120
+ get_valid_caching_options
121
+ do_request
122
+ end
123
+
124
+ teardown do
125
+ destroy_cache_directory
126
+ destroy_caching_options
127
+ end
128
+
129
+ should "not perform the sweep if the timestamp is within the range of the sweep frequency and quota is not exceeded" do
130
+ AmazonAssociate::CachingStrategy::Filesystem.expects(:sweep_time_expired?).returns(false)
131
+ AmazonAssociate::CachingStrategy::Filesystem.expects(:disk_quota_exceeded?).returns(false)
132
+
133
+ AmazonAssociate::CachingStrategy::Filesystem.expects(:perform_sweep).never
134
+
135
+ do_request
136
+ end
137
+
138
+ should "perform a sweep if the quota is exceeded" do
139
+ AmazonAssociate::CachingStrategy::Filesystem.stubs(:sweep_time_expired?).returns(false)
140
+ AmazonAssociate::CachingStrategy::Filesystem.expects(:disk_quota_exceeded?).once.returns(true)
141
+
142
+ AmazonAssociate::CachingStrategy::Filesystem.expects(:perform_sweep).once
143
+
144
+ do_request
145
+ end
146
+
147
+ should "perform a sweep if the sweep time is expired" do
148
+ AmazonAssociate::CachingStrategy::Filesystem.expects(:sweep_time_expired?).once.returns(true)
149
+ AmazonAssociate::CachingStrategy::Filesystem.stubs(:disk_quota_exceeded?).returns(false)
150
+ AmazonAssociate::CachingStrategy::Filesystem.expects(:perform_sweep).once
151
+
152
+ do_request
153
+ end
154
+
155
+ should "create a timestamp file after performing a sweep" do
156
+ AmazonAssociate::CachingStrategy::Filesystem.expects(:sweep_time_expired?).once.returns(true)
157
+
158
+ do_request
159
+ assert FileTest.exists?(File.join(@@cache_path, ".amz_timestamp"))
160
+ end
161
+
162
+ should "purge the cache when performing a sweep" do
163
+ (0..9).each do |n|
164
+ test = File.open(File.join(@@cache_path, "test_file_#{n}"), "w")
165
+ test.puts Time.now
166
+ test.close
167
+ end
168
+
169
+ AmazonAssociate::CachingStrategy::Filesystem.expects(:sweep_time_expired?).once.returns(true)
170
+ do_request
171
+
172
+ (0..9).each do |n|
173
+ assert !FileTest.exists?(File.join(@@cache_path, "test_file_#{n}"))
174
+ end
175
+ end
176
+
177
+ end
178
+
179
+ protected
180
+ def do_request
181
+ @resp = AmazonAssociate::Request.item_lookup("0974514055")
182
+ end
183
+ end
@@ -0,0 +1,58 @@
1
+ require File.dirname(__FILE__) + "/../test_helper"
2
+
3
+ class AmazonAssociate::CartTest < Test::Unit::TestCase
4
+
5
+ # create a cart to store cart_id and hmac for add, get, modify, and clear tests
6
+ def setup
7
+ @asin = "0672328844"
8
+ resp = AmazonAssociate::Request.cart_create(@asin)
9
+ @cart_id = resp.doc.get_elements_by_tag_name("cartid").inner_text
10
+ @hmac = resp.doc.get_elements_by_tag_name("hmac").inner_text
11
+ item = resp.first_item
12
+ # run tests for cart_create with default quantity while we"re at it
13
+ assert resp.is_valid_request?
14
+ assert_equal @asin, item.get("asin")
15
+ assert_equal "1", item.get("quantity")
16
+ assert_not_nil @cart_id
17
+ assert_not_nil @hmac
18
+ end
19
+
20
+ # Test cart_get
21
+ def test_cart_get
22
+ resp = AmazonAssociate::Request.cart_get(@cart_id, @hmac)
23
+ assert resp.is_valid_request?
24
+ assert_not_nil resp.doc.get_elements_by_tag_name("purchaseurl").inner_text
25
+ end
26
+
27
+ # Test cart_modify
28
+ def test_cart_modify
29
+ resp = AmazonAssociate::Request.cart_get(@cart_id, @hmac)
30
+ cart_item_id = resp.doc.get_elements_by_tag_name("cartitemid").inner_text
31
+ resp = AmazonAssociate::Request.cart_modify(cart_item_id, @cart_id, @hmac, 2)
32
+ item = resp.first_item
33
+
34
+ assert resp.is_valid_request?
35
+ assert_equal "2", item.get("quantity")
36
+ assert_not_nil resp.doc.get_elements_by_tag_name("purchaseurl").inner_text
37
+ end
38
+
39
+ # Test cart_clear
40
+ def test_cart_clear
41
+ resp = AmazonAssociate::Request.cart_clear(@cart_id, @hmac)
42
+ assert resp.is_valid_request?
43
+ end
44
+
45
+ ## Test cart_create with a specified quantity
46
+ ## note this will create a separate cart
47
+ def test_cart_create_with_quantity
48
+ asin = "0672328844"
49
+ resp = AmazonAssociate::Request.cart_create(asin, :quantity => 2)
50
+ assert resp.is_valid_request?
51
+ item = resp.first_item
52
+ assert_equal asin, item.get("asin")
53
+ assert_equal "2", item.get("quantity")
54
+ assert_not_nil resp.doc.get_elements_by_tag_name("cartid").inner_text
55
+ assert_not_nil resp.doc.get_elements_by_tag_name("hmac").inner_text
56
+ end
57
+
58
+ end
@@ -0,0 +1,108 @@
1
+ require File.dirname(__FILE__) + "/../test_helper"
2
+
3
+ class AmazonAssociate::RequestTest < Test::Unit::TestCase
4
+ def setup
5
+ AmazonAssociate::Request.configure do |options|
6
+ options[:response_group] = "Large"
7
+
8
+ end
9
+ end
10
+
11
+ ## Test item_search
12
+ def test_item_search
13
+ resp = AmazonAssociate::Request.item_search("ruby")
14
+ assert(resp.is_valid_request?)
15
+ assert(resp.total_results >= 3600)
16
+ assert(resp.total_pages >= 360)
17
+ end
18
+
19
+ def test_item_search_with_paging
20
+ resp = AmazonAssociate::Request.item_search("ruby", :item_page => 2)
21
+ assert resp.is_valid_request?
22
+ assert 2, resp.item_page
23
+ end
24
+
25
+ def test_item_search_with_invalid_request
26
+ resp = AmazonAssociate::Request.item_search(nil)
27
+ assert !resp.is_valid_request?
28
+ end
29
+
30
+ def test_item_search_with_no_result
31
+ resp = AmazonAssociate::Request.item_search("afdsafds")
32
+
33
+ assert resp.is_valid_request?
34
+ assert_equal "We did not find any matches for your request.",
35
+ resp.error
36
+ end
37
+
38
+ def test_item_search_uk
39
+ resp = AmazonAssociate::Request.item_search("ruby", :country => :uk)
40
+ assert resp.is_valid_request?
41
+ end
42
+
43
+ def test_item_search_by_author
44
+ resp = AmazonAssociate::Request.item_search("dave", :type => :author)
45
+ assert resp.is_valid_request?
46
+ end
47
+
48
+ def test_item_get
49
+ resp = AmazonAssociate::Request.item_search("0974514055")
50
+ item = resp.first_item
51
+
52
+ # test get
53
+ assert_equal "Programming Ruby: The Pragmatic Programmers' Guide, Second Edition",
54
+ item.get("itemattributes/title")
55
+
56
+ # test get_array
57
+ assert_equal ["Dave Thomas", "Chad Fowler", "Andy Hunt"],
58
+ item.get_array("author")
59
+
60
+ # test get_hash
61
+ small_image = item.get_hash("smallimage")
62
+
63
+ assert_equal 3, small_image.keys.size
64
+ assert small_image[:url] != nil
65
+ assert_equal "75", small_image[:height]
66
+ assert_equal "59", small_image[:width]
67
+
68
+ # test /
69
+ reviews = item/"editorialreview"
70
+ reviews.each do |review|
71
+ # returns unescaped HTML content, Hpricot escapes all text values
72
+ assert AmazonAssociate::Element.get_unescaped(review, "source")
73
+ assert AmazonAssociate::Element.get_unescaped(review, "content")
74
+ end
75
+ end
76
+
77
+ ## Test item_lookup
78
+ def test_item_lookup
79
+ resp = AmazonAssociate::Request.item_lookup("0974514055")
80
+ assert_equal "Programming Ruby: The Pragmatic Programmers' Guide, Second Edition",
81
+ resp.first_item.get("itemattributes/title")
82
+ end
83
+
84
+ def test_item_lookup_with_invalid_request
85
+ resp = AmazonAssociate::Request.item_lookup(nil)
86
+ assert resp.has_error?
87
+ assert resp.error
88
+ end
89
+
90
+ def test_item_lookup_with_no_result
91
+ resp = AmazonAssociate::Request.item_lookup("abc")
92
+
93
+ assert resp.is_valid_request?
94
+ assert_match(/ABC is not a valid value for ItemId/, resp.error)
95
+ end
96
+
97
+ def test_search_and_convert
98
+ resp = AmazonAssociate::Request.item_lookup("0974514055")
99
+ title = resp.first_item.get("itemattributes/title")
100
+ authors = resp.first_item.search_and_convert("author")
101
+
102
+ assert_equal "Programming Ruby: The Pragmatic Programmers' Guide, Second Edition", title
103
+ assert authors.is_a?(Array)
104
+ assert 3, authors.size
105
+ assert_equal "Dave Thomas", authors.first.get
106
+ end
107
+
108
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dpickett-amazon_associate
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.1
5
+ platform: ruby
6
+ authors:
7
+ - Dan Pickett
8
+ - Herryanto Siatono
9
+ autorequire: amazon_associate
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2008-11-10 00:00:00 -08:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: hpricot
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0.6"
24
+ version:
25
+ description:
26
+ email:
27
+ - dpickett@enlightsolutions.com
28
+ - herryanto@pluitsolutions.com
29
+ executables: []
30
+
31
+ extensions: []
32
+
33
+ extra_rdoc_files:
34
+ - README
35
+ - CHANGELOG
36
+ files:
37
+ - lib/amazon_associate
38
+ - lib/amazon_associate/cache_factory.rb
39
+ - lib/amazon_associate/caching_strategy
40
+ - lib/amazon_associate/caching_strategy/base.rb
41
+ - lib/amazon_associate/caching_strategy/filesystem.rb
42
+ - lib/amazon_associate/caching_strategy.rb
43
+ - lib/amazon_associate/configuration_error.rb
44
+ - lib/amazon_associate/element.rb
45
+ - lib/amazon_associate/request.rb
46
+ - lib/amazon_associate/request_error.rb
47
+ - lib/amazon_associate/response.rb
48
+ - lib/amazon_associate.rb
49
+ - test/amazon_associate/browse_node_lookup_test.rb
50
+ - test/amazon_associate/cache_test.rb
51
+ - test/amazon_associate/caching_strategy/filesystem_test.rb
52
+ - test/amazon_associate/cart_test.rb
53
+ - test/amazon_associate/request_test.rb
54
+ - README
55
+ - CHANGELOG
56
+ has_rdoc: true
57
+ homepage: http://github.com/dpickett/amazon_associate/tree/master
58
+ post_install_message:
59
+ rdoc_options:
60
+ - --line-numbers
61
+ - --inline-source
62
+ - --main
63
+ - README
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: "0"
77
+ version:
78
+ requirements: []
79
+
80
+ rubyforge_project:
81
+ rubygems_version: 1.2.0
82
+ signing_key:
83
+ specification_version: 2
84
+ summary: Generic Amazon Associates Web Service (Formerly ECS) REST API. Supports ECS 4.0.
85
+ test_files:
86
+ - test/amazon_associate/browse_node_lookup_test.rb
87
+ - test/amazon_associate/cache_test.rb
88
+ - test/amazon_associate/caching_strategy/filesystem_test.rb
89
+ - test/amazon_associate/cart_test.rb
90
+ - test/amazon_associate/request_test.rb