jsonrpc2 0.1.1 → 0.3.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,379 @@ 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
234
 
267
- # @!group DSL
268
-
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
+ invoke_before_validation_hook(method, id, params)
243
+ if api_methods.include?(method)
244
+ begin
245
+ Types.valid_params?(self.class, method, params)
246
+ rescue Types::InvalidParamsError => e
247
+ return response_error(-32602, "Invalid params - #{e.message}", {})
248
+ end
249
+
250
+ if self.method(method).arity.zero?
251
+ result = send(method)
252
+ else
253
+ result = send(method, params)
254
+ end
255
+
256
+ Types.valid_result?(self.class, method, result)
257
+
258
+ response_ok(id, result)
259
+ else
260
+ response_error(-32601, "Unknown method `#{method.inspect}'", {})
276
261
  end
277
- options ||= {}
278
- options[:desc] = desc if desc.is_a?(String)
279
-
280
- ___append_param name, type, options
281
262
  end
282
263
 
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
264
+ class << self
265
+ # Store parameter in internal hash when building API
266
+ def ___append_param name, type, options
267
+ @params ||= []
268
+ unless options.has_key?(:required)
269
+ options[:required] = true
270
+ end
271
+ @params << options.merge({ :name => name, :type => type })
287
272
  end
288
- options ||= {}
289
- options[:desc] = desc if desc.is_a?(String)
273
+ private :___append_param
274
+
275
+ # @!group DSL
276
+ # Define a named parameter of type #type for next method
277
+ #
278
+ # @param [String] name parameter name
279
+ # @param [String] type description of type see {Types}
280
+ def param name, type, desc = nil, options = nil
281
+ if options.nil? && desc.is_a?(Hash)
282
+ options, desc = desc, nil
283
+ end
284
+ options ||= {}
285
+ options[:desc] = desc if desc.is_a?(String)
290
286
 
291
- ___append_param(name, type, options.merge(:required => false))
292
- end
287
+ ___append_param name, type, options
288
+ end
293
289
 
294
- # Define type of return value for next method
295
- def result type, desc = nil
296
- @result = { :type => type, :desc => desc }
297
- end
290
+ # Define an optional parameter for next method
291
+ def optional name, type, desc = nil, options = nil
292
+ if options.nil? && desc.is_a?(Hash)
293
+ options, desc = desc, nil
294
+ end
295
+ options ||= {}
296
+ options[:desc] = desc if desc.is_a?(String)
298
297
 
299
- # Set description for next method
300
- def desc str
301
- @desc = str
302
- end
298
+ ___append_param(name, type, options.merge(:required => false))
299
+ end
303
300
 
304
- # Add an example for next method
305
- def example desc, code
306
- @examples ||= []
307
- @examples << { :desc => desc, :code => code }
308
- end
301
+ # Define type of return value for next method
302
+ def result type, desc = nil
303
+ @result = { :type => type, :desc => desc }
304
+ end
309
305
 
310
- # Define a custom type
311
- def type name, *fields
312
- @types ||= {}
313
- type = JsonObjectType.new(name, fields)
306
+ # Set description for next method
307
+ def desc str
308
+ @desc = str
309
+ end
314
310
 
315
- if block_given?
316
- yield(type)
317
- end
311
+ # Add an example for next method
312
+ def example desc, code
313
+ @examples ||= []
314
+ @examples << { :desc => desc, :code => code }
315
+ end
318
316
 
319
- @types[name] = type
320
- end
317
+ # Define a custom type
318
+ def type name, *fields
319
+ @types ||= {}
320
+ type = JsonObjectType.new(name, fields)
321
321
 
322
- # Group methods
323
- def section name, summary=nil
324
- @sections ||= []
325
- @sections << {:name => name, :summary => summary}
322
+ if block_given?
323
+ yield(type)
324
+ end
326
325
 
327
- @current_section = name
328
- if block_given?
329
- yield
330
- @current_section = nil
331
- end
332
- end
326
+ @types[name] = type
327
+ end
333
328
 
334
- # Exclude next method from documentation
335
- def nodoc
336
- @nodoc = true
337
- end
329
+ # Group methods
330
+ def section name, summary=nil
331
+ @sections ||= []
332
+ @sections << {:name => name, :summary => summary}
338
333
 
339
- # Set interface title
340
- def title str = nil
341
- @title = str if str
342
- end
334
+ @current_section = name
335
+ if block_given?
336
+ yield
337
+ @current_section = nil
338
+ end
339
+ end
343
340
 
344
- # Sets introduction for interface
345
- def introduction str = nil
346
- @introduction = str if str
347
- end
341
+ # Exclude next method from documentation
342
+ def nodoc
343
+ @nodoc = true
344
+ end
348
345
 
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... :(")
346
+ # Set interface title
347
+ def title str = nil
348
+ @title = str if str
349
+ end
350
+
351
+ # Sets introduction for interface
352
+ def introduction str = nil
353
+ @introduction = str if str
354
+ end
355
+ # @!endgroup
356
+
357
+ # Catch methods added to class & store documentation
358
+ def method_added(name)
359
+ return if self == JSONRPC2::Interface
360
+ @about ||= {}
361
+ method = {}
362
+ method[:params] = @params if @params
363
+ method[:returns] = @result if @result
364
+ method[:desc] = @desc if @desc
365
+ method[:examples] = @examples if @examples
366
+
367
+ if method.empty?
368
+ if public_methods(false).include?(name)
369
+ unless @nodoc
370
+ #logger.info("#{name} has no API documentation... :(")
371
+ end
372
+ else
373
+ #logger.debug("#{name} isn't public - so no API")
365
374
  end
366
375
  else
367
- #logger.debug("#{name} isn't public - so no API")
376
+ method[:name] = name
377
+ method[:section] = @current_section
378
+ method[:index] = @about.size
379
+ @about[name.to_s] = method
368
380
  end
369
- else
370
- method[:name] = name
371
- method[:section] = @current_section
372
- method[:index] = @about.size
373
- @about[name.to_s] = method
381
+
382
+ @result = nil
383
+ @params = nil
384
+ @desc = nil
385
+ @examples = nil
386
+ @nodoc = false
374
387
  end
388
+ private :method_added
389
+ attr_reader :about, :types
390
+ end
375
391
 
376
- @result = nil
377
- @params = nil
378
- @desc = nil
379
- @examples = nil
380
- @nodoc = false
392
+ extend JSONRPC2::TextileEmitter
393
+
394
+ private
395
+
396
+ def invoke_server_error_hook(error)
397
+ on_server_error(request_id: env['json.request-id'], error: error)
398
+ rescue => error
399
+ log_error("Server error hook failed - #{error.class}: #{error.message} #{error.backtrace.join("\n ")}")
381
400
  end
382
- private :method_added
383
- attr_reader :about, :types
384
401
 
385
- end
402
+ def invoke_before_validation_hook(method, id, params)
403
+ before_validation(method: method, id: id, params: params)
404
+ rescue => error
405
+ log_error("Before validation hook failed - #{error.class}: #{error.message} #{error.backtrace.join("\n ")}")
406
+ end
386
407
 
387
- extend JSONRPC2::TextileEmitter
388
- end
408
+ # Available for reimplementation by a subclass, noop by default
409
+ def on_server_error(request_id:, error:)
410
+ end
411
+
412
+ def before_validation(method:, id:, params:)
413
+ end
414
+
415
+ def log_error(message)
416
+ logger.error("#{env['json.request-id']} #{message}") if logger.respond_to?(:error)
417
+ end
418
+ end
389
419
  end