dpickett-amazon_associate 0.6.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/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