amazon-associates 0.6.3

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.
Files changed (50) hide show
  1. data/.gitignore +3 -0
  2. data/CHANGELOG +29 -0
  3. data/LICENSE +21 -0
  4. data/README.rdoc +84 -0
  5. data/Rakefile +62 -0
  6. data/VERSION.yml +5 -0
  7. data/amazon-associates.gemspec +118 -0
  8. data/lib/amazon-associates.rb +90 -0
  9. data/lib/amazon-associates/caching/filesystem_cache.rb +121 -0
  10. data/lib/amazon-associates/errors.rb +23 -0
  11. data/lib/amazon-associates/extensions/core.rb +31 -0
  12. data/lib/amazon-associates/extensions/hpricot.rb +115 -0
  13. data/lib/amazon-associates/request.rb +143 -0
  14. data/lib/amazon-associates/requests/browse_node.rb +10 -0
  15. data/lib/amazon-associates/requests/cart.rb +81 -0
  16. data/lib/amazon-associates/requests/item.rb +13 -0
  17. data/lib/amazon-associates/responses/browse_node_lookup_response.rb +10 -0
  18. data/lib/amazon-associates/responses/cart_responses.rb +26 -0
  19. data/lib/amazon-associates/responses/item_lookup_response.rb +16 -0
  20. data/lib/amazon-associates/responses/item_search_response.rb +20 -0
  21. data/lib/amazon-associates/responses/response.rb +27 -0
  22. data/lib/amazon-associates/responses/similarity_lookup_response.rb +9 -0
  23. data/lib/amazon-associates/types/api_result.rb +8 -0
  24. data/lib/amazon-associates/types/browse_node.rb +48 -0
  25. data/lib/amazon-associates/types/cart.rb +87 -0
  26. data/lib/amazon-associates/types/customer_review.rb +15 -0
  27. data/lib/amazon-associates/types/editorial_review.rb +8 -0
  28. data/lib/amazon-associates/types/error.rb +8 -0
  29. data/lib/amazon-associates/types/image.rb +37 -0
  30. data/lib/amazon-associates/types/image_set.rb +11 -0
  31. data/lib/amazon-associates/types/item.rb +156 -0
  32. data/lib/amazon-associates/types/listmania_list.rb +9 -0
  33. data/lib/amazon-associates/types/measurement.rb +47 -0
  34. data/lib/amazon-associates/types/offer.rb +10 -0
  35. data/lib/amazon-associates/types/ordinal.rb +24 -0
  36. data/lib/amazon-associates/types/price.rb +29 -0
  37. data/lib/amazon-associates/types/requests.rb +50 -0
  38. data/spec/requests/browse_node_lookup_spec.rb +41 -0
  39. data/spec/requests/item_search_spec.rb +27 -0
  40. data/spec/spec_helper.rb +8 -0
  41. data/spec/types/cart_spec.rb +294 -0
  42. data/spec/types/item_spec.rb +55 -0
  43. data/spec/types/measurement_spec.rb +43 -0
  44. data/test/amazon/browse_node_test.rb +34 -0
  45. data/test/amazon/cache_test.rb +33 -0
  46. data/test/amazon/caching/filesystem_cache_test.rb +198 -0
  47. data/test/amazon/item_test.rb +397 -0
  48. data/test/test_helper.rb +9 -0
  49. data/test/utilities/filesystem_test_helper.rb +35 -0
  50. metadata +216 -0
