jimson 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +0 -0
- data/LICENSE.txt +17 -0
- data/README.rdoc +0 -0
- data/Rakefile +25 -0
- data/VERSION +1 -0
- data/lib/jimson.rb +11 -0
- data/lib/jimson/client.rb +163 -0
- data/lib/jimson/client_error.rb +28 -0
- data/lib/jimson/request.rb +25 -0
- data/lib/jimson/response.rb +30 -0
- data/lib/jimson/server.rb +158 -0
- data/lib/jimson/server_error.rb +58 -0
- data/spec/client_spec.rb +80 -0
- data/spec/em_helper.rb +34 -0
- data/spec/server_spec.rb +202 -0
- data/spec/spec_helper.rb +25 -0
- metadata +117 -0
data/CHANGELOG.rdoc
ADDED
File without changes
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
2
|
+
of this software and associated documentation files (the "Software"), to deal
|
3
|
+
in the Software without restriction, including without limitation the rights
|
4
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
5
|
+
copies of the Software, and to permit persons to whom the Software is
|
6
|
+
furnished to do so, subject to the following conditions:
|
7
|
+
|
8
|
+
The above copyright notice and this permission notice shall be included in
|
9
|
+
all copies or substantial portions of the Software.
|
10
|
+
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
12
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
13
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
14
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
15
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
16
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
17
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
File without changes
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
desc "Run all specs"
|
6
|
+
RSpec::Core::RakeTask.new(:rspec) do |spec|
|
7
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
11
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
12
|
+
spec.rcov = true
|
13
|
+
end
|
14
|
+
|
15
|
+
task :default => :rspec
|
16
|
+
|
17
|
+
require 'rake/rdoctask'
|
18
|
+
Rake::RDocTask.new do |rdoc|
|
19
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
20
|
+
|
21
|
+
rdoc.rdoc_dir = 'rdoc'
|
22
|
+
rdoc.title = "jimson #{version}"
|
23
|
+
rdoc.rdoc_files.include('README*')
|
24
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
25
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/jimson.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'patron'
|
2
|
+
|
3
|
+
module Jimson
|
4
|
+
class ClientHelper
|
5
|
+
|
6
|
+
def self.make_id
|
7
|
+
rand(10**12)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(url)
|
11
|
+
@http = Patron::Session.new
|
12
|
+
uri = URI(url)
|
13
|
+
@path = uri.path
|
14
|
+
@http.base_url = "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
15
|
+
@batch = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def process_call(sym, args)
|
19
|
+
resp = send_single_request(sym.to_s, args)
|
20
|
+
|
21
|
+
begin
|
22
|
+
data = JSON.parse(resp)
|
23
|
+
rescue
|
24
|
+
raise Jimson::ClientError::InvalidJSON.new(json)
|
25
|
+
end
|
26
|
+
|
27
|
+
return process_single_response(data)
|
28
|
+
end
|
29
|
+
|
30
|
+
def send_single_request(method, args)
|
31
|
+
post_data = {
|
32
|
+
'jsonrpc' => '2.0',
|
33
|
+
'method' => method,
|
34
|
+
'params' => args,
|
35
|
+
'id' => self.class.make_id
|
36
|
+
}.to_json
|
37
|
+
resp = @http.post(@path, post_data)
|
38
|
+
if resp.nil? || resp.body.nil? || resp.body.empty?
|
39
|
+
raise Jimson::ClientError::InvalidResponse.new
|
40
|
+
end
|
41
|
+
|
42
|
+
return resp.body
|
43
|
+
|
44
|
+
rescue Exception, StandardError
|
45
|
+
raise new Jimson::ClientError::InternalError.new($!)
|
46
|
+
end
|
47
|
+
|
48
|
+
def send_batch_request(batch)
|
49
|
+
post_data = batch.to_json
|
50
|
+
resp = @http.post(@path, post_data)
|
51
|
+
if resp.nil? || resp.body.nil? || resp.body.empty?
|
52
|
+
raise Jimson::ClientError::InvalidResponse.new
|
53
|
+
end
|
54
|
+
|
55
|
+
return resp.body
|
56
|
+
end
|
57
|
+
|
58
|
+
def process_batch_response(responses)
|
59
|
+
responses.each do |resp|
|
60
|
+
saved_response = @batch.map { |r| r[1] }.select { |r| r.id == resp['id'] }.first
|
61
|
+
raise Jimson::ClientError::InvalidResponse.new unless !!saved_response
|
62
|
+
saved_response.populate!(resp)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def process_single_response(data)
|
67
|
+
raise Jimson::ClientError::InvalidResponse.new if !valid_response?(data)
|
68
|
+
|
69
|
+
if !!data['error']
|
70
|
+
code = data['error']['code']
|
71
|
+
if Jimson::ServerError::CODES.keys.include?(code)
|
72
|
+
raise Jimson::ServerError::CODES[code].new
|
73
|
+
else
|
74
|
+
raise Jimson::ClientError::UnknownServerError.new(code, data['error']['message'])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
return data['result']
|
79
|
+
|
80
|
+
rescue Exception, StandardError
|
81
|
+
raise new Jimson::ClientError::InternalError.new
|
82
|
+
end
|
83
|
+
|
84
|
+
def valid_response?(data)
|
85
|
+
return false if !data.is_a?(Hash)
|
86
|
+
|
87
|
+
return false if data['jsonrpc'] != '2.0'
|
88
|
+
|
89
|
+
return false if !data.has_key?('id')
|
90
|
+
|
91
|
+
return false if data.has_key?('error') && data.has_key?('result')
|
92
|
+
|
93
|
+
if data.has_key?('error')
|
94
|
+
if !data['error'].is_a?(Hash) || !data['error'].has_key?('code') || !data['error'].has_key?('message')
|
95
|
+
return false
|
96
|
+
end
|
97
|
+
|
98
|
+
if !data['error']['code'].is_a?(Fixnum) || !data['error']['message'].is_a?(String)
|
99
|
+
return false
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
return true
|
104
|
+
|
105
|
+
rescue
|
106
|
+
return false
|
107
|
+
end
|
108
|
+
|
109
|
+
def push_batch_request(request)
|
110
|
+
request.id = self.class.make_id
|
111
|
+
response = Jimson::Response.new(request.id)
|
112
|
+
@batch << [request, response]
|
113
|
+
return response
|
114
|
+
end
|
115
|
+
|
116
|
+
def send_batch
|
117
|
+
batch = @batch.map(&:first) # get the requests
|
118
|
+
response = send_batch_request(batch)
|
119
|
+
|
120
|
+
begin
|
121
|
+
responses = JSON.parse(response)
|
122
|
+
rescue
|
123
|
+
raise Jimson::ClientError::InvalidJSON.new(json)
|
124
|
+
end
|
125
|
+
|
126
|
+
process_batch_response(responses)
|
127
|
+
@batch = []
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
class BatchClient
|
133
|
+
|
134
|
+
def initialize(helper)
|
135
|
+
@helper = helper
|
136
|
+
end
|
137
|
+
|
138
|
+
def method_missing(sym, *args, &block)
|
139
|
+
request = Jimson::Request.new(sym.to_s, args)
|
140
|
+
@helper.push_batch_request(request)
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
class Client
|
146
|
+
|
147
|
+
def self.batch(client)
|
148
|
+
helper = client.instance_variable_get(:@helper)
|
149
|
+
batch_client = BatchClient.new(helper)
|
150
|
+
yield batch_client
|
151
|
+
helper.send_batch
|
152
|
+
end
|
153
|
+
|
154
|
+
def initialize(url)
|
155
|
+
@helper = ClientHelper.new(url)
|
156
|
+
end
|
157
|
+
|
158
|
+
def method_missing(sym, *args, &block)
|
159
|
+
@helper.process_call(sym, args)
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Jimson
|
2
|
+
module ClientError
|
3
|
+
class InvalidResponse < Exception
|
4
|
+
def initialize()
|
5
|
+
super('Invalid or empty response from server.')
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class InvalidJSON < Exception
|
10
|
+
def initialize(json)
|
11
|
+
super("Couldn't parse JSON string received from server:\n#{json}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class InternalError < Exception
|
16
|
+
def initialize(e)
|
17
|
+
super("An internal client error occurred when processing the request: #{e}\n#{e.backtrace.join("\n")}")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class UnknownServerError < Exception
|
22
|
+
def initialize(code, message)
|
23
|
+
super("The server specified an error the client doesn't know about: #{code} #{message}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module 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
|
+
self.to_h.to_json(*a)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module 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,158 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'evma_httpserver'
|
3
|
+
require 'logger'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Jimson
|
7
|
+
class HttpServer < EM::Connection
|
8
|
+
include EM::HttpServer
|
9
|
+
|
10
|
+
def self.handler=(handler)
|
11
|
+
@@handler = handler
|
12
|
+
end
|
13
|
+
|
14
|
+
def process_http_request
|
15
|
+
resp = EM::DelegatedHttpResponse.new( self )
|
16
|
+
resp.status = 200
|
17
|
+
resp.content = process_post(@http_post_content)
|
18
|
+
resp.send_response
|
19
|
+
end
|
20
|
+
|
21
|
+
def process_post(content)
|
22
|
+
begin
|
23
|
+
request = parse_request(@http_post_content)
|
24
|
+
if request.is_a?(Array)
|
25
|
+
raise Jimson::ServerError::InvalidRequest.new if request.empty?
|
26
|
+
response = request.map { |req| handle_request(req) }
|
27
|
+
else
|
28
|
+
response = handle_request(request)
|
29
|
+
end
|
30
|
+
rescue Jimson::ServerError::ParseError, Jimson::ServerError::InvalidRequest => e
|
31
|
+
response = error_response(e)
|
32
|
+
rescue Jimson::ServerError::Generic => e
|
33
|
+
response = error_response(e, request)
|
34
|
+
rescue StandardError, Exception
|
35
|
+
response = error_response(Jimson::ServerError::InternalError.new)
|
36
|
+
end
|
37
|
+
|
38
|
+
response.compact! if response.is_a?(Array)
|
39
|
+
|
40
|
+
return nil if response.nil? || (response.respond_to?(:empty?) && response.empty?)
|
41
|
+
|
42
|
+
response.to_json
|
43
|
+
end
|
44
|
+
|
45
|
+
def handle_request(request)
|
46
|
+
response = nil
|
47
|
+
begin
|
48
|
+
if !validate_request(request)
|
49
|
+
response = error_response(Jimson::ServerError::InvalidRequest.new)
|
50
|
+
else
|
51
|
+
response = create_response(request)
|
52
|
+
end
|
53
|
+
rescue Jimson::ServerError::Generic => e
|
54
|
+
response = error_response(e, request)
|
55
|
+
end
|
56
|
+
|
57
|
+
response
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate_request(request)
|
61
|
+
required_keys = %w(jsonrpc method)
|
62
|
+
required_types = {
|
63
|
+
'jsonrpc' => [String],
|
64
|
+
'method' => [String],
|
65
|
+
'params' => [Hash, Array],
|
66
|
+
'id' => [String, Fixnum, NilClass]
|
67
|
+
}
|
68
|
+
|
69
|
+
return false if !request.is_a?(Hash)
|
70
|
+
|
71
|
+
required_keys.each do |key|
|
72
|
+
return false if !request.has_key?(key)
|
73
|
+
end
|
74
|
+
|
75
|
+
required_types.each do |key, types|
|
76
|
+
return false if request.has_key?(key) && !types.any? { |type| request[key].is_a?(type) }
|
77
|
+
end
|
78
|
+
|
79
|
+
return false if request['jsonrpc'] != '2.0'
|
80
|
+
|
81
|
+
true
|
82
|
+
end
|
83
|
+
|
84
|
+
def create_response(request)
|
85
|
+
params = request['params']
|
86
|
+
begin
|
87
|
+
if params.is_a?(Hash)
|
88
|
+
result = @@handler.send(request['method'], params)
|
89
|
+
else
|
90
|
+
result = @@handler.send(request['method'], *params)
|
91
|
+
end
|
92
|
+
rescue NoMethodError
|
93
|
+
raise Jimson::ServerError::MethodNotFound.new
|
94
|
+
rescue ArgumentError
|
95
|
+
raise Jimson::ServerError::InvalidParams.new
|
96
|
+
end
|
97
|
+
|
98
|
+
response = success_response(request, result)
|
99
|
+
|
100
|
+
# A Notification is a Request object without an "id" member.
|
101
|
+
# The Server MUST NOT reply to a Notification, including those
|
102
|
+
# that are within a batch request.
|
103
|
+
response = nil if !request.has_key?('id')
|
104
|
+
|
105
|
+
response
|
106
|
+
end
|
107
|
+
|
108
|
+
def error_response(error, request = nil)
|
109
|
+
resp = {
|
110
|
+
'jsonrpc' => JSON_RPC_VERSION,
|
111
|
+
'error' => error.to_h,
|
112
|
+
}
|
113
|
+
if !!request && request.has_key?('id')
|
114
|
+
resp['id'] = request['id']
|
115
|
+
else
|
116
|
+
resp['id'] = nil
|
117
|
+
end
|
118
|
+
|
119
|
+
resp
|
120
|
+
end
|
121
|
+
|
122
|
+
def success_response(request, result)
|
123
|
+
{
|
124
|
+
'jsonrpc' => JSON_RPC_VERSION,
|
125
|
+
'result' => result,
|
126
|
+
'id' => request['id']
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
def parse_request(post)
|
131
|
+
data = JSON.parse(post)
|
132
|
+
rescue
|
133
|
+
raise Jimson::ServerError::ParseError.new
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
class Server
|
139
|
+
|
140
|
+
attr_accessor :handler, :host, :port, :logger
|
141
|
+
|
142
|
+
def initialize(handler, host = '0.0.0.0', port = 8999, logger = Logger.new(STDOUT))
|
143
|
+
@handler = handler
|
144
|
+
@host = host
|
145
|
+
@port = port
|
146
|
+
@logger = logger
|
147
|
+
end
|
148
|
+
|
149
|
+
def start
|
150
|
+
Jimson::HttpServer.handler = @handler
|
151
|
+
EM.run do
|
152
|
+
EM::start_server(@host, @port, Jimson::HttpServer)
|
153
|
+
@logger.info("Server listening on #{@host}:#{@port} with handler '#{@handler}'")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Jimson
|
2
|
+
module ServerError
|
3
|
+
class Generic < Exception
|
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
|
+
end
|
19
|
+
|
20
|
+
class ParseError < Generic
|
21
|
+
def initialize
|
22
|
+
super(-32700, 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class InvalidRequest < Generic
|
27
|
+
def initialize
|
28
|
+
super(-32600, 'The JSON sent is not a valid Request object.')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class MethodNotFound < Generic
|
33
|
+
def initialize
|
34
|
+
super(-32601, 'Method not found.')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class InvalidParams < Generic
|
39
|
+
def initialize
|
40
|
+
super(-32602, 'Invalid method parameter(s).')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class InternalError < Generic
|
45
|
+
def initialize
|
46
|
+
super(-32603, 'Internal server error.')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
CODES = {
|
51
|
+
-32700 => ParseError,
|
52
|
+
-32600 => InvalidRequest,
|
53
|
+
-32601 => MethodNotFound,
|
54
|
+
-32602 => InvalidParams,
|
55
|
+
-32603 => InternalError
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module Jimson
|
2
|
+
describe Client do
|
3
|
+
BOILERPLATE = {'jsonrpc' => '2.0', 'id' => 1}
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@http_mock = mock('http')
|
7
|
+
Patron::Session.stub!(:new).and_return(@http_mock)
|
8
|
+
@http_mock.should_receive(:base_url=).with(SPEC_URL)
|
9
|
+
@resp_mock = mock('http_response')
|
10
|
+
ClientHelper.stub!(:make_id).and_return(1)
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:each) do
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "sending a single request" do
|
17
|
+
context "when using positional parameters" do
|
18
|
+
before(:each) do
|
19
|
+
@expected = {
|
20
|
+
'jsonrpc' => '2.0',
|
21
|
+
'method' => 'foo',
|
22
|
+
'params' => [1,2,3],
|
23
|
+
'id' => 1
|
24
|
+
}.to_json
|
25
|
+
end
|
26
|
+
it "sends a valid JSON-RPC request and returns the result" do
|
27
|
+
response = BOILERPLATE.merge({'result' => 42}).to_json
|
28
|
+
@http_mock.should_receive(:post).with('', @expected).and_return(@resp_mock)
|
29
|
+
@resp_mock.should_receive(:body).at_least(:once).and_return(response)
|
30
|
+
client = Client.new(SPEC_URL)
|
31
|
+
client.foo(1,2,3).should == 42
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "sending a batch request" do
|
37
|
+
it "sends a valid JSON-RPC batch request and puts the results in the response objects" do
|
38
|
+
batch = [
|
39
|
+
{"jsonrpc" => "2.0", "method" => "sum", "params" => [1,2,4], "id" => "1"},
|
40
|
+
{"jsonrpc" => "2.0", "method" => "subtract", "params" => [42,23], "id" => "2"},
|
41
|
+
{"jsonrpc" => "2.0", "method" => "foo_get", "params" => [{"name" => "myself"}], "id" => "5"},
|
42
|
+
{"jsonrpc" => "2.0", "method" => "get_data", "id" => "9"}
|
43
|
+
].to_json
|
44
|
+
|
45
|
+
response = [
|
46
|
+
{"jsonrpc" => "2.0", "result" => 7, "id" => "1"},
|
47
|
+
{"jsonrpc" => "2.0", "result" => 19, "id" => "2"},
|
48
|
+
{"jsonrpc" => "2.0", "error" => {"code" => -32601, "message" => "Method not found."}, "id" => "5"},
|
49
|
+
{"jsonrpc" => "2.0", "result" => ["hello", 5], "id" => "9"}
|
50
|
+
].to_json
|
51
|
+
|
52
|
+
ClientHelper.stub!(:make_id).and_return('1', '2', '5', '9')
|
53
|
+
@http_mock.should_receive(:post).with('', batch).and_return(@resp_mock)
|
54
|
+
@resp_mock.should_receive(:body).at_least(:once).and_return(response)
|
55
|
+
client = Client.new(SPEC_URL)
|
56
|
+
|
57
|
+
sum = subtract = foo = data = nil
|
58
|
+
Jimson::Client.batch(client) do |batch|
|
59
|
+
sum = batch.sum(1,2,4)
|
60
|
+
subtract = batch.subtract(42,23)
|
61
|
+
foo = batch.foo_get('name' => 'myself')
|
62
|
+
data = batch.get_data
|
63
|
+
end
|
64
|
+
|
65
|
+
sum.succeeded?.should be_true
|
66
|
+
sum.is_error?.should be_false
|
67
|
+
sum.result.should == 7
|
68
|
+
|
69
|
+
subtract.result.should == 19
|
70
|
+
|
71
|
+
foo.is_error?.should be_true
|
72
|
+
foo.succeeded?.should be_false
|
73
|
+
foo.error['code'].should == -32601
|
74
|
+
|
75
|
+
data.result.should == ['hello', 5]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
data/spec/em_helper.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib/')
|
5
|
+
require 'jimson'
|
6
|
+
|
7
|
+
class TestHandler
|
8
|
+
def subtract(a, b = nil)
|
9
|
+
if a.is_a?(Hash)
|
10
|
+
return a['minuend'] - a['subtrahend']
|
11
|
+
else
|
12
|
+
return a - b
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def sum(a,b,c)
|
17
|
+
a + b + c
|
18
|
+
end
|
19
|
+
|
20
|
+
def notify_hello(*args)
|
21
|
+
# notification, doesn't do anything
|
22
|
+
end
|
23
|
+
|
24
|
+
def update(*args)
|
25
|
+
# notification, doesn't do anything
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_data
|
29
|
+
['hello', 5]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
server = Jimson::Server.new(TestHandler.new)
|
34
|
+
server.start
|
data/spec/server_spec.rb
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Jimson
|
4
|
+
describe Server do
|
5
|
+
|
6
|
+
INVALID_RESPONSE_EXPECTATION = {
|
7
|
+
'jsonrpc' => '2.0',
|
8
|
+
'error' => {
|
9
|
+
'code' => -32600,
|
10
|
+
'message' => 'The JSON sent is not a valid Request object.'
|
11
|
+
},
|
12
|
+
'id' => nil
|
13
|
+
}
|
14
|
+
before(:each) do
|
15
|
+
@sess = Patron::Session.new
|
16
|
+
@sess.base_url = SPEC_URL
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "receiving a request with positional parameters" do
|
20
|
+
context "when no errors occur" do
|
21
|
+
it "returns a response with 'result'" do
|
22
|
+
req = {
|
23
|
+
'jsonrpc' => '2.0',
|
24
|
+
'method' => 'subtract',
|
25
|
+
'params' => [24, 20],
|
26
|
+
'id' => 1
|
27
|
+
}
|
28
|
+
resp = JSON.parse(@sess.post('/', req.to_json).body)
|
29
|
+
resp.should == {
|
30
|
+
'jsonrpc' => '2.0',
|
31
|
+
'result' => 4,
|
32
|
+
'id' => 1
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "receiving a request with named parameters" do
|
39
|
+
context "when no errors occur" do
|
40
|
+
it "returns a response with 'result'" do
|
41
|
+
req = {
|
42
|
+
'jsonrpc' => '2.0',
|
43
|
+
'method' => 'subtract',
|
44
|
+
'params' => {'subtrahend'=> 20, 'minuend' => 24},
|
45
|
+
'id' => 1
|
46
|
+
}
|
47
|
+
resp = JSON.parse(@sess.post('/', req.to_json).body)
|
48
|
+
resp.should == {
|
49
|
+
'jsonrpc' => '2.0',
|
50
|
+
'result' => 4,
|
51
|
+
'id' => 1
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "receiving a notification" do
|
58
|
+
context "when no errors occur" do
|
59
|
+
it "returns no response" do
|
60
|
+
req = {
|
61
|
+
'jsonrpc' => '2.0',
|
62
|
+
'method' => 'update',
|
63
|
+
'params' => [1,2,3,4,5]
|
64
|
+
}
|
65
|
+
resp = @sess.post('/', req.to_json).body
|
66
|
+
resp.should be_empty
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "receiving a call for a non-existent method" do
|
72
|
+
it "returns an error response" do
|
73
|
+
req = {
|
74
|
+
'jsonrpc' => '2.0',
|
75
|
+
'method' => 'foobar',
|
76
|
+
'id' => 1
|
77
|
+
}
|
78
|
+
resp = JSON.parse(@sess.post('/', req.to_json).body)
|
79
|
+
resp.should == {
|
80
|
+
'jsonrpc' => '2.0',
|
81
|
+
'error' => {
|
82
|
+
'code' => -32601,
|
83
|
+
'message' => 'Method not found.'
|
84
|
+
},
|
85
|
+
'id' => 1
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "receiving a call with the wrong number of params" do
|
91
|
+
it "returns an error response" do
|
92
|
+
req = {
|
93
|
+
'jsonrpc' => '2.0',
|
94
|
+
'method' => 'subtract',
|
95
|
+
'params' => [1,2,3],
|
96
|
+
'id' => 1
|
97
|
+
}
|
98
|
+
resp = JSON.parse(@sess.post('/', req.to_json).body)
|
99
|
+
resp.should == {
|
100
|
+
'jsonrpc' => '2.0',
|
101
|
+
'error' => {
|
102
|
+
'code' => -32602,
|
103
|
+
'message' => 'Invalid method parameter(s).'
|
104
|
+
},
|
105
|
+
'id' => 1
|
106
|
+
}
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "receiving invalid JSON" do
|
111
|
+
it "returns an error response" do
|
112
|
+
req = {
|
113
|
+
'jsonrpc' => '2.0',
|
114
|
+
'method' => 'foobar',
|
115
|
+
'id' => 1
|
116
|
+
}.to_json
|
117
|
+
req += '}' # make the json invalid
|
118
|
+
resp = JSON.parse(@sess.post('/', req).body)
|
119
|
+
resp.should == {
|
120
|
+
'jsonrpc' => '2.0',
|
121
|
+
'error' => {
|
122
|
+
'code' => -32700,
|
123
|
+
'message' => 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.'
|
124
|
+
},
|
125
|
+
'id' => nil
|
126
|
+
}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe "receiving an invalid request" do
|
131
|
+
context "when the request is not a batch" do
|
132
|
+
it "returns an error response" do
|
133
|
+
req = {
|
134
|
+
'jsonrpc' => '2.0',
|
135
|
+
'method' => 1 # method as int is invalid
|
136
|
+
}.to_json
|
137
|
+
resp = JSON.parse(@sess.post('/', req).body)
|
138
|
+
resp.should == INVALID_RESPONSE_EXPECTATION
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context "when the request is an empty batch" do
|
143
|
+
it "returns an error response" do
|
144
|
+
req = [].to_json
|
145
|
+
resp = JSON.parse(@sess.post('/', req).body)
|
146
|
+
resp.should == INVALID_RESPONSE_EXPECTATION
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
context "when the request is an invalid batch" do
|
151
|
+
it "returns an error response" do
|
152
|
+
req = [1,2].to_json
|
153
|
+
resp = JSON.parse(@sess.post('/', req).body)
|
154
|
+
resp.should == [INVALID_RESPONSE_EXPECTATION, INVALID_RESPONSE_EXPECTATION]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe "receiving a valid batch request" do
|
160
|
+
context "when not all requests are notifications" do
|
161
|
+
it "returns an array of responses" do
|
162
|
+
reqs = [
|
163
|
+
{'jsonrpc' => '2.0', 'method' => 'sum', 'params' => [1,2,4], 'id' => '1'},
|
164
|
+
{'jsonrpc' => '2.0', 'method' => 'notify_hello', 'params' => [7]},
|
165
|
+
{'jsonrpc' => '2.0', 'method' => 'subtract', 'params' => [42,23], 'id' => '2'},
|
166
|
+
{'foo' => 'boo'},
|
167
|
+
{'jsonrpc' => '2.0', 'method' => 'foo.get', 'params' => {'name' => 'myself'}, 'id' => '5'},
|
168
|
+
{'jsonrpc' => '2.0', 'method' => 'get_data', 'id' => '9'}
|
169
|
+
].to_json
|
170
|
+
resp = JSON.parse(@sess.post('/', reqs).body)
|
171
|
+
resp.should == [
|
172
|
+
{'jsonrpc' => '2.0', 'result' => 7, 'id' => '1'},
|
173
|
+
{'jsonrpc' => '2.0', 'result' => 19, 'id' => '2'},
|
174
|
+
{'jsonrpc' => '2.0', 'error' => {'code' => -32600, 'message' => 'The JSON sent is not a valid Request object.'}, 'id' => nil},
|
175
|
+
{'jsonrpc' => '2.0', 'error' => {'code' => -32601, 'message' => 'Method not found.'}, 'id' => '5'},
|
176
|
+
{'jsonrpc' => '2.0', 'result' => ['hello', 5], 'id' => '9'}
|
177
|
+
]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
context "when all the requests are notifications" do
|
182
|
+
it "returns no response" do
|
183
|
+
req = [
|
184
|
+
{
|
185
|
+
'jsonrpc' => '2.0',
|
186
|
+
'method' => 'update',
|
187
|
+
'params' => [1,2,3,4,5]
|
188
|
+
},
|
189
|
+
{
|
190
|
+
'jsonrpc' => '2.0',
|
191
|
+
'method' => 'update',
|
192
|
+
'params' => [1,2,3,4,5]
|
193
|
+
}
|
194
|
+
]
|
195
|
+
resp = @sess.post('/', req.to_json).body
|
196
|
+
resp.should be_empty
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib/')
|
3
|
+
require 'jimson'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'json'
|
6
|
+
require 'patron'
|
7
|
+
require 'fakeweb'
|
8
|
+
|
9
|
+
SPEC_URL = 'http://localhost:8999'
|
10
|
+
|
11
|
+
|
12
|
+
def fake_response(json)
|
13
|
+
FakeWeb.register_uri(:post, SPEC_URL, :body => json)
|
14
|
+
end
|
15
|
+
|
16
|
+
pid = Process.spawn(File.dirname(__FILE__) + '/em_helper.rb')
|
17
|
+
|
18
|
+
RSpec.configure do |config|
|
19
|
+
config.after(:all) do
|
20
|
+
Process.kill(9, pid)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
sleep 1
|
25
|
+
|
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jimson
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.1.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Chris Kite
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-07-19 00:00:00 -05:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: patron
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 0.4.12
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: eventmachine
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 0.12.10
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: eventmachine_httpserver
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.2.1
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: json
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 1.5.1
|
58
|
+
type: :runtime
|
59
|
+
version_requirements: *id004
|
60
|
+
description:
|
61
|
+
email:
|
62
|
+
executables: []
|
63
|
+
|
64
|
+
extensions: []
|
65
|
+
|
66
|
+
extra_rdoc_files:
|
67
|
+
- README.rdoc
|
68
|
+
files:
|
69
|
+
- VERSION
|
70
|
+
- LICENSE.txt
|
71
|
+
- CHANGELOG.rdoc
|
72
|
+
- README.rdoc
|
73
|
+
- Rakefile
|
74
|
+
- lib/jimson.rb
|
75
|
+
- lib/jimson/client_error.rb
|
76
|
+
- lib/jimson/server.rb
|
77
|
+
- lib/jimson/server_error.rb
|
78
|
+
- lib/jimson/response.rb
|
79
|
+
- lib/jimson/request.rb
|
80
|
+
- lib/jimson/client.rb
|
81
|
+
- spec/server_spec.rb
|
82
|
+
- spec/client_spec.rb
|
83
|
+
- spec/em_helper.rb
|
84
|
+
- spec/spec_helper.rb
|
85
|
+
has_rdoc: true
|
86
|
+
homepage: http://www.github.com/chriskite/jimson
|
87
|
+
licenses: []
|
88
|
+
|
89
|
+
post_install_message:
|
90
|
+
rdoc_options: []
|
91
|
+
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: "0"
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: "0"
|
106
|
+
requirements: []
|
107
|
+
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 1.6.2
|
110
|
+
signing_key:
|
111
|
+
specification_version: 3
|
112
|
+
summary: JSON-RPC 2.0 client and server
|
113
|
+
test_files:
|
114
|
+
- spec/server_spec.rb
|
115
|
+
- spec/client_spec.rb
|
116
|
+
- spec/em_helper.rb
|
117
|
+
- spec/spec_helper.rb
|