ebay-trader 0.9.5
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.
- 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
|