jimson 0.1.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.
- 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
|