jsonrpc2 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require 'digest'
1
5
  require 'jsonrpc2'
2
6
  require 'jsonrpc2/accept'
3
7
  require 'jsonrpc2/textile'
@@ -9,12 +13,15 @@ require 'base64'
9
13
 
10
14
  module JSONRPC2
11
15
  module_function
16
+
12
17
  def environment
13
18
  ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
14
19
  end
20
+
15
21
  def development?
16
22
  environment.eql?('development')
17
23
  end
24
+
18
25
  # Authentication failed error - only used if transport level authentication isn't.
19
26
  # e.g. BasicAuth returns a 401 HTTP response, rather than throw this.
20
27
  class AuthFail < RuntimeError; end
@@ -34,356 +41,369 @@ module JSONRPC2
34
41
  end
35
42
  end
36
43
 
37
- # Base class for JSONRPC2 interface
38
- class Interface
39
- class << self
44
+ # Base class for JSONRPC2 interface
45
+ class Interface
46
+ class << self
47
+ # @!group Authentication
48
+ # Get/set authenticator object for API interface - see {JSONRPC2::Auth} and {JSONRPC2::BasicAuth}
49
+ #
50
+ # @param [#check] args An object that responds to check(environment, json_call_data)
51
+ # @return [#check, nil] Currently set object or nil
52
+ def auth_with *args
53
+ if args.empty?
54
+ return @auth_with
55
+ else
56
+ @auth_with = args[0]
57
+ end
58
+ end
59
+ # @!endgroup
60
+
61
+ # @!group Rack-related
62
+ # Rack compatible call handler
63
+ #
64
+ # @param [Hash] environment Rack environment hash
65
+ # @return [Array<Fixnum, Hash<String,String>, Array<String>>] Rack-compatible response
66
+ def call(environment)
67
+ environment['json.request-id'] = Digest::MD5.hexdigest("#{$host ||= Socket.gethostname}-#{$$}-#{Time.now.to_f}")[0,8]
68
+ request = Rack::Request.new(environment)
69
+ catch :rack_response do
70
+ best = JSONRPC2::HTTPUtils.which(environment['HTTP_ACCEPT'], %w[text/html application/json-rpc application/json])
71
+
72
+ if request.path_info =~ %r'/_assets' or request.path_info == '/favicon.ico'
73
+ best = 'text/html' # hack for assets
74
+ end
75
+
76
+ case best
77
+ when 'text/html', 'text/css', 'image/png' # Assume browser
78
+ monitor_time(environment, request.POST['__json__']) { JSONRPC2::HTML.call(self, request) }
79
+ when 'application/json-rpc', 'application/json', nil # Assume correct by default
80
+ environment['rack.input'].rewind
81
+ raw = environment['rack.input'].read
82
+ data = JSON.parse(raw) if raw.to_s.size >= 2
83
+ monitor_time(environment, raw) { self.new(environment).rack_dispatch(data) }
84
+ else
85
+ [406, {'Content-Type' => 'text/html'},
86
+ ["<!DOCTYPE html><html><head><title>Media type mismatch</title></head><body>I am unable to acquiesce to your request</body></html>"]]
87
+ end
88
+ end
40
89
 
41
- # @!group Authentication
90
+ rescue Exception => e
91
+ if environment['rack.logger'].respond_to?(:error)
92
+ environment['rack.logger'].error "#{e.class}: #{e.message} - #{e.backtrace * "\n "}"
93
+ end
94
+ raise e.class, e.message, e.backtrace
95
+ end
42
96
 
