jsonrpc2 0.1.1 → 0.2.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.
- checksums.yaml +5 -5
- data/.github/workflows/test.yml +26 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +38 -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 +309 -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 +162 -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,369 @@ 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
|
-
|
267
|
-
# @!group DSL
|
268
234
|
|
269
|
-
#
|
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
|
+
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
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
-
|
289
|
-
|
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
|
-
|
292
|
-
|
286
|
+
___append_param name, type, options
|
287
|
+
end
|
293
288
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
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
|
-
|
300
|
-
|
301
|
-
@desc = str
|
302
|
-
end
|
297
|
+
___append_param(name, type, options.merge(:required => false))
|
298
|
+
end
|
303
299
|
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
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
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
305
|
+
# Set description for next method
|
306
|
+
def desc str
|
307
|
+
@desc = str
|
308
|
+
end
|
314
309
|
|
315
|
-
|
316
|
-
|
317
|
-
|
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
|
-
|
320
|
-
|
316
|
+
# Define a custom type
|
317
|
+
def type name, *fields
|
318
|
+
@types ||= {}
|
319
|
+
type = JsonObjectType.new(name, fields)
|
321
320
|
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
@sections << {:name => name, :summary => summary}
|
321
|
+
if block_given?
|
322
|
+
yield(type)
|
323
|
+
end
|
326
324
|
|
327
|
-
|
328
|
-
|
329
|
-
yield
|
330
|
-
@current_section = nil
|
331
|
-
end
|
332
|
-
end
|
325
|
+
@types[name] = type
|
326
|
+
end
|
333
327
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
328
|
+
# Group methods
|
329
|
+
def section name, summary=nil
|
330
|
+
@sections ||= []
|
331
|
+
@sections << {:name => name, :summary => summary}
|
338
332
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
333
|
+
@current_section = name
|
334
|
+
if block_given?
|
335
|
+
yield
|
336
|
+
@current_section = nil
|
337
|
+
end
|
338
|
+
end
|
343
339
|
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
340
|
+
# Exclude next method from documentation
|
341
|
+
def nodoc
|
342
|
+
@nodoc = true
|
343
|
+
end
|
348
344
|
|
349
|
-
#
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
if
|
363
|
-
|
364
|
-
|
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
|
-
|
375
|
+
method[:name] = name
|
376
|
+
method[:section] = @current_section
|
377
|
+
method[:index] = @about.size
|
378
|
+
@about[name.to_s] = method
|
368
379
|
end
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
@
|
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
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
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
|
-
|
401
|
+
# Available for reimplementation by a subclass, noop by default
|
402
|
+
def on_server_error(request_id:, error:)
|
403
|
+
end
|
386
404
|
|
387
|
-
|
388
|
-
|
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
|