@@ -0,0 +1,16 @@
1
+ module Amazon
2
+ module Associates
3
+ class ItemLookupResponse < Response
4
+ xml_name 'ItemLookupResponse'
5
+ xml_reader :items, :as => [Item]
6
+ xml_reader :current_page, :from => 'xmlns:ItemPage', :as => Integer, :else => 1
7
+ xml_reader :request, :as => Request, :in => 'xmlns:Items', :required => true
8
+ xml_reader :request_query, :as => ItemLookupRequest, :from => 'xmlns:SimilarityLookupRequest', :in => 'xmlns:Items/xmlns:Request'
9
+
10
+ def item
11
+ raise IndexError, "more than one item" if items.size > 1
12
+ items.first
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ module Amazon
2
+ module Associates
3
+ class SearchResponse < Response
4
+ attr_accessor :url # TODO would be just a reader if we can figure out xml construction better
5
+ xml_reader :operation_request, :as => OperationRequest, :required => true
6
+ xml_reader :request, :as => Request, :in => 'xmlns:Items'
7
+ delegate :current_page, :to => :request_query
8
+
9
+ xml_reader :items, :as => [Item]
10
+ xml_reader :total_results, :in => 'xmlns:Items', :as => Integer
11
+ xml_reader :total_pages, :in => 'xmlns:Items', :as => Integer
12
+ end
13
+
14
+ class ItemSearchResponse < SearchResponse
15
+ xml_name 'ItemSearchResponse'
16
+
17
+ xml_reader :request_query, :as => ItemSearchRequest, :in => 'xmlns:Items/xmlns:Request', :required => true
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ module Amazon
2
+ module Associates
3
+ class Response < ApiResult
4
+ attr_reader :url
5
+
6
+ def errors
7
+ request.errors
8
+ end
9
+
10
+ def initialize(url)
11
+ @url = url
12
+ end
13
+
14
+ def ==(other)
15
+ (instance_variables.sort == other.instance_variables.sort) && instance_variables.all? do |v|
16
+ instance_variable_get(v) == other.instance_variable_get(v)
17
+ end
18
+ end
19
+
20
+ private
21
+ def after_parse
22
+ # these can't be done as blocks because we need @url available
23
+ raise errors.first unless errors.empty?
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ module Amazon
2
+ module Associates
3
+ class SimilarityLookupResponse < SearchResponse
4
+ xml_name 'SimilarityLookupResponse'
5
+
6
+ # xml_reader :request_query, :as => SimilarityLookupRequest, :in => 'Items/Request', :required => true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ module Amazon
2
+ module Associates
3
+ class ApiResult
4
+ include ROXML
5
+ xml_convention :camelcase
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,48 @@
1
+ module Amazon
2
+ module Associates
3
+ class Item < ApiResult
4
+ # forward-declaration...
5
+ end
6
+
7
+ class BrowseNode < ApiResult
8
+ xml_reader :id, :from => 'xmlns:BrowseNodeId', :required => true
9
+ xml_reader :name, :from => 'Name'
10
+ xml_reader :parent, :as => BrowseNode, :from => 'xmlns:BrowseNode', :in => 'xmlns:Ancestors'
11
+ xml_reader :children, :as => [BrowseNode]
12
+ xml_reader :top_sellers, :as => [Item]
13
+
14
+ def initialize(id = nil, name = nil, parent = nil)
15
+ @id = id
16
+ @name = name
17
+ @parent = parent
18
+ end
19
+
20
+ def to_s
21
+ "#{@name}#{' : ' + @parent.to_s if @parent}"
22
+ end
23
+
24
+ def inspect
25
+ "#<#{self.class}:#{@id} #{self}>"
26
+ end
27
+
28
+ def ==(other)
29
+ return false unless other.respond_to?(:name, :id)
30
+ @name == other.name and @id == other.id
31
+ end
32
+
33
+ {:brand => [:manufacturers, :custom_brands], :type => [:categories]}.each_pair do |name, aliases|
34
+ define_method("#{name}?") do
35
+ markers = ([name] + aliases).map {|n| n.to_s.titleize}
36
+ return true if markers.include? instance_variable_get(:@name)
37
+
38
+ parent = instance_variable_get :@parent
39
+ while parent
40
+ return true if markers.include? parent.name
41
+ parent = parent.parent
42
+ end
43
+ false
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,87 @@
1
+ require 'roxml'
2
+
3
+ module Amazon
4
+ module Associates
5
+ class Cart < ApiResult
6
+ xml_reader :items, :as => [CartItem], :from => 'xmlns:CartItem', :in => 'xmlns:CartItems', :frozen => true
7
+ xml_reader :purchase_url, :from => 'xmlns:PurchaseURL'
8
+ xml_reader :id, :from => 'xmlns:CartId', :required => true
9
+ xml_reader :hmac, :from => 'xmlns:HMAC', :required => true
10
+
11
+ delegate :empty?, :include?, :to => :items
12
+
13
+ def to_hash
14
+ {:id => id,
15
+ :hmac => hmac}
16
+ end
17
+
18
+ # CartCreate
19
+ # defaults:
20
+ # response_group: Cart
21
+ # merge_cart: false
22
+ # list_item_id: nil
23
+ def self.create(items, args = nil)
24
+ if items.is_a?(Array)
25
+ items = items.inject({}) do |all, (item, count)|
26
+ all[item] = count || 1
27
+ all
28
+ end
29
+ end
30
+ Amazon::Associates.cart_create(items, args).cart
31
+ end
32
+
33
+ # CartGet
34
+ def self.get(args)
35
+ Amazon::Associates.cart_get(args).cart
36
+ end
37
+
38
+ def save
39
+ @changes.each do |action, *args|
40
+ cart = Amazon::Associates.send(action, *args).cart
41
+ @items = cart.items
42
+ @id = cart.id
43
+ @hmac = cart.hmac
44
+ end
45
+ @changes.clear
46
+ self
47
+ end
48
+
49
+ def changed?
50
+ !@changes.empty?
51
+ end
52
+
53
+ def add(item, count = 1)
54
+ raise ArgumentError, "item is nil" if item.nil?
55
+ raise ArgumentError, "count isn't positive" if count <= 0
56
+
57
+ if @items.include? item
58
+ action = :cart_modify
59
+ item = @items.find {|i| i == item } # we need the CartItemId for CartModify
60
+ count += item.quantity
61
+ else
62
+ action = :cart_add
63
+ end
64
+ # TODO: This could be much more sophisticated, collapsing operations and such
65
+ @changes << [action, self, {:items => {item => count}}]
66
+ end
67
+ alias_method :<<, :add
68
+
69
+ def clear
70
+ @changes << [:cart_clear, self]
71
+ end
72
+
73
+ def quantity
74
+ items.sum(&:quantity)
75
+ end
76
+
77
+ def ==(other)
78
+ id == other.id
79
+ end
80
+
81
+ private
82
+ def initialize
83
+ @changes = []
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,15 @@
1
+ module Amazon
2
+ module Associates
3
+ class CustomerReview < ApiResult
4
+ xml_name 'Review'
5
+ xml_reader :asin, :from => 'ASIN'
6
+ xml_reader :rating, :as => Integer
7
+ xml_reader :helpful_votes, :as => Integer
8
+ xml_reader :total_votes, :as => Integer
9
+ xml_reader :summary
10
+ xml_reader :content
11
+ xml_reader :customer_id
12
+ xml_reader :date, :as => Date
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ module Amazon
2
+ module Associates
3
+ class EditorialReview < ApiResult
4
+ xml_reader :source
5
+ xml_reader :content
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module Amazon
2
+ module Associates
3
+ class Error < ApiResult
4
+ xml_reader :code
5
+ xml_reader :message
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,37 @@
1
+ module Amazon
2
+ module Associates
3
+ class Image < ApiResult
4
+ xml_reader :url, :from => 'URL'
5
+ xml_reader :width, :as => Measurement
6
+ xml_reader :height, :as => Measurement
7
+
8
+ def initialize(url = nil, width = nil, height = nil)
9
+ @url = url
10
+ @width = to_measurement(width)
11
+ @height = to_measurement(height)
12
+ end
13
+
14
+ def ==(other)
15
+ return nil unless other.is_a? Image
16
+ url == other.url and width == other.width and height == other.height
17
+ end
18
+
19
+ def size
20
+ unless height.units == 'pixels' and width.units == 'pixels'
21
+ raise 'size not available for images not denominated in pixels'
22
+ end
23
+
24
+ "#{width.value.round}x#{height.value.round}"
25
+ end
26
+
27
+ def inspect
28
+ "#<#{self.class}: #{url},#{width}x#{height}>"
29
+ end
30
+
31
+ private
32
+ def to_measurement(arg)
33
+ arg && (arg.is_a?(Measurement) ? arg : Measurement.new(arg, 'pixels'))
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,11 @@
1
+ module Amazon
2
+ module Associates
3
+ class ImageSet < ApiResult
4
+ xml_reader :category, :from => :attr, :required => true
5
+ xml_reader :small, :as => Image, :from => 'SmallImage'
6
+ xml_reader :medium, :as => Image, :from => 'MediumImage'
7
+ xml_reader :large, :as => Image, :from => 'LargeImage'
8
+ xml_reader :swatch, :as => Image, :from => 'SwatchImage'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,156 @@
1
+ require 'will_paginate/collection'
2
+
3
+ module Amazon
4
+ module Associates
5
+ class Item < ApiResult
6
+ xml_name 'Item'
7
+
8
+ xml_reader :asin, :from => 'ASIN'
9
+ xml_reader :detail_page_url
10
+ xml_reader :list_price, :as => Price, :in => 'xmlns:ItemAttributes'
11
+ xml_reader :attributes, :as => {:key => :name,
12
+ :value => :content}, :in => 'xmlns:ItemAttributes'
13
+ xml_reader :small_image, :as => Image
14
+ xml_reader :medium_image, :as => Image
15
+ #TODO: would be nice to have :key => '@category' and :value => {[Image] => 'SwatchImage'}
16
+ xml_reader :image_sets, :as => [ImageSet] do |sets|
17
+ sets.index_by(&:category)
18
+ end
19
+ xml_reader :listmania_lists, :as => [ListmaniaList]
20
+ xml_reader :browse_nodes, :as => [BrowseNode]
21
+ xml_reader :offers, :as => [Offer]
22
+ # TODO: This should be offers.total_new
23
+ xml_reader :total_new_offers, :from => 'TotalNew', :in => 'xmlns:OfferSummary', :as => Integer
24
+ # TODO: This should be offers.total
25
+ xml_reader :total_offers, :in => 'xmlns:Offers', :as => Integer
26
+
27
+ xml_reader :creators, :as => {:key => '@Role', :value => :content}, :in => 'xmlns:ItemAttributes'
28
+ xml_reader :authors, :as => [], :in => 'xmlns:ItemAttributes'
29
+ xml_reader :edition, :as => Ordinal, :in => 'xmlns:ItemAttributes'
30
+ xml_reader :lowest_new_price, :as => Price, :in => 'xmlns:OfferSummary'
31
+ xml_reader :publisher, :studio, :batteries_included?, :label, :brand, :in => 'xmlns:ItemAttributes'
32
+
33
+ xml_reader :editorial_reviews, :as => [EditorialReview]
34
+ xml_reader :customer_reviews, :as => [CustomerReview]
35
+
36
+ def ==(other)
37
+ asin == other.asin
38
+ end
39
+
40
+ def eql?(other)
41
+ asin == other.asin
42
+ end
43
+
44
+ def author
45
+ authors.only
46
+ end
47
+
48
+ alias_method :offer_price, :lowest_new_price
49
+
50
+ def title
51
+ root_title || attr_title
52
+ end
53
+
54
+ def inspect
55
+ "#<#{self.class}: #{asin} #{attributes.inspect}>"
56
+ end
57
+
58
+ PER_PAGE = 10
59
+
60
+ def self.find(scope, opts = {})
61
+ opts = opts.dup # it seems this hash picks up some internal amazon stuff if we don't copy it
62
+
63
+ if scope.is_a? String
64
+ opts.merge!(:item_id => scope, :item_type => 'ASIN')
65
+ scope = :one
66
+ end
67
+
68
+ case scope
69
+ when :top_sellers then top_sellers(opts)
70
+ when :similar then similar(opts)
71
+ when :all then all(opts)
72
+ when :first then first(opts)
73
+ when :one then one(opts)
74
+ else
75
+ raise ArgumentError, "scope should be :all, :first, :one, or an item id string"
76
+ end
77
+ end
78
+
79
+ def self.top_sellers(opts)
80
+ opts = opts.dup.to_options!
81
+ opts.merge!(:response_group => 'TopSellers')
82
+ opts[:browse_node_id] = opts.delete(:browse_node).id if opts[:browse_node]
83
+ items = Amazon::Associates.browse_node_lookup(opts).top_sellers.map {|i| i.text_at('asin') }
84
+ Amazon::Associates.item_lookup(:item_id => items * ',', :response_group => SMALL_RESPONSE_GROUPS).items
85
+ end
86
+
87
+ def self.similar(*ids)
88
+ opts = ids.extract_options!
89
+ opts.reverse_merge!(:response_group => SMALL_RESPONSE_GROUPS)
90
+ Amazon::Associates.similarity_lookup(ids, opts).items
91
+ end
92
+
93
+ def self.top_sellers(opts)
94
+ opts = opts.dup.to_options!
95
+ opts.merge!(:response_group => 'TopSellers')
96
+ opts[:browse_node_id] = opts.delete(:browse_node).id if opts[:browse_node]
97
+ items = Amazon::Associates.browse_node_lookup(opts).browse_nodes.map(&:top_sellers).flatten.map(&:asin)
98
+ Amazon::Associates.item_lookup(:item_id => items * ',', :response_group => SMALL_RESPONSE_GROUPS).items
99
+ end
100
+
101
+ def self.first(opts)
102
+ all(opts).first
103
+ end
104
+
105
+ def self.all(opts)
106
+ opts = opts.dup.to_options!
107
+ unless %w[All Blended Merchants].include? opts[:search_index]
108
+ opts.reverse_merge!(:merchant_id => 'Amazon',
109
+ :condition => 'All')
110
+ end
111
+ opts[:availability] ||= 'Available' unless opts[:condition].nil? or opts[:condition] == 'New'
112
+ opts[:item_page] ||= (opts.delete(:page) || 1)
113
+ prep_responses(opts)
114
+
115
+ response = Amazon::Associates.item_search(opts)
116
+
117
+ # TODO: Max count is different for different indexes, for example, All only returns 5 pages
118
+ WillPaginate::Collection.create(response.current_page, PER_PAGE, response.total_results) do |pager|
119
+ # TODO: Some of the returned items may not include offers, we may need something like this:
120
+ #.reject {|i| i.offers[:totaloffers] == '0' }
121
+ pager.replace response.items
122
+ end
123
+ end
124
+
125
+ def self.one(opts)
126
+ prep_responses(opts)
127
+ Amazon::Associates.item_lookup(opts.delete(:item_id), opts).items.first
128
+ end
129
+
130
+ def similar
131
+ Item.similar(asin)
132
+ end
133
+
134
+ private
135
+ xml_reader :attr_title, :from => 'xmlns:Title', :in => 'xmlns:ItemAttributes'
136
+ xml_reader :root_title, :from => 'xmlns:Title'
137
+
138
+ SMALL_RESPONSE_GROUPS = %w{Small ItemAttributes Images}
139
+ DEFAULT_RESPONSE_GROUPS = SMALL_RESPONSE_GROUPS + %w{Offers VariationSummary BrowseNodes}
140
+
141
+ def self.prep_responses(opts)
142
+ opts[:response_group] ||= []
143
+ unless opts[:response_group].is_a? Array
144
+ raise ArgumentError, "Response groups are required to be in array form"
145
+ end
146
+ opts[:response_group] += DEFAULT_RESPONSE_GROUPS
147
+ end
148
+ end
149
+
150
+ class CartItem < Item
151
+ # TODO: This could probably just be #id
152
+ xml_reader :cart_item_id
153
+ xml_reader :quantity, :as => Integer, :required => true
154
+ end
155
+ end
156
+ end