43
- # Get/set authenticator object for API interface - see {JSONRPC2::Auth} and {JSONRPC2::BasicAuth}
44
- #
45
- # @param [#check] args An object that responds to check(environment, json_call_data)
46
- # @return [#check, nil] Currently set object or nil
47
- def auth_with *args
48
- if args.empty?
49
- return @auth_with
50
- else
51
- @auth_with = args[0]
52
- end
97
+ private
98
+
99
+ def monitor_time(env, data, &block)
100
+ if env['rack.logger'].respond_to?(:info)
101
+ if env["HTTP_AUTHORIZATION"].to_s =~ /Basic /i
102
+ auth = Base64.decode64(env["HTTP_AUTHORIZATION"].to_s.sub(/Basic /i, '')) rescue nil
103
+ auth ||= env["HTTP_AUTHORIZATION"]
104
+ else
105
+ auth = env["HTTP_AUTHORIZATION"]
106
+ end
107
+ env['rack.logger'].info("[JSON-RPC2] #{env['json.request-id']} #{env['REQUEST_URI']} - Auth: #{auth}, Data: #{data.is_a?(String) ? data : data.inspect}")
108
+ end
109
+ t = Time.now.to_f
110
+ return yield
111
+ ensure
112
+ if env['rack.logger'].respond_to?(:info)
113
+ env['rack.logger'].info("[JSON-RPC2] #{env['json.request-id']} Completed in #{'%.3f' % ((Time.now.to_f - t) * 1000)}ms#{ $! ? " - exception = #{$!.class}:#{$!.message}" : "" }")
114
+ end
115
+ end
116
+ # @!endgroup
53
117
  end
54
118
 
55
- # @!endgroup
56
-
57
- # @!group Rack-related
58
-
59
- # Rack compatible call handler
119
+ # Create new interface object
60
120
  #
61
- # @param [Hash] environment Rack environment hash
62
- # @return [Array<Fixnum, Hash<String,String>, Array<String>>] Rack-compatible response
63
- def call(environment)
64
- environment['json.request-id'] = Digest::MD5.hexdigest("#{$host ||= Socket.gethostname}-#{$$}-#{Time.now.to_f}")[0,8]
65
- request = Rack::Request.new(environment)
66
- catch :rack_response do
67
- best = JSONRPC2::HTTPUtils.which(environment['HTTP_ACCEPT'], %w[text/html application/json-rpc application/json])
68
-
69
- if request.path_info =~ %r'/_assets' or request.path_info == '/favicon.ico'
70
- best = 'text/html' # hack for assets
71
- end
72
-
73
- case best
74
- when 'text/html', 'text/css', 'image/png' # Assume browser
75
- monitor_time(environment, request.POST['__json__']) { JSONRPC2::HTML.call(self, request) }
76
- when 'application/json-rpc', 'application/json', nil # Assume correct by default
77
- environment['rack.input'].rewind
78
- raw = environment['rack.input'].read
79
- data = JSON.parse(raw) if raw.to_s.size >= 2
80
- monitor_time(environment, raw) { self.new(environment).rack_dispatch(data) }
81
- else
82
- [406, {'Content-Type' => 'text/html'},
83
- ["<!DOCTYPE html><html><head><title>Media type mismatch</title></head><body>I am unable to acquiesce to your request</body></html>"]]
84
- end
85
- end
121
+ # @param [Hash] env Rack environment
122
+ def initialize(env)
123
+ @_jsonrpc_env = env
124
+ @_jsonrpc_request = Rack::Request.new(env)
125
+ end
86
126
 
87
- rescue Exception => e
88
- if environment['rack.logger'].respond_to?(:error)
89
- environment['rack.logger'].error "#{e.class}: #{e.message} - #{e.backtrace * "\n "}"
127
+ # Internal
128
+ def rack_dispatch(rpcData)
129
+ catch(:rack_response) do
130
+ json = dispatch(rpcData)
131
+ [200, {'Content-Type' => 'application/json-rpc'}, [json]]
90
132
  end
91
- raise e.class, e.message, e.backtrace
92
133
  end
93
134
 
94
- private
95
- def monitor_time(env, data, &block)
96
- if env['rack.logger'].respond_to?(:info)
97
- if env["HTTP_AUTHORIZATION"].to_s =~ /Basic /i
98
- auth = Base64.decode64(env["HTTP_AUTHORIZATION"].to_s.sub(/Basic /i, '')) rescue nil
99
- auth ||= env["HTTP_AUTHORIZATION"]
100
- else
101
- auth = env["HTTP_AUTHORIZATION"]
102
- end
103
- env['rack.logger'].info("[JSON-RPC2] #{env['json.request-id']} #{env['REQUEST_URI']} - Auth: #{auth}, Data: #{data.is_a?(String) ? data : data.inspect}")
104
- end
105
- t = Time.now.to_f
106
- return yield
107
- ensure
108
- if env['rack.logger'].respond_to?(:info)
109
- env['rack.logger'].info("[JSON-RPC2] #{env['json.request-id']} Completed in #{'%.3f' % ((Time.now.to_f - t) * 1000)}ms#{ $! ? " - exception = #{$!.class}:#{$!.message}" : "" }")
135
+ # Dispatch call to api method(s)
136
+ #
137
+ # @param [Hash,Array] rpc_data Array of calls or Hash containing one call
138
+ # @return [Hash,Array] Depends on input, but either a hash result or an array of results corresponding to calls.
139
+ def dispatch(rpc_data)
140
+ result = case rpc_data
141
+ when Array
142
+ rpc_data.map { |rpc| dispatch_single(rpc) }
143
+ else
144
+ dispatch_single(rpc_data)
110
145
  end
146
+
147
+ return result.to_json
111
148
  end
112
149
 
113
- # @!endgroup
150
+ protected
114
151
 
115
- end
152
+ # JSON result helper
153
+ def response_ok(id, result)
154
+ { 'jsonrpc' => '2.0', 'result' => result, 'id' => id }
155
+ end
116
156
 
117
- # Create new interface object
118
- #
119
- # @param [Hash] env Rack environment
120
- def initialize(env)
121
- @_jsonrpc_env = env
122
- @_jsonrpc_request = Rack::Request.new(env)
123
- end
124
- # Internal
125
- def rack_dispatch(rpcData)
126
- catch(:rack_response) do
127
- json = dispatch(rpcData)
128
- [200, {'Content-Type' => 'application/json-rpc'}, [json]]
157
+ # JSON error helper
158
+ def response_error(code, message, data)
159
+ { 'jsonrpc' => '2.0', 'error' => { 'code' => code, 'message' => message, 'data' => data }, 'id' => (@_jsonrpc_call && @_jsonrpc_call['id'] || nil) }
129
160
  end
130
- end
131
161
 
132
- # Dispatch call to api method(s)
133
- #
134
- # @param [Hash,Array] rpc_data Array of calls or Hash containing one call
135
- # @return [Hash,Array] Depends on input, but either a hash result or an array of results corresponding to calls.
136
- def dispatch(rpc_data)
137
- result = case rpc_data
138
- when Array
139
- rpc_data.map { |rpc| dispatch_single(rpc) }
140
- else
141
- dispatch_single(rpc_data)
162
+ # Params helper
163
+ def params
164
+ @_jsonrpc_call['params']
142
165
  end
143
166
 
144
- return result.to_json
145
- end
167
+ # Auth info
168
+ def auth
169
+ @_jsonrpc_auth
170
+ end
146
171
 
147
- protected
148
- # JSON result helper
149
- def response_ok(id, result)
150
- { 'jsonrpc' => '2.0', 'result' => result, 'id' => id }
151
- end
152
- # JSON error helper
153
- def response_error(code, message, data)
154
- { 'jsonrpc' => '2.0', 'error' => { 'code' => code, 'message' => message, 'data' => data }, 'id' => (@_jsonrpc_call && @_jsonrpc_call['id'] || nil) }
155
- end
156
- # Params helper
157
- def params
158
- @_jsonrpc_call['params']
159
- end
160
- # Auth info
161
- def auth
162
- @_jsonrpc_auth
163
- end
164
- # Rack::Request
165
- def request
166
- @_jsonrpc_request
167
- end
168
- def env
169
- @_jsonrpc_env
170
- end
172
+ # Rack::Request
173
+ def request
174
+ @_jsonrpc_request
175
+ end
171
176
 
172
- # Logger
173
- def logger
174
- @_jsonrpc_logger ||= (@_jsonrpc_env['rack.logger'] || Rack::NullLogger.new("null"))
175
- end
176
- # Check call validity and authentication & make a single method call
177
- #
178
- # @param [Hash] rpc JSON-RPC-2 call
179
- def dispatch_single(rpc)
180
- t = Time.now.to_f
181
-
182
- result = _dispatch_single(rpc)
183
-
184
- if result['result']
185
- logger.info("[JSON-RPC2] #{env['json.request-id']} Call completed OK in #{'%.3f' % ((Time.now.to_f - t) * 1000)}ms")
186
- elsif result['error']
187
- logger.info("[JSON-RPC2] #{env['json.request-id']} Call to ##{rpc['method']} failed in #{'%.3f' % ((Time.now.to_f - t) * 1000)}ms with error #{result['error']['code']} - #{result['error']['message']}")
177
+ def env
178
+ @_jsonrpc_env
188
179
  end
189
180
 
190
- result
191
- end
192
- def _dispatch_single(rpc)
193
- t = Time.now.to_f
194
- unless rpc.has_key?('id') && rpc.has_key?('method') && rpc['jsonrpc'].eql?('2.0')
195
- return response_error(-32600, 'Invalid request', nil)
181
+ # Logger
182
+ def logger
183
+ @_jsonrpc_logger ||= (@_jsonrpc_env['rack.logger'] || Rack::NullLogger.new("null"))
196
184
  end
197
- @_jsonrpc_call = rpc
198
185
 
199
- begin
200
- if self.class.auth_with && ! @_jsonrpc_auth
201
- (@_jsonrpc_auth = self.class.auth_with.client_check(@_jsonrpc_env, rpc)) or raise AuthFail, "Invalid credentials"
202
- end
186
+ # Check call validity and authentication & make a single method call
187
+ #
188
+ # @param [Hash] rpc JSON-RPC-2 call
189
+ def dispatch_single(rpc)
190
+ t = Time.now.to_f
203
191
 
204
- call(rpc['method'], rpc['id'], rpc['params'])
205
- rescue AuthFail => e
206
- response_error(-32000, "AuthFail: #{e.class}: #{e.message}", {}) # XXX: Change me
207
- rescue APIFail => e
208
- response_error(-32000, "APIFail: #{e.class}: #{e.message}", {}) # XXX: Change me
209
- rescue KnownError => e
210
- response_error(e.code, e.message, e.data) # XXX: Change me
211
- rescue Exception => e
212
- logger.error("#{env['json.request-id']} Internal error calling #{rpc.inspect} - #{e.class}: #{e.message} #{e.backtrace.join("\n ")}") if logger.respond_to?(:error)
213
- response_error(-32000, "#{e.class}: #{e.message}", e.backtrace) # XXX: Change me
214
- else
215
- end
216
- end
217
- # List API methods
218
- #
219
- # @return [Array] List of api method names
220
- def api_methods
221
- public_methods(false).map(&:to_s) - ['rack_dispatch', 'dispatch']
222
- end
192
+ result = _dispatch_single(rpc)
223
193
 
224
- # Call method, checking param and return types
225
- #
226
- # @param [String] method Method name
227
- # @param [Integer] id Method call ID - for response
228
- # @param [Hash] params Method parameters
229
- # @return [Hash] JSON response
230
- def call(method, id, params)
231
- if api_methods.include?(method)
232
- begin
233
- Types.valid_params?(self.class, method, params)
234
- rescue Exception => e
235
- return response_error(-32602, "Invalid params - #{e.message}", {})
194
+ if result['result']
195
+ logger.info("[JSON-RPC2] #{env['json.request-id']} Call completed OK in #{'%.3f' % ((Time.now.to_f - t) * 1000)}ms")
196
+ elsif result['error']
197
+ logger.info("[JSON-RPC2] #{env['json.request-id']} Call to ##{rpc['method']} failed in #{'%.3f' % ((Time.now.to_f - t) * 1000)}ms with error #{result['error']['code']} - #{result['error']['message']}")
236
198
  end
237
199
 
238
- if self.method(method).arity.zero?
239
- result = send(method)
240
- else
241
- result = send(method, params)
200
+ result
201
+ end
202
+
203
+ def _dispatch_single(rpc)
204
+ unless rpc.has_key?('id') && rpc.has_key?('method') && rpc['jsonrpc'].eql?('2.0')
205
+ return response_error(-32600, 'Invalid request', nil)
242
206
  end
207
+ @_jsonrpc_call = rpc
243
208
 
244
209
  begin
245
- Types.valid_result?(self.class, method, result)
210
+ if self.class.auth_with && ! @_jsonrpc_auth
211
+ (@_jsonrpc_auth = self.class.auth_with.client_check(@_jsonrpc_env, rpc)) or raise AuthFail, "Invalid credentials"
212
+ end
213
+
214
+ call(rpc['method'], rpc['id'], rpc['params'])
215
+ rescue AuthFail => e
216
+ response_error(-32000, 'AuthFail: Invalid credentials', {})
217
+ rescue APIFail => e
218
+ response_error(-32000, 'APIFail', {})
219
+ rescue KnownError => e
220
+ response_error(e.code, 'An error occurred', {})
246
221
  rescue Exception => e
247
- return response_error(-32602, "Invalid result - #{e.message}", {})
222
+ log_error("Internal error calling #{rpc.inspect} - #{e.class}: #{e.message} #{e.backtrace.join("\n ")}")
223
+ invoke_server_error_hook(e)
224
+ response_error(-32000, "An error occurred. Check logs for details", {})
248
225
  end
249
-
250
- response_ok(id, result)
251
- else
252
- response_error(-32601, "Unknown method `#{method.inspect}'", {})
253
226
  end
254
- end
255
227
 
256
- class << self
257
- # Store parameter in internal hash when building API
258
- def ___append_param name, type, options
259
- @params ||= []
260
- unless options.has_key?(:required)
261
- options[:required] = true
262
- end
263
- @params << options.merge({ :name => name, :type => type })
228
+ # List API methods
229
+ #
230
+ # @return [Array] List of api method names
231
+ def api_methods
232
+ public_methods(false).map(&:to_s) - ['rack_dispatch', 'dispatch']
264
233
  end
265
- private :___append_param
266
-
267
- # @!group DSL
268
234
 
269
- # Define a named parameter of type #type for next method
235
+ # Call method, checking param and return types
270
236
  #
271
- # @param [String] name parameter name
272
- # @param [String] type description of type see {Types}
273
- def param name, type, desc = nil, options = nil
274
- if options.nil? && desc.is_a?(Hash)
275
- options, desc = desc, nil
237
+ # @param [String] method Method name
238
+ # @param [Integer] id Method call ID - for response
239
+ # @param [Hash] params Method parameters
240
+ # @return [Hash] JSON response
241
+ def call(method, id, params)
242
+ if api_methods.include?(method)
243
+ begin
244
+ Types.valid_params?(self.class, method, params)
245
+ rescue Types::InvalidParamsError => e
246
+ return response_error(-32602, "Invalid params - #{e.message}", {})
247
+ end
248
+
249
+ if self.method(method).arity.zero?
250
+ result = send(method)
251
+ else
252
+ result = send(method, params)
253
+ end
254
+
255
+ Types.valid_result?(self.class, method, result)
256
+
257
+ response_ok(id, result)
258
+ else
259
+ response_error(-32601, "Unknown method `#{method.inspect}'", {})
276
260
  end
277
- options ||= {}
278
- options[:desc] = desc if desc.is_a?(String)
279
-
280
- ___append_param name, type, options
281
261
  end
282
262
 
283
- # Define an optional parameter for next method
284
- def optional name, type, desc = nil, options = nil
285
- if options.nil? && desc.is_a?(Hash)
286
- options, desc = desc, nil
263
+ class << self
264
+ # Store parameter in internal hash when building API
265
+ def ___append_param name, type, options
266
+ @params ||= []
267
+ unless options.has_key?(:required)
268
+ options[:required] = true
269
+ end
270
+ @params << options.merge({ :name => name, :type => type })
287
271
  end
288
- options ||= {}
289
- options[:desc] = desc if desc.is_a?(String)
272
+ private :___append_param
273
+
274
+ # @!group DSL
275
+ # Define a named parameter of type #type for next method
276
+ #
277
+ # @param [String] name parameter name
278
+ # @param [String] type description of type see {Types}
279
+ def param name, type, desc = nil, options = nil
280
+ if options.nil? && desc.is_a?(Hash)
281
+ options, desc = desc, nil
282
+ end
283
+ options ||= {}
284
+ options[:desc] = desc if desc.is_a?(String)
290
285
 
291
- ___append_param(name, type, options.merge(:required => false))
292
- end
286
+ ___append_param name, type, options
287
+ end
293
288
 
294
- # Define type of return value for next method
295
- def result type, desc = nil
296
- @result = { :type => type, :desc => desc }
297
- end
289
+ # Define an optional parameter for next method
290
+ def optional name, type, desc = nil, options = nil
291
+ if options.nil? && desc.is_a?(Hash)
292
+ options, desc = desc, nil
293
+ end
294
+ options ||= {}
295
+ options[:desc] = desc if desc.is_a?(String)
298
296
 
299
- # Set description for next method
300
- def desc str
301
- @desc = str
302
- end
297
+ ___append_param(name, type, options.merge(:required => false))
298
+ end
303
299
 
304
- # Add an example for next method
305
- def example desc, code
306
- @examples ||= []
307
- @examples << { :desc => desc, :code => code }
308
- end
300
+ # Define type of return value for next method
301
+ def result type, desc = nil
302
+ @result = { :type => type, :desc => desc }
303
+ end
309
304
 
310
- # Define a custom type
311
- def type name, *fields
312
- @types ||= {}
313
- type = JsonObjectType.new(name, fields)
305
+ # Set description for next method
306
+ def desc str
307
+ @desc = str
308
+ end
314
309
 
315
- if block_given?
316
- yield(type)
317
- end
310
+ # Add an example for next method
311
+ def example desc, code
312
+ @examples ||= []
313
+ @examples << { :desc => desc, :code => code }
314
+ end
318
315
 
319
- @types[name] = type
320
- end
316
+ # Define a custom type
317
+ def type name, *fields
318
+ @types ||= {}
319
+ type = JsonObjectType.new(name, fields)
321
320
 
322
- # Group methods
323
- def section name, summary=nil
324
- @sections ||= []
325
- @sections << {:name => name, :summary => summary}
321
+ if block_given?
322
+ yield(type)
323
+ end
326
324
 
327
- @current_section = name
328
- if block_given?
329
- yield
330
- @current_section = nil
331
- end
332
- end
325
+ @types[name] = type
326
+ end
333
327
 
334
- # Exclude next method from documentation
335
- def nodoc
336
- @nodoc = true
337
- end
328
+ # Group methods
329
+ def section name, summary=nil
330
+ @sections ||= []
331
+ @sections << {:name => name, :summary => summary}
338
332
 
339
- # Set interface title
340
- def title str = nil
341
- @title = str if str
342
- end
333
+ @current_section = name
334
+ if block_given?
335
+ yield
336
+ @current_section = nil
337
+ end
338
+ end
343
339
 
344
- # Sets introduction for interface
345
- def introduction str = nil
346
- @introduction = str if str
347
- end
340
+ # Exclude next method from documentation
341
+ def nodoc
342
+ @nodoc = true
343
+ end
348
344
 
349
- # @!endgroup
350
-
351
- # Catch methods added to class & store documentation
352
- def method_added(name)
353
- return if self == JSONRPC2::Interface
354
- @about ||= {}
355
- method = {}
356
- method[:params] = @params if @params
357
- method[:returns] = @result if @result
358
- method[:desc] = @desc if @desc
359
- method[:examples] = @examples if @examples
360
-
361
- if method.empty?
362
- if public_methods(false).include?(name)
363
- unless @nodoc
364
- #logger.info("#{name} has no API documentation... :(")
345
+ # Set interface title
346
+ def title str = nil
347
+ @title = str if str
348
+ end
349
+
350
+ # Sets introduction for interface
351
+ def introduction str = nil
352
+ @introduction = str if str
353
+ end
354
+ # @!endgroup
355
+
356
+ # Catch methods added to class & store documentation
357
+ def method_added(name)
358
+ return if self == JSONRPC2::Interface
359
+ @about ||= {}
360
+ method = {}
361
+ method[:params] = @params if @params
362
+ method[:returns] = @result if @result
363
+ method[:desc] = @desc if @desc
364
+ method[:examples] = @examples if @examples
365
+
366
+ if method.empty?
367
+ if public_methods(false).include?(name)
368
+ unless @nodoc
369
+ #logger.info("#{name} has no API documentation... :(")
370
+ end
371
+ else
372
+ #logger.debug("#{name} isn't public - so no API")
365
373
  end
366
374
  else
367
- #logger.debug("#{name} isn't public - so no API")
375
+ method[:name] = name
376
+ method[:section] = @current_section
377
+ method[:index] = @about.size
378
+ @about[name.to_s] = method
368
379
  end
369
- else
370
- method[:name] = name
371
- method[:section] = @current_section
372
- method[:index] = @about.size
373
- @about[name.to_s] = method
380
+
381
+ @result = nil
382
+ @params = nil
383
+ @desc = nil
384
+ @examples = nil
385
+ @nodoc = false
374
386
  end
387
+ private :method_added
388
+ attr_reader :about, :types
389
+ end
390
+
391
+ extend JSONRPC2::TextileEmitter
375
392
 
376
- @result = nil
377
- @params = nil
378
- @desc = nil
379
- @examples = nil
380
- @nodoc = false
393
+ private
394
+
395
+ def invoke_server_error_hook(error)
396
+ on_server_error(request_id: env['json.request-id'], error: error)
397
+ rescue => error
398
+ log_error("Server error hook failed - #{error.class}: #{error.message} #{error.backtrace.join("\n ")}")
381
399
  end
382
- private :method_added
383
- attr_reader :about, :types
384
400
 
385
- end
401
+ # Available for reimplementation by a subclass, noop by default
402
+ def on_server_error(request_id:, error:)
403
+ end
386
404
 
387
- extend JSONRPC2::TextileEmitter
388
- end
405
+ def log_error(message)
406
+ logger.error("#{env['json.request-id']} #{message}") if logger.respond_to?(:error)
407
+ end
408
+ end
389
409
  end