jimson-temp 0.9.2

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.
@@ -0,0 +1,24 @@
1
+ require 'jimson/router/map'
2
+ require 'forwardable'
3
+
4
+ module Jimson
5
+ class Router
6
+ extend Forwardable
7
+
8
+ def_delegators :@map, :handler_for_method,
9
+ :root,
10
+ :namespace,
11
+ :jimson_methods,
12
+ :strip_method_namespace
13
+
14
+ def initialize
15
+ @map = Map.new
16
+ end
17
+
18
+ def draw(&block)
19
+ @map.instance_eval &block
20
+ self
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,75 @@
1
+ module 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 = 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?(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?(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,225 @@
1
+ require 'rack'
2
+ require 'rack/request'
3
+ require 'rack/response'
4
+ require 'multi_json'
5
+ require 'jimson/handler'
6
+ require 'jimson/router'
7
+ require 'jimson/server/error'
8
+
9
+ module 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 Jimson::Router or extends 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
+ data = MultiJson.decode(post)
220
+ rescue
221
+ raise Server::Error::ParseError.new
222
+ end
223
+
224
+ end
225
+ end
@@ -0,0 +1,66 @@
1
+ module 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
@@ -0,0 +1,191 @@
1
+ require 'spec_helper'
2
+
3
+ module Jimson
4
+ describe Client do
5
+ BOILERPLATE = {'jsonrpc' => '2.0', 'id' => 1}
6
+
7
+ before(:each) do
8
+ @resp_mock = mock('http_response')
9
+ ClientHelper.stub!(:make_id).and_return(1)
10
+ end
11
+
12
+ after(:each) do
13
+ end
14
+
15
+ describe "hidden methods" do
16
+ it "should reveal inspect" do
17
+ Client.new(SPEC_URL).inspect.should match /Jimson::Client/
18
+ end
19
+
20
+ it "should reveal to_s" do
21
+ Client.new(SPEC_URL).to_s.should match /Jimson::Client/
22
+ end
23
+ end
24
+
25
+ describe "#[]" do
26
+ before(:each) do
27
+ @client = Client.new(SPEC_URL)
28
+ end
29
+
30
+ context "when using a symbol to specify a namespace" do
31
+ it "sends the method prefixed with the namespace in the request" do
32
+ expected = MultiJson.encode({
33
+ 'jsonrpc' => '2.0',
34
+ 'method' => 'foo.sum',
35
+ 'params' => [1,2,3],
36
+ 'id' => 1
37
+ })
38
+ response = MultiJson.encode(BOILERPLATE.merge({'result' => 42}))
39
+ RestClient.should_receive(:post).with(SPEC_URL, expected, {:content_type => 'application/json'}).and_return(@resp_mock)
40
+ @resp_mock.should_receive(:body).at_least(:once).and_return(response)
41
+ @client[:foo].sum(1, 2, 3).should == 42
42
+ end
43
+
44
+ context "when the namespace is nested" do
45
+ it "sends the method prefixed with the full namespace in the request" do
46
+ expected = MultiJson.encode({
47
+ 'jsonrpc' => '2.0',
48
+ 'method' => 'foo.bar.sum',
49
+ 'params' => [1,2,3],
50
+ 'id' => 1
51
+ })
52
+ response = MultiJson.encode(BOILERPLATE.merge({'result' => 42}))
53
+ RestClient.should_receive(:post).with(SPEC_URL, expected, {:content_type => 'application/json'}).and_return(@resp_mock)
54
+ @resp_mock.should_receive(:body).at_least(:once).and_return(response)
55
+ @client[:foo][:bar].sum(1, 2, 3).should == 42
56
+ end
57
+ end
58
+ end
59
+
60
+ context "when sending positional arguments" do
61
+ it "sends a request with the correct method and args" do
62
+ expected = MultiJson.encode({
63
+ 'jsonrpc' => '2.0',
64
+ 'method' => 'foo',
65
+ 'params' => [1,2,3],
66
+ 'id' => 1
67
+ })
68
+ response = MultiJson.encode(BOILERPLATE.merge({'result' => 42}))
69
+ RestClient.should_receive(:post).with(SPEC_URL, expected, {:content_type => 'application/json'}).and_return(@resp_mock)
70
+ @resp_mock.should_receive(:body).at_least(:once).and_return(response)
71
+ @client['foo', 1, 2, 3].should == 42
72
+ end
73
+
74
+ context "when one of the args is an array" do
75
+ it "sends a request with the correct method and args" do
76
+ expected = MultiJson.encode({
77
+ 'jsonrpc' => '2.0',
78
+ 'method' => 'foo',
79
+ 'params' => [[1,2],3],
80
+ 'id' => 1
81
+ })
82
+ response = MultiJson.encode(BOILERPLATE.merge({'result' => 42}))
83
+ RestClient.should_receive(:post).with(SPEC_URL, expected, {:content_type => 'application/json'}).and_return(@resp_mock)
84
+ @resp_mock.should_receive(:body).at_least(:once).and_return(response)
85
+ @client['foo', [1, 2], 3].should == 42
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ describe "sending a single request" do
92
+ context "when using positional parameters" do
93
+ before(:each) do
94
+ @expected = MultiJson.encode({
95
+ 'jsonrpc' => '2.0',
96
+ 'method' => 'foo',
97
+ 'params' => [1,2,3],
98
+ 'id' => 1
99
+ })
100
+ end
101
+ it "sends a valid JSON-RPC request and returns the result" do
102
+ response = MultiJson.encode(BOILERPLATE.merge({'result' => 42}))
103
+ RestClient.should_receive(:post).with(SPEC_URL, @expected, {:content_type => 'application/json'}).and_return(@resp_mock)
104
+ @resp_mock.should_receive(:body).at_least(:once).and_return(response)
105
+ client = Client.new(SPEC_URL)
106
+ client.foo(1,2,3).should == 42
107
+ end
108
+
109
+ it "sends a valid JSON-RPC request with custom options" do
110
+ response = MultiJson.encode(BOILERPLATE.merge({'result' => 42}))
111
+ RestClient.should_receive(:post).with(SPEC_URL, @expected, {:content_type => 'application/json', :timeout => 10000}).and_return(@resp_mock)
112
+ @resp_mock.should_receive(:body).at_least(:once).and_return(response)
113
+ client = Client.new(SPEC_URL, :timeout => 10000)
114
+ client.foo(1,2,3).should == 42
115
+ end
116
+ end
117
+
118
+ context "when one of the parameters is an array" do
119
+ it "sends a correct JSON-RPC request (array is preserved) and returns the result" do
120
+ expected = MultiJson.encode({
121
+ 'jsonrpc' => '2.0',
122
+ 'method' => 'foo',
123
+ 'params' => [[1,2],3],
124
+ 'id' => 1
125
+ })
126
+ response = MultiJson.encode(BOILERPLATE.merge({'result' => 42}))
127
+ RestClient.should_receive(:post).with(SPEC_URL, expected, {:content_type => 'application/json'}).and_return(@resp_mock)
128
+ @resp_mock.should_receive(:body).at_least(:once).and_return(response)
129
+ client = Client.new(SPEC_URL)
130
+ client.foo([1,2],3).should == 42
131
+ end
132
+ end
133
+ end
134
+
135
+ describe "sending a batch request" do
136
+ it "sends a valid JSON-RPC batch request and puts the results in the response objects" do
137
+ batch = MultiJson.encode([
138
+ {"jsonrpc" => "2.0", "method" => "sum", "params" => [1,2,4], "id" => "1"},
139
+ {"jsonrpc" => "2.0", "method" => "subtract", "params" => [42,23], "id" => "2"},
140
+ {"jsonrpc" => "2.0", "method" => "foo_get", "params" => [{"name" => "myself"}], "id" => "5"},
141
+ {"jsonrpc" => "2.0", "method" => "get_data", "id" => "9"}
142
+ ])
143
+
144
+ response = MultiJson.encode([
145
+ {"jsonrpc" => "2.0", "result" => 7, "id" => "1"},
146
+ {"jsonrpc" => "2.0", "result" => 19, "id" => "2"},
147
+ {"jsonrpc" => "2.0", "error" => {"code" => -32601, "message" => "Method not found."}, "id" => "5"},
148
+ {"jsonrpc" => "2.0", "result" => ["hello", 5], "id" => "9"}
149
+ ])
150
+
151
+ ClientHelper.stub!(:make_id).and_return('1', '2', '5', '9')
152
+ RestClient.should_receive(:post).with(SPEC_URL, batch, {:content_type => 'application/json'}).and_return(@resp_mock)
153
+ @resp_mock.should_receive(:body).at_least(:once).and_return(response)
154
+ client = Client.new(SPEC_URL)
155
+
156
+ sum = subtract = foo = data = nil
157
+ Jimson::Client.batch(client) do |batch|
158
+ sum = batch.sum(1,2,4)
159
+ subtract = batch.subtract(42,23)
160
+ foo = batch.foo_get('name' => 'myself')
161
+ data = batch.get_data
162
+ end
163
+
164
+ sum.succeeded?.should be_true
165
+ sum.is_error?.should be_false
166
+ sum.result.should == 7
167
+
168
+ subtract.result.should == 19
169
+
170
+ foo.is_error?.should be_true
171
+ foo.succeeded?.should be_false
172
+ foo.error['code'].should == -32601
173
+
174
+ data.result.should == ['hello', 5]
175
+ end
176
+ end
177
+
178
+ describe "error handling" do
179
+ context "when an error occurs in the Jimson::Client code" do
180
+ it "tags the raised exception with Jimson::Client::Error" do
181
+ client_helper = ClientHelper.new(SPEC_URL)
182
+ ClientHelper.stub!(:new).and_return(client_helper)
183
+ client = Client.new(SPEC_URL)
184
+ client_helper.stub!(:send_single_request).and_raise "intentional error"
185
+ lambda { client.foo }.should raise_error(Jimson::Client::Error)
186
+ end
187
+ end
188
+ end
189
+
190
+ end
191
+ end