sr-jimson 0.11.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 +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.rdoc +92 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +17 -0
- data/README.md +29 -0
- data/Rakefile +21 -0
- data/VERSION +1 -0
- data/lib/sr/jimson.rb +10 -0
- data/lib/sr/jimson/blankslate.rb +132 -0
- data/lib/sr/jimson/client.rb +180 -0
- data/lib/sr/jimson/client/error.rb +23 -0
- data/lib/sr/jimson/handler.rb +25 -0
- data/lib/sr/jimson/request.rb +25 -0
- data/lib/sr/jimson/response.rb +30 -0
- data/lib/sr/jimson/router.rb +25 -0
- data/lib/sr/jimson/router/map.rb +75 -0
- data/lib/sr/jimson/server.rb +224 -0
- data/lib/sr/jimson/server/error.rb +66 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/sr/jimson/client_spec.rb +191 -0
- data/spec/sr/jimson/handler_spec.rb +61 -0
- data/spec/sr/jimson/router_spec.rb +75 -0
- data/spec/sr/jimson/server_spec.rb +466 -0
- data/spec/sr/jimson_spec.rb +7 -0
- data/sr-jimson.gemspec +31 -0
- metadata +206 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
module Sr::Jimson
|
2
|
+
class Client
|
3
|
+
module Error
|
4
|
+
class InvalidResponse < StandardError
|
5
|
+
def initialize()
|
6
|
+
super('Invalid or empty response from server.')
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class InvalidJSON < StandardError
|
11
|
+
def initialize(json)
|
12
|
+
super("Couldn't parse JSON string received from server:\n#{json}")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ServerError < StandardError
|
17
|
+
def initialize(code, message)
|
18
|
+
super("Server error #{code}: #{message}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Sr::Jimson
|
2
|
+
module Handler
|
3
|
+
|
4
|
+
def jimson_default_methods
|
5
|
+
self.instance_methods.map(&:to_s) - Object.methods.map(&:to_s)
|
6
|
+
end
|
7
|
+
|
8
|
+
def jimson_expose(*methods)
|
9
|
+
@jimson_exposed_methods ||= []
|
10
|
+
@jimson_exposed_methods += methods.map(&:to_s)
|
11
|
+
end
|
12
|
+
|
13
|
+
def jimson_exclude(*methods)
|
14
|
+
@jimson_excluded_methods ||= []
|
15
|
+
@jimson_excluded_methods += methods.map(&:to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
def jimson_exposed_methods
|
19
|
+
@jimson_exposed_methods ||= []
|
20
|
+
@jimson_excluded_methods ||= []
|
21
|
+
(jimson_default_methods - @jimson_excluded_methods + @jimson_exposed_methods).sort
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Sr::Jimson
|
2
|
+
class Request
|
3
|
+
|
4
|
+
attr_accessor :method, :params, :id
|
5
|
+
def initialize(method, params, id = nil)
|
6
|
+
@method = method
|
7
|
+
@params = params
|
8
|
+
@id = id
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_h
|
12
|
+
h = {
|
13
|
+
'jsonrpc' => '2.0',
|
14
|
+
'method' => @method
|
15
|
+
}
|
16
|
+
h.merge!('params' => @params) if !!@params && !params.empty?
|
17
|
+
h.merge!('id' => id)
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_json(*a)
|
21
|
+
MultiJson.encode(self.to_h)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Sr::Jimson
|
2
|
+
class Response
|
3
|
+
attr_accessor :result, :error, :id
|
4
|
+
|
5
|
+
def initialize(id)
|
6
|
+
@id = id
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_h
|
10
|
+
h = {'jsonrpc' => '2.0'}
|
11
|
+
h.merge!('result' => @result) if !!@result
|
12
|
+
h.merge!('error' => @error) if !!@error
|
13
|
+
h.merge!('id' => @id)
|
14
|
+
end
|
15
|
+
|
16
|
+
def is_error?
|
17
|
+
!!@error
|
18
|
+
end
|
19
|
+
|
20
|
+
def succeeded?
|
21
|
+
!!@result
|
22
|
+
end
|
23
|
+
|
24
|
+
def populate!(data)
|
25
|
+
@error = data['error'] if !!data['error']
|
26
|
+
@result = data['result'] if !!data['result']
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'sr/jimson/router/map'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Sr::Jimson
|
5
|
+
class Router
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
ROUTER_DELEGATORS = [
|
9
|
+
:handler_for_method, :root, :namespace,
|
10
|
+
:jimson_methods, :strip_method_namespace
|
11
|
+
]
|
12
|
+
|
13
|
+
def_delegators :@map, *ROUTER_DELEGATORS
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@map = Map.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def draw(&block)
|
20
|
+
@map.instance_eval(&block)
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Sr::Jimson
|
2
|
+
class Router
|
3
|
+
|
4
|
+
#
|
5
|
+
# Provides a DSL for routing method namespaces to handlers.
|
6
|
+
# Only handles root-level and non-nested namespaces, e.g. 'foo.bar' or 'foo'.
|
7
|
+
#
|
8
|
+
class Map
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@routes = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Set the root handler, i.e. the handler used for a bare method like 'foo'
|
16
|
+
#
|
17
|
+
def root(handler)
|
18
|
+
handler = handler.new if handler.is_a?(Class)
|
19
|
+
@routes[''] = handler
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Define the handler for a namespace
|
24
|
+
#
|
25
|
+
def namespace(ns, handler = nil, &block)
|
26
|
+
if !!handler
|
27
|
+
handler = handler.new if handler.is_a?(Class)
|
28
|
+
@routes[ns.to_s] = handler
|
29
|
+
else
|
30
|
+
# passed a block for nested namespacing
|
31
|
+
map = Sr::Jimson::Router::Map.new
|
32
|
+
@routes[ns.to_s] = map
|
33
|
+
map.instance_eval(&block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Return the handler for a (possibly namespaced) method name
|
39
|
+
#
|
40
|
+
def handler_for_method(method)
|
41
|
+
parts = method.split('.')
|
42
|
+
ns = (method.index('.') == nil ? '' : parts.first)
|
43
|
+
handler = @routes[ns]
|
44
|
+
if handler.is_a?(Sr::Jimson::Router::Map)
|
45
|
+
return handler.handler_for_method(parts[1..-1].join('.'))
|
46
|
+
end
|
47
|
+
handler
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Strip off the namespace part of a method and return the bare method name
|
52
|
+
#
|
53
|
+
def strip_method_namespace(method)
|
54
|
+
method.split('.').last
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Return an array of all methods on handlers in the map, fully namespaced
|
59
|
+
#
|
60
|
+
def jimson_methods
|
61
|
+
arr = @routes.keys.map do |ns|
|
62
|
+
prefix = (ns == '' ? '' : "#{ns}.")
|
63
|
+
handler = @routes[ns]
|
64
|
+
if handler.is_a?(Sr::Jimson::Router::Map)
|
65
|
+
handler.jimson_methods
|
66
|
+
else
|
67
|
+
handler.class.jimson_exposed_methods.map { |method| prefix + method }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
arr.flatten
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'rack/request'
|
3
|
+
require 'rack/response'
|
4
|
+
require 'multi_json'
|
5
|
+
require 'sr/jimson/handler'
|
6
|
+
require 'sr/jimson/router'
|
7
|
+
require 'sr/jimson/server/error'
|
8
|
+
|
9
|
+
module Sr::Jimson
|
10
|
+
class Server
|
11
|
+
|
12
|
+
class System
|
13
|
+
extend Handler
|
14
|
+
|
15
|
+
def initialize(router)
|
16
|
+
@router = router
|
17
|
+
end
|
18
|
+
|
19
|
+
def listMethods
|
20
|
+
@router.jimson_methods
|
21
|
+
end
|
22
|
+
|
23
|
+
def isAlive
|
24
|
+
true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
JSON_RPC_VERSION = '2.0'
|
29
|
+
|
30
|
+
attr_accessor :router, :host, :port, :show_errors, :opts
|
31
|
+
|
32
|
+
#
|
33
|
+
# Create a Server with routes defined
|
34
|
+
#
|
35
|
+
def self.with_routes(opts = {}, &block)
|
36
|
+
router = Router.new
|
37
|
+
router.send(:draw, &block)
|
38
|
+
self.new(router, opts)
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# +router_or_handler+ is an instance of Sr::Jimson::Router or extends Sr::Jimson::Handler
|
43
|
+
#
|
44
|
+
# +opts+ may include:
|
45
|
+
# * :host - the hostname or ip to bind to
|
46
|
+
# * :port - the port to listen on
|
47
|
+
# * :server - the rack handler to use, e.g. 'webrick' or 'thin'
|
48
|
+
# * :show_errors - true or false, send backtraces in error responses?
|
49
|
+
#
|
50
|
+
# Remaining options are forwarded to the underlying Rack server.
|
51
|
+
#
|
52
|
+
def initialize(router_or_handler, opts = {})
|
53
|
+
if !router_or_handler.is_a?(Router)
|
54
|
+
# arg is a handler, wrap it in a Router
|
55
|
+
@router = Router.new
|
56
|
+
@router.root router_or_handler
|
57
|
+
else
|
58
|
+
# arg is a router
|
59
|
+
@router = router_or_handler
|
60
|
+
end
|
61
|
+
@router.namespace 'system', System.new(@router)
|
62
|
+
|
63
|
+
@host = opts.delete(:host) || '0.0.0.0'
|
64
|
+
@port = opts.delete(:port) || 8999
|
65
|
+
@show_errors = opts.delete(:show_errors) || false
|
66
|
+
@opts = opts
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Starts the server so it can process requests
|
71
|
+
#
|
72
|
+
def start
|
73
|
+
Rack::Server.start(opts.merge(
|
74
|
+
:app => self,
|
75
|
+
:Host => @host,
|
76
|
+
:Port => @port
|
77
|
+
))
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Entry point for Rack
|
82
|
+
#
|
83
|
+
def call(env)
|
84
|
+
req = Rack::Request.new(env)
|
85
|
+
resp = Rack::Response.new
|
86
|
+
return resp.finish if !req.post?
|
87
|
+
resp.write process(req.body.read)
|
88
|
+
resp.finish
|
89
|
+
end
|
90
|
+
|
91
|
+
def process(content)
|
92
|
+
begin
|
93
|
+
request = parse_request(content)
|
94
|
+
if request.is_a?(Array)
|
95
|
+
raise Server::Error::InvalidRequest.new if request.empty?
|
96
|
+
response = request.map { |req| handle_request(req) }
|
97
|
+
else
|
98
|
+
response = handle_request(request)
|
99
|
+
end
|
100
|
+
rescue Server::Error::ParseError, Server::Error::InvalidRequest => e
|
101
|
+
response = error_response(e)
|
102
|
+
rescue Server::Error => e
|
103
|
+
response = error_response(e, request)
|
104
|
+
rescue StandardError, Exception => e
|
105
|
+
response = error_response(Server::Error::InternalError.new(e))
|
106
|
+
end
|
107
|
+
|
108
|
+
response.compact! if response.is_a?(Array)
|
109
|
+
|
110
|
+
return nil if response.nil? || (response.respond_to?(:empty?) && response.empty?)
|
111
|
+
|
112
|
+
MultiJson.encode(response)
|
113
|
+
end
|
114
|
+
|
115
|
+
def handle_request(request)
|
116
|
+
response = nil
|
117
|
+
begin
|
118
|
+
if !validate_request(request)
|
119
|
+
response = error_response(Server::Error::InvalidRequest.new)
|
120
|
+
else
|
121
|
+
response = create_response(request)
|
122
|
+
end
|
123
|
+
rescue Server::Error => e
|
124
|
+
response = error_response(e, request)
|
125
|
+
end
|
126
|
+
|
127
|
+
response
|
128
|
+
end
|
129
|
+
|
130
|
+
def validate_request(request)
|
131
|
+
required_keys = %w(jsonrpc method)
|
132
|
+
required_types = {
|
133
|
+
'jsonrpc' => [String],
|
134
|
+
'method' => [String],
|
135
|
+
'params' => [Hash, Array],
|
136
|
+
'id' => [String, Fixnum, Bignum, NilClass]
|
137
|
+
}
|
138
|
+
|
139
|
+
return false if !request.is_a?(Hash)
|
140
|
+
|
141
|
+
required_keys.each do |key|
|
142
|
+
return false if !request.has_key?(key)
|
143
|
+
end
|
144
|
+
|
145
|
+
required_types.each do |key, types|
|
146
|
+
return false if request.has_key?(key) && !types.any? { |type| request[key].is_a?(type) }
|
147
|
+
end
|
148
|
+
|
149
|
+
return false if request['jsonrpc'] != JSON_RPC_VERSION
|
150
|
+
|
151
|
+
true
|
152
|
+
end
|
153
|
+
|
154
|
+
def create_response(request)
|
155
|
+
method = request['method']
|
156
|
+
params = request['params']
|
157
|
+
result = dispatch_request(method, params)
|
158
|
+
|
159
|
+
response = success_response(request, result)
|
160
|
+
|
161
|
+
# A Notification is a Request object without an "id" member.
|
162
|
+
# The Server MUST NOT reply to a Notification, including those
|
163
|
+
# that are within a batch request.
|
164
|
+
response = nil if !request.has_key?('id')
|
165
|
+
|
166
|
+
return response
|
167
|
+
|
168
|
+
rescue Server::Error => e
|
169
|
+
raise e
|
170
|
+
rescue ArgumentError
|
171
|
+
raise Server::Error::InvalidParams.new
|
172
|
+
rescue Exception, StandardError => e
|
173
|
+
raise Server::Error::ApplicationError.new(e, @show_errors)
|
174
|
+
end
|
175
|
+
|
176
|
+
def dispatch_request(method, params)
|
177
|
+
method_name = method.to_s
|
178
|
+
handler = @router.handler_for_method(method_name)
|
179
|
+
method_name = @router.strip_method_namespace(method_name)
|
180
|
+
|
181
|
+
if handler.nil? \
|
182
|
+
|| !handler.class.jimson_exposed_methods.include?(method_name) \
|
183
|
+
|| !handler.respond_to?(method_name)
|
184
|
+
raise Server::Error::MethodNotFound.new(method)
|
185
|
+
end
|
186
|
+
|
187
|
+
if params.nil?
|
188
|
+
return handler.send(method_name)
|
189
|
+
elsif params.is_a?(Hash)
|
190
|
+
return handler.send(method_name, params)
|
191
|
+
else
|
192
|
+
return handler.send(method_name, *params)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def error_response(error, request = nil)
|
197
|
+
resp = {
|
198
|
+
'jsonrpc' => JSON_RPC_VERSION,
|
199
|
+
'error' => error.to_h,
|
200
|
+
}
|
201
|
+
if !!request && request.has_key?('id')
|
202
|
+
resp['id'] = request['id']
|
203
|
+
else
|
204
|
+
resp['id'] = nil
|
205
|
+
end
|
206
|
+
|
207
|
+
resp
|
208
|
+
end
|
209
|
+
|
210
|
+
def success_response(request, result)
|
211
|
+
{
|
212
|
+
'jsonrpc' => JSON_RPC_VERSION,
|
213
|
+
'result' => result,
|
214
|
+
'id' => request['id']
|
215
|
+
}
|
216
|
+
end
|
217
|
+
|
218
|
+
def parse_request(post)
|
219
|
+
MultiJson.decode(post)
|
220
|
+
rescue
|
221
|
+
raise Server::Error::ParseError.new
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Sr::Jimson
|
2
|
+
class Server
|
3
|
+
class Error < StandardError
|
4
|
+
attr_accessor :code, :message
|
5
|
+
|
6
|
+
def initialize(code, message)
|
7
|
+
@code = code
|
8
|
+
@message = message
|
9
|
+
super(message)
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_h
|
13
|
+
{
|
14
|
+
'code' => @code,
|
15
|
+
'message' => @message
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
class ParseError < Error
|
20
|
+
def initialize
|
21
|
+
super(-32700, 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class InvalidRequest < Error
|
26
|
+
def initialize
|
27
|
+
super(-32600, 'The JSON sent is not a valid Request object.')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class MethodNotFound < Error
|
32
|
+
def initialize(method)
|
33
|
+
super(-32601, "Method '#{method}' not found.")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class InvalidParams < Error
|
38
|
+
def initialize
|
39
|
+
super(-32602, 'Invalid method parameter(s).')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class InternalError < Error
|
44
|
+
def initialize(e)
|
45
|
+
super(-32603, "Internal server error: #{e}")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class ApplicationError < Error
|
50
|
+
def initialize(err, show_error = false)
|
51
|
+
msg = "Server application error"
|
52
|
+
msg += ': ' + err.message + ' at ' + err.backtrace.first if show_error
|
53
|
+
super(-32099, msg)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
CODES = {
|
58
|
+
-32700 => ParseError,
|
59
|
+
-32600 => InvalidRequest,
|
60
|
+
-32601 => MethodNotFound,
|
61
|
+
-32602 => InvalidParams,
|
62
|
+
-32603 => InternalError
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|