jsonrpc2 0.1.1 → 0.3.0

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.
@@ -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