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,23 @@
|
|
1
|
+
module Amazon
|
2
|
+
module Associates
|
3
|
+
class RequestError < StandardError; end
|
4
|
+
class InvalidParameterValue < ArgumentError; end
|
5
|
+
class ParameterOutOfRange < InvalidParameterValue; end
|
6
|
+
class RequiredParameterMissing < ArgumentError; end
|
7
|
+
class ItemNotFound < StandardError; end
|
8
|
+
|
9
|
+
# Map AWS error types to ruby exceptions
|
10
|
+
ERROR = {
|
11
|
+
'AWS.InvalidParameterValue' => InvalidParameterValue,
|
12
|
+
'AWS.MissingParameters' => RequiredParameterMissing,
|
13
|
+
'AWS.MinimumParameterRequirement' => RequiredParameterMissing,
|
14
|
+
'AWS.ECommerceService.NoExactMatches' => ItemNotFound,
|
15
|
+
'AWS.ParameterOutOfRange' => ParameterOutOfRange,
|
16
|
+
'AWS.InvalidOperationParameter'=> InvalidParameterValue,
|
17
|
+
'AWS.InvalidResponseGroup' => InvalidParameterValue,
|
18
|
+
'AWS.RestrictedParameterValueCombination' => InvalidParameterValue
|
19
|
+
}
|
20
|
+
|
21
|
+
IGNORE_ERRORS = ['AWS.ECommerceService.NoSimilarities']
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'active_support'
|
3
|
+
|
4
|
+
class Object # http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html
|
5
|
+
def meta_def name, &blk
|
6
|
+
(class << self; self; end).instance_eval { define_method name, &blk }
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class OpenHash < Hash
|
11
|
+
def method_missing_with_attributes_query(meth, *args)
|
12
|
+
fetch(meth) do
|
13
|
+
method_missing_without_attributes_query(meth)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
alias_method_chain :method_missing, :attributes_query
|
17
|
+
end
|
18
|
+
|
19
|
+
class Float
|
20
|
+
def whole?
|
21
|
+
(self % 1) < 0.0001
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Hash
|
26
|
+
def rekey!(keys)
|
27
|
+
keys.each_pair do |old, new|
|
28
|
+
store(new, delete(old)) if has_key?(old)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'active_support'
|
3
|
+
require 'roxml'
|
4
|
+
|
5
|
+
# TODO: Untange this inter-dependency...
|
6
|
+
%w{ browse_node measurement image ordinal price }.each do |type|
|
7
|
+
require File.join(File.dirname(__FILE__), '..', 'types', type)
|
8
|
+
end
|
9
|
+
|
10
|
+
module Hpricot
|
11
|
+
class Element < OpenHash
|
12
|
+
def initialize(value, attributes = {})
|
13
|
+
merge! :value => value,
|
14
|
+
:attributes => attributes
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Extend with some convenience methods
|
19
|
+
module Traverse
|
20
|
+
def self.induce(type, &block)
|
21
|
+
raise ArgumentError, "block missing" unless block_given?
|
22
|
+
|
23
|
+
type_at, to_type, types_at = "#{type}_at", "to_#{type}", "#{type.to_s.pluralize}_at"
|
24
|
+
if [type_at, to_type, types_at].any? {|m| method_defined?(m) }
|
25
|
+
raise ArgumentError, "some methods already defined"
|
26
|
+
end
|
27
|
+
|
28
|
+
define_method type_at do |path|
|
29
|
+
result = at(path) and yield result
|
30
|
+
end
|
31
|
+
define_method to_type do
|
32
|
+
method(type_at).call('')
|
33
|
+
end
|
34
|
+
define_method types_at do |path|
|
35
|
+
results = search(path) and results.collect {|r| yield r }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get the text value of the given path, leave empty to retrieve current element value.
|
40
|
+
induce :text do |result|
|
41
|
+
CGI::unescapeHTML(result.inner_html)
|
42
|
+
end
|
43
|
+
|
44
|
+
induce :int do |result|
|
45
|
+
Integer(result.inner_html)
|
46
|
+
end
|
47
|
+
|
48
|
+
induce :bool do |result|
|
49
|
+
case result.inner_html
|
50
|
+
when '0': false
|
51
|
+
when 'False': false
|
52
|
+
when '1': true
|
53
|
+
when 'True': true
|
54
|
+
else
|
55
|
+
raise TypeError, "String #{result.inspect} is not convertible to bool"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
induce :element do |result|
|
60
|
+
# TODO: Use to_h here?
|
61
|
+
attrs = result.attributes.inject({}) do |hash, attr|
|
62
|
+
hash[attr[0].to_sym] = attr[1].to_s; hash
|
63
|
+
end
|
64
|
+
|
65
|
+
children = result.children
|
66
|
+
if children.size == 1 and children.first.is_a? Text
|
67
|
+
value = children.first.to_s
|
68
|
+
else
|
69
|
+
result = children.inject({}) do |hash, item|
|
70
|
+
name = item.name.to_sym
|
71
|
+
hash[name] ||= []
|
72
|
+
hash[name] << item.to_hash
|
73
|
+
hash
|
74
|
+
end
|
75
|
+
|
76
|
+
value = result.each_pair {|key, value| result[key] = value[0] if value.size == 1 }
|
77
|
+
end
|
78
|
+
|
79
|
+
(attrs.empty?) ? value : Element.new(value, attrs)
|
80
|
+
end
|
81
|
+
|
82
|
+
# TODO: This probably doesn't belong here... References to Amazon:: types indicate as much anyway
|
83
|
+
# Get the children element text values in hash format with the element names as the hash keys.
|
84
|
+
induce :hash do |result|
|
85
|
+
# TODO: date?, image? &c
|
86
|
+
# TODO: This is super-ugly... is there a better way to map?
|
87
|
+
if ['width', 'height', 'length', 'weight'].include? result.name
|
88
|
+
Amazon::Measurement.from_xml(result.to_s)
|
89
|
+
elsif ['batteriesincluded', 'iseligibleforsupersavershipping', 'isautographed', 'ismemorabilia', 'isvalid'].include? result.name
|
90
|
+
result.to_bool
|
91
|
+
elsif result.name == 'browsenode'
|
92
|
+
Amazon::BrowseNode.from_xml(result.to_s)
|
93
|
+
elsif result.name == 'edition'
|
94
|
+
begin
|
95
|
+
Amazon::Ordinal.from_xml(result.to_s)
|
96
|
+
rescue TypeError
|
97
|
+
# a few edition types aren't ordinals, but strings (e.g., "First American Edition")
|
98
|
+
result.to_text
|
99
|
+
end
|
100
|
+
elsif result.name.starts_with? 'total' or result.name.starts_with? 'number' or ['quantity'].include? result.name
|
101
|
+
result.to_int
|
102
|
+
elsif result.name.ends_with? 'price' or result.name.ends_with? 'total'
|
103
|
+
Amazon::Price.from_xml(result.to_s)
|
104
|
+
elsif result.name.ends_with? 'image'
|
105
|
+
Amazon::Image.from_xml(result.to_s)
|
106
|
+
else
|
107
|
+
if (result.children.size > 1 or !result.children.first.is_a? Text) and names = result.children.collect {|c| c.name }.uniq and names.size == 1 and names[0].pluralize == result.name
|
108
|
+
result.children.map(&:to_hash)
|
109
|
+
else
|
110
|
+
result.to_element
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
%w{errors extensions/core types/api_result
|
2
|
+
types/error types/customer_review types/editorial_review types/ordinal types/listmania_list types/browse_node types/measurement types/image types/image_set types/price types/offer types/item types/requests types/cart
|
3
|
+
responses/response responses/item_search_response responses/item_lookup_response responses/similarity_lookup_response responses/browse_node_lookup_response responses/cart_responses }.each do |file|
|
4
|
+
require File.join(File.dirname(__FILE__), file)
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'net/http'
|
8
|
+
require 'cgi'
|
9
|
+
require 'hmac'
|
10
|
+
require 'hmac-sha2'
|
11
|
+
require 'base64'
|
12
|
+
|
13
|
+
module Amazon
|
14
|
+
module Associates
|
15
|
+
def self.request(actions, &block)
|
16
|
+
actions.each_pair do |action, main_arg|
|
17
|
+
meta_def(action) do |*args|
|
18
|
+
opts = args.extract_options!
|
19
|
+
opts[main_arg] = args.first unless args.empty?
|
20
|
+
opts[:operation] = action.to_s.camelize
|
21
|
+
|
22
|
+
opts = yield opts if block_given?
|
23
|
+
send_request(opts)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
# Generic send request to ECS REST service. You have to specify the :operation parameter.
|
30
|
+
def self.send_request(opts)
|
31
|
+
opts.to_options!
|
32
|
+
opts.reverse_merge! options.except(:caching_options, :caching_strategy)
|
33
|
+
if opts[:aWS_access_key_id].blank?
|
34
|
+
raise ArgumentError, "amazon-associates requires the :aws_access_key_id option"
|
35
|
+
end
|
36
|
+
|
37
|
+
request_url = prepare_url(opts)
|
38
|
+
response = nil
|
39
|
+
|
40
|
+
if cacheable?(opts['Operation'])
|
41
|
+
FilesystemCache.sweep
|
42
|
+
|
43
|
+
response = FilesystemCache.get(request_url)
|
44
|
+
end
|
45
|
+
|
46
|
+
if response.nil?
|
47
|
+
log "Request URL: #{request_url}"
|
48
|
+
|
49
|
+
response = Net::HTTP.get_response(URI::parse(request_url))
|
50
|
+
unless response.kind_of? Net::HTTPSuccess
|
51
|
+
raise RequestError, "HTTP Response: #{response.inspect}"
|
52
|
+
end
|
53
|
+
cache_response(request_url, response) if cacheable?(opts['Operation'])
|
54
|
+
end
|
55
|
+
|
56
|
+
doc = ROXML::XML::Parser.parse(response.body).root
|
57
|
+
eval(doc.name).from_xml(doc, request_url)
|
58
|
+
end
|
59
|
+
|
60
|
+
BASE_ARGS = [:aWS_access_key_id, :operation, :associate_tag, :response_group]
|
61
|
+
CART_ARGS = [:cart_id, :hMAC]
|
62
|
+
ITEM_ARGS = (0..99).inject([:items]) do |all, i|
|
63
|
+
all << :"Item.#{i}.ASIN"
|
64
|
+
all << :"Item.#{i}.OfferListingId"
|
65
|
+
all << :"Item.#{i}.CartItemId"
|
66
|
+
all << :"Item.#{i}.Quantity"
|
67
|
+
all
|
68
|
+
end
|
69
|
+
OTHER_ARGS = [
|
70
|
+
:item_page, :item_id, :country, :type, :item_type,
|
71
|
+
:browse_node_id, :actor, :artist, :audience_rating, :author,
|
72
|
+
:availability, :brand, :browse_node, :city, :composer,
|
73
|
+
:condition, :conductor, :director, :page, :keywords,
|
74
|
+
:manufacturer, :maximum_price, :merchant_id,
|
75
|
+
:minimum_price, :neighborhood, :orchestra,
|
76
|
+
:postal_code, :power, :publisher, :search_index, :sort,
|
77
|
+
:tag_page, :tags_per_page, :tag_sort, :text_stream,
|
78
|
+
:title, :variation_page
|
79
|
+
]
|
80
|
+
VALID_ARGS = {
|
81
|
+
'CartCreate' => ITEM_ARGS,
|
82
|
+
'CartAdd' => ITEM_ARGS + CART_ARGS,
|
83
|
+
'CartModify' => ITEM_ARGS + CART_ARGS,
|
84
|
+
'CartGet' => CART_ARGS,
|
85
|
+
'CartClear' => CART_ARGS
|
86
|
+
}
|
87
|
+
|
88
|
+
def self.valid_arguments(operation)
|
89
|
+
BASE_ARGS + VALID_ARGS.fetch(operation, OTHER_ARGS)
|
90
|
+
end
|
91
|
+
|
92
|
+
TLDS = HashWithIndifferentAccess.new(
|
93
|
+
'us' => 'com',
|
94
|
+
'uk' => 'co.uk',
|
95
|
+
'ca' => 'ca',
|
96
|
+
'de' => 'de',
|
97
|
+
'jp' => 'co.jp',
|
98
|
+
'fr' => 'fr'
|
99
|
+
)
|
100
|
+
def self.tld(country)
|
101
|
+
TLDS.fetch(country || 'us') do
|
102
|
+
raise RequestError, "Invalid country '#{country}'"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.prepare_url(opts)
|
107
|
+
opts = opts.to_hash.to_options!
|
108
|
+
raise opts.inspect if opts.has_key?(:cart)
|
109
|
+
opts.assert_valid_keys(*valid_arguments(opts[:operation]))
|
110
|
+
|
111
|
+
params = opts.each_pair do |k, v|
|
112
|
+
opts.delete(k)
|
113
|
+
v *= ',' if v.is_a? Array
|
114
|
+
opts[k.to_s.camelize] = v.to_s
|
115
|
+
params
|
116
|
+
end
|
117
|
+
|
118
|
+
params.merge!(
|
119
|
+
'Service' => 'AWSECommerceService',
|
120
|
+
'Timestamp' => Time.now.gmtime.iso8601,
|
121
|
+
'SignatureVersion' => '2',
|
122
|
+
'SignatureMethod' => "HmacSHA256"
|
123
|
+
)
|
124
|
+
|
125
|
+
unsigned_uri = URI.parse("http://webservices.amazon.#{tld(opts.delete("Country"))}/onca/xml?#{params.sort { |a, b| a[0] <=> b[0] }.map { |key, val| "#{key}=#{CGI::escape(val).gsub('+', '%20')}" }.join("&")}")
|
126
|
+
hmac = HMAC::SHA256.new(ENV['AMAZON_SECRET_ACCESS_KEY'])
|
127
|
+
hmac.update("GET\n#{unsigned_uri.host}\n#{unsigned_uri.path}\n#{unsigned_uri.query}")
|
128
|
+
"#{unsigned_uri}&Signature=#{CGI::escape(Base64.encode64(hmac.digest).chomp)}"
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.cacheable?(operation)
|
132
|
+
caching_enabled? && !operation.starts_with?('Cart')
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.caching_enabled?
|
136
|
+
!options[:caching_strategy].blank?
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.cache_response(request, response)
|
140
|
+
FilesystemCache.cache(request, response)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../request')
|
2
|
+
|
3
|
+
module Amazon
|
4
|
+
module Associates
|
5
|
+
private
|
6
|
+
def self.unpack_item(opts, index, item, count = 1)
|
7
|
+
case item
|
8
|
+
when CartItem
|
9
|
+
opts[:"Item.#{index}.CartItemId"] = item.cart_item_id
|
10
|
+
when String
|
11
|
+
opts["Item.#{index}.ASIN"] = item
|
12
|
+
when Item
|
13
|
+
opts["Item.#{index}.ASIN"] = item.asin
|
14
|
+
else
|
15
|
+
item = item.to_hash.dup
|
16
|
+
unless [:offer_listing_id, :asin, :list_item_id, :cart_item_id].any?{|id| item.has_key?(id)}
|
17
|
+
raise ArgumentError, "item needs an OfferListingId, ASIN, or ListItemId"
|
18
|
+
end
|
19
|
+
|
20
|
+
if id = item[:cart_item_id]
|
21
|
+
opts[:"Item.#{index}.CartItemId"] = id
|
22
|
+
elsif id = item[:offer_listing_id]
|
23
|
+
opts[:"Item.#{index}.OfferListingId"] = id
|
24
|
+
elsif id = item.delete(:asin)
|
25
|
+
opts["Item.#{index}.ASIN"] = id
|
26
|
+
elsif id = item.delete(:list_item_id)
|
27
|
+
opts["Item.#{index}.ListItemId"] = id
|
28
|
+
end
|
29
|
+
end
|
30
|
+
opts[:"Item.#{index}.Quantity"] = count
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.unpack_items(opts)
|
34
|
+
raise ArgumentError, "items are required" if opts[:items].blank?
|
35
|
+
|
36
|
+
opts.delete(:items).each_with_index do |(item, count), index|
|
37
|
+
unpack_item(opts, index, item, count || item[:quantity] || 1)
|
38
|
+
end
|
39
|
+
opts
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.unpack_cart(opts)
|
43
|
+
opts.merge!(opts.delete(:cart).to_hash) if opts[:cart]
|
44
|
+
opts[:cart_id] ||= opts.delete(:id)
|
45
|
+
opts[:hMAC] ||= opts.delete(:hmac)
|
46
|
+
opts
|
47
|
+
end
|
48
|
+
|
49
|
+
public
|
50
|
+
# Cart operations build the Item tags from the ASIN
|
51
|
+
|
52
|
+
# Creates remote shopping cart containing _asin_
|
53
|
+
request :cart_create => :items do |opts|
|
54
|
+
unpack_items(opts)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Adds item to remote shopping cart
|
58
|
+
request :cart_add => :cart do |opts|
|
59
|
+
opts = unpack_items(opts)
|
60
|
+
unpack_cart(opts)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Adds item to remote shopping cart
|
64
|
+
request :cart_get => :cart do |opts|
|
65
|
+
unpack_cart(opts)
|
66
|
+
end
|
67
|
+
|
68
|
+
# modifies _cart_item_id_ in remote shopping cart
|
69
|
+
# _quantity_ defaults to 0 to remove the given _cart_item_id_
|
70
|
+
# specify _quantity_ to update cart contents
|
71
|
+
request :cart_modify => :cart do |opts|
|
72
|
+
opts = unpack_items(opts)
|
73
|
+
unpack_cart(opts)
|
74
|
+
end
|
75
|
+
|
76
|
+
# clears contents of remote shopping cart
|
77
|
+
request :cart_clear => :cart do |opts|
|
78
|
+
unpack_cart(opts)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../request')
|
2
|
+
|
3
|
+
module Amazon
|
4
|
+
module Associates
|
5
|
+
request :item_search => :keywords do |opts|
|
6
|
+
# TODO: Default to blended? Don't show others except on refined search page?
|
7
|
+
opts[:search_index] ||= 'Books'
|
8
|
+
opts
|
9
|
+
end
|
10
|
+
request :similarity_lookup => :item_id,
|
11
|
+
:item_lookup => :item_id
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Amazon
|
2
|
+
module Associates
|
3
|
+
class BrowseNodeLookupResponse < Response
|
4
|
+
xml_name 'BrowseNodeLookupResponse'
|
5
|
+
xml_reader :browse_nodes, :as => [BrowseNode]
|
6
|
+
xml_reader :request, :as => Request, :in => 'xmlns:BrowseNodes'
|
7
|
+
xml_reader :request_query, :as => BrowseNodeLookupRequest, :in => 'xmlns:BrowseNodes/xmlns:Request'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Amazon
|
2
|
+
module Associates
|
3
|
+
class CartResponse < Response
|
4
|
+
xml_reader :cart, :as => Cart, :required => true
|
5
|
+
xml_reader :request, :as => Request, :in => 'xmlns:Cart'
|
6
|
+
xml_reader :query_request, :as => CartRequest, :in => 'xmlns:Cart/xmlns:Request'
|
7
|
+
end
|
8
|
+
|
9
|
+
class CartCreateResponse < CartResponse
|
10
|
+
xml_name 'CartCreateResponse'
|
11
|
+
end
|
12
|
+
|
13
|
+
class CartGetResponse < CartResponse
|
14
|
+
end
|
15
|
+
|
16
|
+
class CartAddResponse < CartResponse
|
17
|
+
end
|
18
|
+
|
19
|
+
class CartModifyResponse < CartResponse
|
20
|
+
end
|
21
|
+
|
22
|
+
class CartClearResponse < CartResponse
|
23
|
+
xml_name 'CartClearResponse'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|