sr-jimson 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|