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.
- data/.gitignore +3 -0
- data/CHANGELOG +29 -0
- data/LICENSE +21 -0
- data/README.rdoc +84 -0
- data/Rakefile +62 -0
- data/VERSION.yml +5 -0
- data/amazon-associates.gemspec +118 -0
- data/lib/amazon-associates.rb +90 -0
- data/lib/amazon-associates/caching/filesystem_cache.rb +121 -0
- data/lib/amazon-associates/errors.rb +23 -0
- data/lib/amazon-associates/extensions/core.rb +31 -0
- data/lib/amazon-associates/extensions/hpricot.rb +115 -0
- data/lib/amazon-associates/request.rb +143 -0
- data/lib/amazon-associates/requests/browse_node.rb +10 -0
- data/lib/amazon-associates/requests/cart.rb +81 -0
- data/lib/amazon-associates/requests/item.rb +13 -0
- data/lib/amazon-associates/responses/browse_node_lookup_response.rb +10 -0
- data/lib/amazon-associates/responses/cart_responses.rb +26 -0
- data/lib/amazon-associates/responses/item_lookup_response.rb +16 -0
- data/lib/amazon-associates/responses/item_search_response.rb +20 -0
- data/lib/amazon-associates/responses/response.rb +27 -0
- data/lib/amazon-associates/responses/similarity_lookup_response.rb +9 -0
- data/lib/amazon-associates/types/api_result.rb +8 -0
- data/lib/amazon-associates/types/browse_node.rb +48 -0
- data/lib/amazon-associates/types/cart.rb +87 -0
- data/lib/amazon-associates/types/customer_review.rb +15 -0
- data/lib/amazon-associates/types/editorial_review.rb +8 -0
- data/lib/amazon-associates/types/error.rb +8 -0
- data/lib/amazon-associates/types/image.rb +37 -0
- data/lib/amazon-associates/types/image_set.rb +11 -0
- data/lib/amazon-associates/types/item.rb +156 -0
- data/lib/amazon-associates/types/listmania_list.rb +9 -0
- data/lib/amazon-associates/types/measurement.rb +47 -0
- data/lib/amazon-associates/types/offer.rb +10 -0
- data/lib/amazon-associates/types/ordinal.rb +24 -0
- data/lib/amazon-associates/types/price.rb +29 -0
- data/lib/amazon-associates/types/requests.rb +50 -0
- data/spec/requests/browse_node_lookup_spec.rb +41 -0
- data/spec/requests/item_search_spec.rb +27 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/types/cart_spec.rb +294 -0
- data/spec/types/item_spec.rb +55 -0
- data/spec/types/measurement_spec.rb +43 -0
- data/test/amazon/browse_node_test.rb +34 -0
- data/test/amazon/cache_test.rb +33 -0
- data/test/amazon/caching/filesystem_cache_test.rb +198 -0
- data/test/amazon/item_test.rb +397 -0
- data/test/test_helper.rb +9 -0
- data/test/utilities/filesystem_test_helper.rb +35 -0
- 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,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,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
|