ebay-trader 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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