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 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