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.
@@ -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
@@ -0,0 +1,5 @@
1
+ #
2
+ # A more comprehensive list of examples and use cases can be found here:
3
+ #
4
+ # https://github.com/altabyte/ebay_trader_support
5
+ #
@@ -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