ebay-trader 0.9.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +325 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/ebay_trader.gemspec +34 -0
- data/examples/get_categories.rb +39 -0
- data/examples/more_examples.rb +5 -0
- data/lib/ebay_trader.rb +45 -0
- data/lib/ebay_trader/configuration.rb +242 -0
- data/lib/ebay_trader/fetch_token.rb +45 -0
- data/lib/ebay_trader/request.rb +354 -0
- data/lib/ebay_trader/sax_handler.rb +151 -0
- data/lib/ebay_trader/session_id.rb +71 -0
- data/lib/ebay_trader/version.rb +3 -0
- data/lib/ebay_trader/xml_builder.rb +113 -0
- data/lib/hash_with_indifferent_access.rb +23 -0
- metadata +138 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# This example downloads the list of categories from eBay and prints out their names.
|
5
|
+
#
|
6
|
+
# With no arguments this script will display all category names.
|
7
|
+
# To get a list of sub-categories provide a parent category ID as the first argument.
|
8
|
+
#
|
9
|
+
|
10
|
+
require 'ebay_trader'
|
11
|
+
require 'ebay_trader/request'
|
12
|
+
|
13
|
+
EbayTrader.configure do |config|
|
14
|
+
|
15
|
+
config.ebay_api_version = 935
|
16
|
+
config.environment = :sandbox
|
17
|
+
config.ebay_site_id = 0 # ebay.com
|
18
|
+
|
19
|
+
config.auth_token = ENV['EBAY_API_AUTH_TOKEN_TEST_USER_1']
|
20
|
+
|
21
|
+
config.dev_id = ENV['EBAY_API_DEV_ID_SANDBOX']
|
22
|
+
config.app_id = ENV['EBAY_API_APP_ID_SANDBOX']
|
23
|
+
config.cert_id = ENV['EBAY_API_CERT_ID_SANDBOX']
|
24
|
+
config.ru_name = ENV['EBAY_API_RU_NAME_01_SANDBOX']
|
25
|
+
end
|
26
|
+
|
27
|
+
request = EbayTrader::Request.new('GetCategories') do
|
28
|
+
CategorySiteID 0
|
29
|
+
CategoryParent ARGV.first unless ARGV.empty?
|
30
|
+
DetailLevel 'ReturnAll'
|
31
|
+
LevelLimit 5
|
32
|
+
ViewAllNodes true
|
33
|
+
end
|
34
|
+
request.errors_and_warnings.each { |error| puts error.long_message } if request.has_errors_or_warnings?
|
35
|
+
|
36
|
+
if request.success?
|
37
|
+
category_names = request.response_hash[:category_array][:category].map { |c| c[:category_name] }
|
38
|
+
category_names.each { |name| puts name }
|
39
|
+
end
|
data/lib/ebay_trader.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'ebay_trader/version'
|
2
|
+
require 'ebay_trader/configuration'
|
3
|
+
|
4
|
+
module EbayTrader
|
5
|
+
|
6
|
+
# Generic runtime error for this gem.
|
7
|
+
#
|
8
|
+
class EbayTraderError < RuntimeError; end
|
9
|
+
|
10
|
+
# The error raised when the HTTP connection times out.
|
11
|
+
#
|
12
|
+
# *Note:* A request can timeout and technically still succeed!
|
13
|
+
# Consider the case when a
|
14
|
+
# {http://developer.ebay.com/DevZone/XML/docs/Reference/ebay/AddFixedPriceItem.html AddFixedPriceItem}
|
15
|
+
# call raises a +EbayTraderTimeoutError+. The item could have been successfully
|
16
|
+
# uploaded, but a network issue delayed the response.
|
17
|
+
# Hence, a +EbayTraderTimeoutError+ does not always imply the call failed.
|
18
|
+
#
|
19
|
+
class EbayTraderTimeoutError < EbayTraderError; end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
|
23
|
+
def configuration
|
24
|
+
@configuration ||= Configuration.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def configure
|
28
|
+
yield configuration
|
29
|
+
end
|
30
|
+
|
31
|
+
# Determine if the {https://github.com/RubyMoney/money Money} gem is installed.
|
32
|
+
# @return [Boolean] +true+ if Money gem can be used by this app.
|
33
|
+
#
|
34
|
+
def is_money_gem_installed?
|
35
|
+
begin
|
36
|
+
return true if defined? Money
|
37
|
+
gem 'money'
|
38
|
+
require 'money' unless defined? Money
|
39
|
+
true
|
40
|
+
rescue Gem::LoadError
|
41
|
+
false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module EbayTrader
|
5
|
+
class Configuration
|
6
|
+
|
7
|
+
# URL for eBay's Trading API *Production* environment.
|
8
|
+
# @see https://ebaydts.com/eBayKBDetails?KBid=429
|
9
|
+
URI_PRODUCTION = 'https://api.ebay.com/ws/api.dll'
|
10
|
+
|
11
|
+
# URL for eBay's Trading API *Sandbox* environment.
|
12
|
+
# @see https://ebaydts.com/eBayKBDetails?KBid=429
|
13
|
+
URI_SANDBOX = 'https://api.sandbox.ebay.com/ws/api.dll'
|
14
|
+
|
15
|
+
DEFAULT_AUTH_TOKEN_KEY = '__DEFAULT__'
|
16
|
+
|
17
|
+
# The Dev ID application key.
|
18
|
+
# @return [String] Application keys Developer ID.
|
19
|
+
attr_accessor :dev_id
|
20
|
+
|
21
|
+
# @return [String] Application keys App ID.
|
22
|
+
attr_accessor :app_id
|
23
|
+
|
24
|
+
# @return [String] Application keys Certificate ID.
|
25
|
+
attr_accessor :cert_id
|
26
|
+
|
27
|
+
# @return [URI] Get the URI for eBay API requests, which will be different for
|
28
|
+
# sandbox and production environments.
|
29
|
+
attr_accessor :uri
|
30
|
+
|
31
|
+
# @return [Fixnum] The default eBay site ID to use in API requests, default is 0.
|
32
|
+
# This can be overridden by including an ebay_site_id value in the list of
|
33
|
+
# arguments to {EbayTrader::Request#initialize}.
|
34
|
+
# @see https://developer.ebay.com/DevZone/merchandising/docs/Concepts/SiteIDToGlobalID.html
|
35
|
+
attr_accessor :ebay_site_id
|
36
|
+
|
37
|
+
# @return [Fixnum] the eBay Trading API version.
|
38
|
+
# @see http://developer.ebay.com/DevZone/XML/docs/ReleaseNotes.html
|
39
|
+
attr_accessor :ebay_api_version
|
40
|
+
|
41
|
+
# @return [String] the eBay RuName for the application.
|
42
|
+
# @see http://developer.ebay.com/DevZone/xml/docs/HowTo/Tokens/GettingTokens.html#step1
|
43
|
+
attr_accessor :ru_name
|
44
|
+
|
45
|
+
# @return [Fixnum] the number of seconds before the HTTP session times out.
|
46
|
+
attr_reader :http_timeout
|
47
|
+
|
48
|
+
# Set the type of object to be used to represent price values, with the default being +:big_decimal+.
|
49
|
+
#
|
50
|
+
# * +*:big_decimal*+ expose price values as +BigDecimal+
|
51
|
+
# * +*:money*+ expose price values as {https://github.com/RubyMoney/money Money} objects, but only if the +Money+ gem is available to your app.
|
52
|
+
# * +*:fixnum*+ expose price values as +Fixnum+
|
53
|
+
# * +*:integer*+ expose price values as +Fixnum+
|
54
|
+
# * +*:float*+ expose price values as +Float+ - not recommended!
|
55
|
+
#
|
56
|
+
# @return [Symbol] :big_decimal, :money, :fixnum or :float
|
57
|
+
attr_accessor :price_type
|
58
|
+
|
59
|
+
# @return [Proc] an optional Proc or Lambda to record application level API request call volume.
|
60
|
+
attr_reader :counter_callback
|
61
|
+
|
62
|
+
# Specify if the SSL certificate should be verified, +true+ by default.
|
63
|
+
# It is recommended that all SSL certificates are verified to prevent
|
64
|
+
# man-in-the-middle type attacks.
|
65
|
+
#
|
66
|
+
# One potential reason for temporarily deactivating verification is when
|
67
|
+
# certificates expire, which they periodically do, and you need to take
|
68
|
+
# emergency steps to keep your service running. In such cases you may
|
69
|
+
# see the following error message:
|
70
|
+
#
|
71
|
+
# SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed
|
72
|
+
#
|
73
|
+
# @return [Boolean|String] +true+, +false+ or the path to {http://curl.haxx.se/ca/cacert.pem PEM certificate} file.
|
74
|
+
#
|
75
|
+
# @see http://www.rubyinside.com/how-to-cure-nethttps-risky-default-https-behavior-4010.html
|
76
|
+
# @see http://www.rubyinside.com/nethttp-cheat-sheet-2940.html
|
77
|
+
#
|
78
|
+
attr_accessor :ssl_verify
|
79
|
+
|
80
|
+
def initialize
|
81
|
+
self.environment = :sandbox
|
82
|
+
@dev_id = nil
|
83
|
+
@environment = :sandbox
|
84
|
+
|
85
|
+
@dev_id = nil
|
86
|
+
@app_id = nil
|
87
|
+
@cert_id = nil
|
88
|
+
|
89
|
+
@ebay_site_id = 0
|
90
|
+
@ebay_api_version = 935 # 2015-Jul-24
|
91
|
+
@http_timeout = 30 # seconds
|
92
|
+
|
93
|
+
@price_type = :big_decimal
|
94
|
+
|
95
|
+
@username_auth_tokens = {}
|
96
|
+
|
97
|
+
@ssl_verify = true
|
98
|
+
end
|
99
|
+
|
100
|
+
# Set the eBay environment to either *:sandbox* or *:production*.
|
101
|
+
# If the value of +env+ is not recognized :sandbox will be assumed.
|
102
|
+
#
|
103
|
+
# @param [Symbol] env :sandbox or :production
|
104
|
+
# @return [Symbol] :sandbox or :production
|
105
|
+
#
|
106
|
+
def environment=(env)
|
107
|
+
@environment = (env.to_s.downcase.strip == 'production') ? :production : :sandbox
|
108
|
+
@uri = URI.parse(production? ? URI_PRODUCTION : URI_SANDBOX)
|
109
|
+
@environment
|
110
|
+
end
|
111
|
+
|
112
|
+
# Determine if this app is targeting eBay's production environment.
|
113
|
+
# @return [Boolean] +true+ if production mode, otherwise +false+.
|
114
|
+
#
|
115
|
+
def production?
|
116
|
+
@environment == :production
|
117
|
+
end
|
118
|
+
|
119
|
+
# Determine if this app is targeting eBay's sandbox environment.
|
120
|
+
# @return [Boolean] +true+ if sandbox mode, otherwise +false+.
|
121
|
+
#
|
122
|
+
def sandbox?
|
123
|
+
!production?
|
124
|
+
end
|
125
|
+
|
126
|
+
# Determine if all {#dev_id}, {#app_id} and {#cert_id} have all been set.
|
127
|
+
# @return [Boolean] +true+ if dev_id, app_id and cert_id have been defined.
|
128
|
+
#
|
129
|
+
def has_keys_set?
|
130
|
+
!(dev_id.nil? || app_id.nil? || cert_id.nil?)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Optionally set a default authentication token to be used in API requests.
|
134
|
+
#
|
135
|
+
# @param [String] auth_token the eBay auth token for the user making requests.
|
136
|
+
#
|
137
|
+
def auth_token=(auth_token)
|
138
|
+
map_auth_token(DEFAULT_AUTH_TOKEN_KEY, auth_token)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Get the default authentication token, or +nil+ if not set.
|
142
|
+
# @return [String] the default auth token.
|
143
|
+
#
|
144
|
+
def auth_token
|
145
|
+
auth_token_for(DEFAULT_AUTH_TOKEN_KEY)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Map an eBay API auth token to an easy to remember +String+ key.
|
149
|
+
# This could be the corresponding eBay username thus making it easier
|
150
|
+
# to select the user auth token from a UI list or command line argument.
|
151
|
+
#
|
152
|
+
# @param [String] key auth_token identifier, typically an eBay username.
|
153
|
+
# @param [String] auth_token an eBay API authentication token.
|
154
|
+
#
|
155
|
+
def map_auth_token(key, auth_token)
|
156
|
+
@username_auth_tokens[secure_auth_token_key(key)] = auth_token
|
157
|
+
end
|
158
|
+
|
159
|
+
# Get the eBay API auth token matching the given +key+, or +nil+ if
|
160
|
+
# not found.
|
161
|
+
#
|
162
|
+
# @return [String] the corresponding auth token, or +nil+.
|
163
|
+
#
|
164
|
+
def auth_token_for(key)
|
165
|
+
@username_auth_tokens[secure_auth_token_key(key)]
|
166
|
+
end
|
167
|
+
|
168
|
+
# Provide a callback to track the number of eBay API calls made.
|
169
|
+
#
|
170
|
+
# As eBay rations the number of API calls you can make in a single day,
|
171
|
+
# typically to 5_000, it is advisable to record the volume of calls submitted.
|
172
|
+
# Here you can provide an application level callback that will be called
|
173
|
+
# during each API {Request}.
|
174
|
+
#
|
175
|
+
# @param [Proc|lambda] callback to be called during each eBay API request call.
|
176
|
+
# @return [Proc]
|
177
|
+
#
|
178
|
+
def counter=(callback)
|
179
|
+
@counter_callback = callback if callback && callback.is_a?(Proc)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Determine if a {#counter_callback} has been set for this application.
|
183
|
+
#
|
184
|
+
# @return [Boolean] +true+ if a counter proc or lambda has been provided.
|
185
|
+
#
|
186
|
+
def has_counter?
|
187
|
+
@counter_callback != nil
|
188
|
+
end
|
189
|
+
|
190
|
+
def dev_id=(id)
|
191
|
+
raise EbayTraderError, 'Dev ID does not appear to be valid' unless application_key_valid?(id)
|
192
|
+
@dev_id = id
|
193
|
+
end
|
194
|
+
|
195
|
+
def app_id=(id)
|
196
|
+
raise EbayTraderError, 'App ID does not appear to be valid' unless application_key_valid?(id)
|
197
|
+
@app_id = id
|
198
|
+
end
|
199
|
+
|
200
|
+
def cert_id=(id)
|
201
|
+
raise EbayTraderError, 'Cert ID does not appear to be valid' unless application_key_valid?(id)
|
202
|
+
@cert_id = id
|
203
|
+
end
|
204
|
+
|
205
|
+
def price_type=(price_type_symbol)
|
206
|
+
case price_type_symbol
|
207
|
+
when :fixnum then @price_type = :fixnum
|
208
|
+
when :integer then @price_type = :fixnum
|
209
|
+
when :float then @price_type = :float
|
210
|
+
when :money then @price_type = EbayTrader.is_money_gem_installed? ? :money : :fixnum
|
211
|
+
else
|
212
|
+
@price_type = :big_decimal
|
213
|
+
end
|
214
|
+
@price_type
|
215
|
+
end
|
216
|
+
|
217
|
+
def ssl_verify=(verify)
|
218
|
+
if verify
|
219
|
+
@ssl_verify = verify.is_a?(String) ? verify : true
|
220
|
+
else
|
221
|
+
@ssl_verify = false
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
#---------------------------------------------------------------------------
|
226
|
+
private
|
227
|
+
|
228
|
+
# Validate the given {#dev_id}, {#app_id} or {#cert_id}.
|
229
|
+
# These are almost like GUID/UUID values with the exception that the first
|
230
|
+
# block of 8 digits of AppID can be any letters.
|
231
|
+
# @return [Boolean] +true+ if the ID has the correct format.
|
232
|
+
#
|
233
|
+
def application_key_valid?(id)
|
234
|
+
id =~ /[A-Z0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}/i
|
235
|
+
end
|
236
|
+
|
237
|
+
def secure_auth_token_key(key)
|
238
|
+
Digest::MD5.hexdigest(key.to_s.downcase)
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'active_support/time'
|
2
|
+
require 'ebay_trader/request'
|
3
|
+
require 'ebay_trader/session_id'
|
4
|
+
|
5
|
+
module EbayTrader
|
6
|
+
|
7
|
+
# Fetch an eBay user authentication token using a {SessionID} value.
|
8
|
+
#
|
9
|
+
# @see http://developer.ebay.com/DevZone/XML/docs/Reference/eBay/FetchToken.html
|
10
|
+
# @see http://developer.ebay.com/DevZone/XML/docs/HowTo/Tokens/GettingTokens.html
|
11
|
+
# @see http://developer.ebay.com/DevZone/guides/ebayfeatures/Basics/Tokens-MultipleUsers.html
|
12
|
+
#
|
13
|
+
class FetchToken < Request
|
14
|
+
|
15
|
+
CALL_NAME = 'FetchToken'
|
16
|
+
|
17
|
+
attr_reader :session_id
|
18
|
+
|
19
|
+
# Construct a fetch token eBay API request with the given session ID.
|
20
|
+
# @param [SessionID|String] session_id the session ID.
|
21
|
+
# @param [Hash] args a hash of optional arguments.
|
22
|
+
#
|
23
|
+
def initialize(session_id, args = {})
|
24
|
+
session_id = session_id.id if session_id.is_a?(SessionID)
|
25
|
+
@session_id = session_id.freeze
|
26
|
+
super(CALL_NAME, args) do
|
27
|
+
SessionID session_id
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get the authentication token.
|
32
|
+
# @return [String] the authentication token.
|
33
|
+
#
|
34
|
+
def auth_token
|
35
|
+
response_hash[:ebay_auth_token]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get the Time at which the authentication token expires.
|
39
|
+
# @return [Time] the expiry time.
|
40
|
+
#
|
41
|
+
def expiry_time
|
42
|
+
response_hash[:hard_expiration_time]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,354 @@
|
|
1
|
+
require 'active_support/gzip'
|
2
|
+
require 'net/http'
|
3
|
+
require 'ox'
|
4
|
+
require 'rexml/document'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'yaml'
|
7
|
+
require 'openssl'
|
8
|
+
require 'base64'
|
9
|
+
|
10
|
+
require 'ebay_trader'
|
11
|
+
require 'ebay_trader/sax_handler'
|
12
|
+
require 'ebay_trader/xml_builder'
|
13
|
+
|
14
|
+
module EbayTrader
|
15
|
+
|
16
|
+
class Request
|
17
|
+
|
18
|
+
# A Struct wrapper around eBay generated error and warning messages.
|
19
|
+
Error = Struct.new(:error_classification, :severity_code, :error_code, :short_message, :long_message) do
|
20
|
+
def error?; severity_code == 'Error'; end
|
21
|
+
def warning?; severity_code == 'Warning'; end
|
22
|
+
end
|
23
|
+
|
24
|
+
# eBay Trading API XML Namespace
|
25
|
+
XMLNS = 'urn:ebay:apis:eBLBaseComponents'
|
26
|
+
|
27
|
+
attr_reader :call_name
|
28
|
+
attr_reader :auth_token
|
29
|
+
attr_reader :ebay_site_id
|
30
|
+
attr_reader :message_id
|
31
|
+
attr_reader :response_hash
|
32
|
+
attr_reader :skip_type_casting
|
33
|
+
attr_reader :known_arrays
|
34
|
+
attr_reader :xml_tab_width
|
35
|
+
attr_reader :xml_request
|
36
|
+
attr_reader :xml_response
|
37
|
+
attr_reader :http_timeout
|
38
|
+
attr_reader :http_response_code
|
39
|
+
attr_reader :response_time
|
40
|
+
|
41
|
+
# Construct a new eBay Trading API call.
|
42
|
+
#
|
43
|
+
# @param [String] call_name the name of the API call, for example 'GeteBayOfficialTime'.
|
44
|
+
#
|
45
|
+
# @param [Hash] args optional configuration values for this request.
|
46
|
+
#
|
47
|
+
# @option args [String] :auth_token the eBay Auth Token for the user submitting this request.
|
48
|
+
# If not defined the value of {Configuration#auth_token} will be assumed.
|
49
|
+
#
|
50
|
+
# @option args [Fixnum] :ebay_site_id Override the default eBay site ID in {Configuration#ebay_site_id}
|
51
|
+
#
|
52
|
+
# @option args [Fixnum] :http_timeout Override the default value of {Configuration#http_timeout}.
|
53
|
+
#
|
54
|
+
# This may be necessary for one-off calls such as
|
55
|
+
# {http://developer.ebay.com/DevZone/XML/docs/Reference/ebay/UploadSiteHostedPictures.html UploadSiteHostedPictures}
|
56
|
+
# which can take significantly longer.
|
57
|
+
#
|
58
|
+
# @option args [Array [String]] :skip_type_casting An array of the keys for which the values should *not*
|
59
|
+
# get automatically type cast.
|
60
|
+
#
|
61
|
+
# Take for example the 'BuyerUserID' field. If someone has the username '123456'
|
62
|
+
# the auto-type-casting would consider this to be a Fixnum. Adding 'BuyerUserID'
|
63
|
+
# to skip_type_casting list will ensure it remains a String.
|
64
|
+
#
|
65
|
+
# @option args [Array [String]] :known_arrays a list of the names of elements that are known to have arrays
|
66
|
+
# of values. If defined here {#response_hash} will ensure array values in circumstances
|
67
|
+
# where there is only a single child element in the response XML.
|
68
|
+
#
|
69
|
+
# It is not necessary to use this feature, but doing so can simplify later stage logic
|
70
|
+
# as certain fields are guaranteed to be arrays. As there is no concept of arrays in XML
|
71
|
+
# it is not otherwise possible to determine if a field should be an array.
|
72
|
+
#
|
73
|
+
# An example case is when building a tree of nested categories. Some categories may only have
|
74
|
+
# one child category, but adding 'Category' or :category to this list will ensure the
|
75
|
+
# response_hash values is always an array. Hence it will not necessary to check if the elements
|
76
|
+
# of a category element is a Hash or an Array of Hashes when recursing through the data.
|
77
|
+
#
|
78
|
+
# @option args [String] :xml_response inject a pre-prepared XML response.
|
79
|
+
#
|
80
|
+
# If an XML response is given here the request will not actually be sent to eBay.
|
81
|
+
# Using this feature can dramatically speed up testing and also ensure you stay
|
82
|
+
# within eBay's 5,000 requests per day throttling rate.
|
83
|
+
#
|
84
|
+
# It is also a useful feature for parsing locally cached/archived XML files.
|
85
|
+
#
|
86
|
+
# @option args [Fixnum] :xml_tab_width the number of spaces to indent child elements in the generated XML.
|
87
|
+
# The default is 0, meaning the XML is a single line string, but it's
|
88
|
+
# nice to have the option of pretty-printing the XML for debugging.
|
89
|
+
#
|
90
|
+
# @yield [xml_builder] a block describing the XML DOM.
|
91
|
+
#
|
92
|
+
# @yieldparam name [XMLBuilder] an XML builder node allowing customization of the request specific details.
|
93
|
+
#
|
94
|
+
# @yieldreturn [XMLBuilder] the same XML builder originally provided by the block.
|
95
|
+
#
|
96
|
+
# @raise [EbayTraderError] if the API call fails.
|
97
|
+
#
|
98
|
+
# @raise [EbayTraderTimeoutError] if the HTTP call times out.
|
99
|
+
#
|
100
|
+
def initialize(call_name, args = {}, &block)
|
101
|
+
time = Time.now
|
102
|
+
@call_name = call_name.freeze
|
103
|
+
|
104
|
+
auth_token = %w"GetSessionID FetchToken GetTokenStatus RevokeToken".include?(call_name) ?
|
105
|
+
nil : (args[:auth_token] || EbayTrader.configuration.auth_token)
|
106
|
+
@auth_token = auth_token.freeze
|
107
|
+
|
108
|
+
@ebay_site_id = (args[:ebay_site_id] || EbayTrader.configuration.ebay_site_id).to_i
|
109
|
+
@http_timeout = (args[:http_timeout] || EbayTrader.configuration.http_timeout).to_f
|
110
|
+
@xml_tab_width = (args[:xml_tab_width] || 0).to_i
|
111
|
+
|
112
|
+
@xml_response = args[:xml_response] || ''
|
113
|
+
|
114
|
+
@skip_type_casting = args[:skip_type_casting] || []
|
115
|
+
@skip_type_casting = @skip_type_casting.split if @skip_type_casting.is_a?(String)
|
116
|
+
|
117
|
+
@known_arrays = args[:known_arrays] || []
|
118
|
+
@known_arrays = @known_arrays.split if @known_arrays.is_a?(String)
|
119
|
+
@known_arrays << 'errors'
|
120
|
+
|
121
|
+
@message_id = nil
|
122
|
+
if args.key?(:message_id)
|
123
|
+
@message_id = (args[:message_id] == true) ? SecureRandom.uuid : args[:message_id].to_s
|
124
|
+
end
|
125
|
+
|
126
|
+
@xml_request = '<?xml version="1.0" encoding="utf-8"?>' << "\n"
|
127
|
+
@xml_request << XMLBuilder.new(tab_width: xml_tab_width).root("#{call_name}Request", xmlns: XMLNS) do
|
128
|
+
unless auth_token.blank?
|
129
|
+
RequesterCredentials do
|
130
|
+
eBayAuthToken auth_token.to_s
|
131
|
+
end
|
132
|
+
end
|
133
|
+
instance_eval(&block) if block_given?
|
134
|
+
MessageID message_id unless message_id.nil?
|
135
|
+
end
|
136
|
+
|
137
|
+
@http_response_code = 200
|
138
|
+
submit if xml_response.blank?
|
139
|
+
|
140
|
+
parsed_hash = parse(xml_response)
|
141
|
+
root_key = parsed_hash.keys.first
|
142
|
+
raise EbayTraderError, "Response '#{root_key}' does not match call name" unless root_key.gsub('_', '').eql?("#{call_name}Response".downcase)
|
143
|
+
|
144
|
+
@response_hash = parsed_hash[root_key]
|
145
|
+
@response_hash.freeze
|
146
|
+
@response_time = Time.now - time
|
147
|
+
|
148
|
+
@errors = []
|
149
|
+
deep_find(:errors, []).each do |error|
|
150
|
+
@errors << Error.new(error[:error_classification],
|
151
|
+
error[:severity_code],
|
152
|
+
error[:error_code],
|
153
|
+
error[:short_message],
|
154
|
+
error[:long_message])
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Determine if this request has been successful.
|
159
|
+
# This should return the opposite of {#failure?}
|
160
|
+
def success?
|
161
|
+
deep_find(:ack, '').downcase.eql?('success')
|
162
|
+
end
|
163
|
+
|
164
|
+
# Determine if this request has failed.
|
165
|
+
# This should return the opposite of {#success?}
|
166
|
+
def failure?
|
167
|
+
deep_find(:ack, '').downcase.eql?('failure')
|
168
|
+
end
|
169
|
+
|
170
|
+
# Determine if this response has partially failed.
|
171
|
+
# This eBay response is somewhat ambiguous, but generally means the request was
|
172
|
+
# processed by eBay, but warnings were generated.
|
173
|
+
def partial_failure?
|
174
|
+
deep_find(:ack, '').downcase.eql?('partialfailure')
|
175
|
+
end
|
176
|
+
|
177
|
+
# Determine if this request has generated any {#errors} or {#warnings}.
|
178
|
+
# @return [Boolean] +true+ if errors or warnings present.
|
179
|
+
def has_errors_or_warnings?
|
180
|
+
has_errors? || has_warnings?
|
181
|
+
end
|
182
|
+
|
183
|
+
# Get an array of all {#errors} and {#warnings}.
|
184
|
+
# @return [Array[Error]] all {#errors} and {#warnings} combined.
|
185
|
+
def errors_and_warnings
|
186
|
+
@errors
|
187
|
+
end
|
188
|
+
|
189
|
+
# Determine if this request has generated any {#errors}, excluding {#warnings}.
|
190
|
+
# @return [Boolean] +true+ if any errors present.
|
191
|
+
def has_errors?
|
192
|
+
errors.count > 0
|
193
|
+
end
|
194
|
+
|
195
|
+
# Get an array of {Error}s, excluding {#warnings}. This will be an empty array if there are no errors.
|
196
|
+
# @return [Array[Error]] which have a severity_code of 'Error'.
|
197
|
+
def errors
|
198
|
+
@errors.select { |error| error.error? }
|
199
|
+
end
|
200
|
+
|
201
|
+
# Determine if this request has generated any {#warnings}.
|
202
|
+
# @return [Boolean] +true+ if warnings present.
|
203
|
+
def has_warnings?
|
204
|
+
warnings.count > 0
|
205
|
+
end
|
206
|
+
|
207
|
+
# Get an array of {Error}s representing warnings. This will be an empty array if there are no errors.
|
208
|
+
# @return [Array[Error]] which have a severity_code of 'Warning'.
|
209
|
+
def warnings
|
210
|
+
@errors.select { |error| error.warning? }
|
211
|
+
end
|
212
|
+
|
213
|
+
# Get the timestamp of the response returned by eBay API.
|
214
|
+
# The timestamp indicates the time when eBay processed the request; it does
|
215
|
+
# not necessarily indicate the current eBay official eBay time.
|
216
|
+
# In particular, calls like
|
217
|
+
# {http://developer.ebay.com/DevZone/XML/docs/Reference/eBay/GetCategories.html GetCategories}
|
218
|
+
# can return a cached response, so the time stamp may not be current.
|
219
|
+
# @return [Time] the response timestamp.
|
220
|
+
#
|
221
|
+
def timestamp
|
222
|
+
deep_find :timestamp
|
223
|
+
end
|
224
|
+
|
225
|
+
# Recursively deep search through the {#response_hash} tree and return the
|
226
|
+
# first value matching the given +path+ of node names.
|
227
|
+
# If +path+ cannot be matched the value of +default+ is returned.
|
228
|
+
# @param [Array [String|Symbol]] path an array of the keys defining the path to the node of interest.
|
229
|
+
# @param [Object] default the value to be returned if +path+ cannot be matched.
|
230
|
+
# @return [Array] the first value found in +path+, or +default+.
|
231
|
+
def deep_find(path, default = nil)
|
232
|
+
@response_hash.deep_find(path, default)
|
233
|
+
end
|
234
|
+
|
235
|
+
# Get a String representation of the response XML with indentation.
|
236
|
+
# @return [String] the response XML.
|
237
|
+
def to_s(indent = xml_tab_width)
|
238
|
+
xml = ''
|
239
|
+
if defined? Ox
|
240
|
+
ox_doc = Ox.parse(xml_response)
|
241
|
+
xml = Ox.dump(ox_doc, indent: indent)
|
242
|
+
else
|
243
|
+
rexml_doc = REXML::Document.new(xml_response)
|
244
|
+
rexml_doc.write(xml, indent)
|
245
|
+
end
|
246
|
+
xml
|
247
|
+
end
|
248
|
+
|
249
|
+
# Get a String representation of the XML data hash in JSON notation.
|
250
|
+
# @return [String] pretty printed JSON.
|
251
|
+
def to_json_s
|
252
|
+
require 'json' unless defined? JSON
|
253
|
+
puts JSON.pretty_generate(JSON.parse(@response_hash.to_json))
|
254
|
+
end
|
255
|
+
|
256
|
+
#-------------------------------------------------------------------------
|
257
|
+
private
|
258
|
+
|
259
|
+
# Post the xml_request to eBay and record the xml_response.
|
260
|
+
def submit
|
261
|
+
raise EbayTraderError, 'Cannot post an eBay API request before application keys have been set' unless EbayTrader.configuration.has_keys_set?
|
262
|
+
|
263
|
+
uri = EbayTrader.configuration.uri
|
264
|
+
|
265
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
266
|
+
http.read_timeout = http_timeout
|
267
|
+
|
268
|
+
if uri.port == 443
|
269
|
+
# http://www.rubyinside.com/nethttp-cheat-sheet-2940.html
|
270
|
+
http.use_ssl = true
|
271
|
+
verify = EbayTrader.configuration.ssl_verify
|
272
|
+
if verify
|
273
|
+
if verify.is_a?(String)
|
274
|
+
pem = File.read(verify)
|
275
|
+
http.cert = OpenSSL::X509::Certificate.new(pem)
|
276
|
+
http.key = OpenSSL::PKey::RSA.new(pem)
|
277
|
+
end
|
278
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
279
|
+
else
|
280
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
|
285
|
+
post = Net::HTTP::Post.new(uri.path, headers)
|
286
|
+
post.body = xml_request
|
287
|
+
|
288
|
+
begin
|
289
|
+
response = http.start { |http| http.request(post) }
|
290
|
+
rescue OpenSSL::SSL::SSLError => e
|
291
|
+
# SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed
|
292
|
+
raise EbayTraderError, e
|
293
|
+
rescue Net::ReadTimeout
|
294
|
+
raise EbayTraderTimeoutError, "Failed to complete #{call_name} in #{http_timeout} seconds"
|
295
|
+
rescue Exception => e
|
296
|
+
raise EbayTraderError, e
|
297
|
+
ensure
|
298
|
+
EbayTrader.configuration.counter_callback.call if EbayTrader.configuration.has_counter?
|
299
|
+
end
|
300
|
+
|
301
|
+
@http_response_code = response.code.to_i.freeze
|
302
|
+
|
303
|
+
# If the call was successful it should have a response code starting with '2'
|
304
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
305
|
+
raise EbayTraderError, "HTTP Response Code: #{http_response_code}" unless http_response_code.between?(200, 299)
|
306
|
+
|
307
|
+
if response['Content-Encoding'] == 'gzip'
|
308
|
+
@xml_response = ActiveSupport::Gzip.decompress(response.body)
|
309
|
+
else
|
310
|
+
@xml_response = response.body
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# Parse the given XML using {SaxHandler} and return a nested Hash.
|
315
|
+
# @param [String] xml the XML string to be parsed.
|
316
|
+
# @return [Hash] a Hash corresponding to +xml+.
|
317
|
+
def parse(xml)
|
318
|
+
xml ||= ''
|
319
|
+
xml = StringIO.new(xml) unless xml.respond_to?(:read)
|
320
|
+
|
321
|
+
handler = SaxHandler.new(skip_type_casting: skip_type_casting, known_arrays: known_arrays)
|
322
|
+
Ox.sax_parse(handler, xml, convert_special: true)
|
323
|
+
handler.to_hash
|
324
|
+
end
|
325
|
+
|
326
|
+
#
|
327
|
+
# Get a hash of the default headers to be submitted to eBay API via httparty.
|
328
|
+
# Additional headers can be merged into this hash as follows:
|
329
|
+
# ebay_headers.merge({'X-EBAY-API-CALL-NAME' => 'CallName'})
|
330
|
+
# http://developer.ebay.com/Devzone/XML/docs/WebHelp/InvokingWebServices-Routing_the_Request_(Gateway_URLs).html
|
331
|
+
#
|
332
|
+
def headers
|
333
|
+
headers = {
|
334
|
+
'X-EBAY-API-COMPATIBILITY-LEVEL' => "#{EbayTrader.configuration.ebay_api_version}",
|
335
|
+
'X-EBAY-API-SITEID' => "#{ebay_site_id}",
|
336
|
+
'X-EBAY-API-CALL-NAME' => call_name,
|
337
|
+
'Content-Type' => 'text/xml',
|
338
|
+
'Accept-Encoding' => 'gzip'
|
339
|
+
}
|
340
|
+
xml = xml_request
|
341
|
+
headers.merge!({'Content-Length' => "#{xml.length}"}) if xml && !xml.strip.empty?
|
342
|
+
|
343
|
+
# These values are only required for calls that set up and retrieve a user's authentication token
|
344
|
+
# (these calls are: GetSessionID, FetchToken, GetTokenStatus, and RevokeToken).
|
345
|
+
# In all other calls, these value are ignored..
|
346
|
+
if %w"GetSessionID FetchToken GetTokenStatus RevokeToken".include?(call_name)
|
347
|
+
headers.merge!({'X-EBAY-API-DEV-NAME' => EbayTrader.configuration.dev_id})
|
348
|
+
headers.merge!({'X-EBAY-API-APP-NAME' => EbayTrader.configuration.app_id})
|
349
|
+
headers.merge!({'X-EBAY-API-CERT-NAME' => EbayTrader.configuration.cert_id})
|
350
|
+
end
|
351
|
+
headers
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|