jsonrpc2 0.1.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/test.yml +26 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +42 -0
- data/Dockerfile +8 -0
- data/README.markdown +36 -56
- data/bin/run-example +7 -0
- data/bin/setup +3 -0
- data/bin/test +7 -0
- data/example/config.ru +27 -25
- data/{bin → exe}/jsonrpc2 +0 -0
- data/jsonrpc2.gemspec +15 -3
- data/lib/jsonrpc2/html.rb +3 -3
- data/lib/jsonrpc2/interface.rb +319 -289
- data/lib/jsonrpc2/textile.rb +5 -5
- data/lib/jsonrpc2/types.rb +14 -10
- data/lib/jsonrpc2/version.rb +1 -1
- data/spec/lib/interface_spec.rb +208 -0
- data/spec/spec_helper.rb +94 -0
- metadata +133 -39
data/lib/jsonrpc2/interface.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
#
|
56
|
-
|
57
|
-
# @!group Rack-related
|
58
|
-
|
59
|
-
# Rack compatible call handler
|
119
|
+
# Create new interface object
|
60
120
|
#
|
61
|
-
# @param [Hash]
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
150
|
+
protected
|
114
151
|
|
115
|
-
|
152
|
+
# JSON result helper
|
153
|
+
def response_ok(id, result)
|
154
|
+
{ 'jsonrpc' => '2.0', 'result' => result, 'id' => id }
|
155
|
+
end
|
116
156
|
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
133
|
-
|
134
|
-
|
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
|
-
|
145
|
-
|
167
|
+
# Auth info
|
168
|
+
def auth
|
169
|
+
@_jsonrpc_auth
|
170
|
+
end
|
146
171
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
173
|
-
|
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
|
-
|
191
|
-
|
192
|
-
|
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
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
-
|
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
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
257
|
-
#
|
258
|
-
|
259
|
-
|
260
|
-
|
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
|
-
#
|
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]
|
272
|
-
# @param [
|
273
|
-
|
274
|
-
|
275
|
-
|
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
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
-
|
289
|
-
|
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
|
-
|
292
|
-
|
287
|
+
___append_param name, type, options
|
288
|
+
end
|
293
289
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
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
|
-
|
300
|
-
|
301
|
-
@desc = str
|
302
|
-
end
|
298
|
+
___append_param(name, type, options.merge(:required => false))
|
299
|
+
end
|
303
300
|
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
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
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
306
|
+
# Set description for next method
|
307
|
+
def desc str
|
308
|
+
@desc = str
|
309
|
+
end
|
314
310
|
|
315
|
-
|
316
|
-
|
317
|
-
|
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
|
-
|
320
|
-
|
317
|
+
# Define a custom type
|
318
|
+
def type name, *fields
|
319
|
+
@types ||= {}
|
320
|
+
type = JsonObjectType.new(name, fields)
|
321
321
|
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
@sections << {:name => name, :summary => summary}
|
322
|
+
if block_given?
|
323
|
+
yield(type)
|
324
|
+
end
|
326
325
|
|
327
|
-
|
328
|
-
|
329
|
-
yield
|
330
|
-
@current_section = nil
|
331
|
-
end
|
332
|
-
end
|
326
|
+
@types[name] = type
|
327
|
+
end
|
333
328
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
329
|
+
# Group methods
|
330
|
+
def section name, summary=nil
|
331
|
+
@sections ||= []
|
332
|
+
@sections << {:name => name, :summary => summary}
|
338
333
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
334
|
+
@current_section = name
|
335
|
+
if block_given?
|
336
|
+
yield
|
337
|
+
@current_section = nil
|
338
|
+
end
|
339
|
+
end
|
343
340
|
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
341
|
+
# Exclude next method from documentation
|
342
|
+
def nodoc
|
343
|
+
@nodoc = true
|
344
|
+
end
|
348
345
|
|
349
|
-
#
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
if
|
363
|
-
|
364
|
-
|
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
|
-
|
376
|
+
method[:name] = name
|
377
|
+
method[:section] = @current_section
|
378
|
+
method[:index] = @about.size
|
379
|
+
@about[name.to_s] = method
|
368
380
|
end
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
@
|
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
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
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
|
-
|
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
|
-
|
388
|
-
|
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
|