jimson 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,11 @@
1
+ require 'rubygems'
2
+ require 'jimson/request'
3
+ require 'jimson/response'
4
+ require 'jimson/server_error'
5
+ require 'jimson/server'
6
+ require 'jimson/client_error'
7
+ require 'jimson/client'
8
+
9
+ module Jimson
10
+ JSON_RPC_VERSION = '2.0'
11
+ end
@@ -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
@@ -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
@@ -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
@@ -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