jimson-client 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,6 @@
1
+ require 'rubygems'
2
+ require 'jimson/server'
3
+ require 'jimson/client'
4
+
5
+ module Jimson
6
+ end
@@ -0,0 +1,168 @@
1
+ require 'patron'
2
+ require 'jimson/server_error'
3
+ require 'jimson/client_error'
4
+ require 'jimson/request'
5
+ require 'jimson/response'
6
+
7
+ module Jimson
8
+ class ClientHelper
9
+ JSON_RPC_VERSION = '2.0'
10
+
11
+ def self.make_id
12
+ rand(10**12)
13
+ end
14
+
15
+ def initialize(url)
16
+ @http = Patron::Session.new
17
+ uri = URI(url)
18
+ @path = uri.path
19
+ @http.base_url = "#{uri.scheme}://#{uri.host}:#{uri.port}"
20
+ @batch = []
21
+ end
22
+
23
+ def process_call(sym, args)
24
+ resp = send_single_request(sym.to_s, args)
25
+
26
+ begin
27
+ data = JSON.parse(resp)
28
+ rescue
29
+ raise Jimson::ClientError::InvalidJSON.new(json)
30
+ end
31
+
32
+ return process_single_response(data)
33
+ end
34
+
35
+ def send_single_request(method, args)
36
+ post_data = {
37
+ 'jsonrpc' => JSON_RPC_VERSION,
38
+ 'method' => method,
39
+ 'params' => args,
40
+ 'id' => self.class.make_id
41
+ }.to_json
42
+ resp = @http.post(@path, post_data)
43
+ if resp.nil? || resp.body.nil? || resp.body.empty?
44
+ raise Jimson::ClientError::InvalidResponse.new
45
+ end
46
+
47
+ return resp.body
48
+
49
+ rescue Exception, StandardError
50
+ raise new Jimson::ClientError::InternalError.new($!)
51
+ end
52
+
53
+ def send_batch_request(batch)
54
+ post_data = batch.to_json
55
+ resp = @http.post(@path, post_data)
56
+ if resp.nil? || resp.body.nil? || resp.body.empty?
57
+ raise Jimson::ClientError::InvalidResponse.new
58
+ end
59
+
60
+ return resp.body
61
+ end
62
+
63
+ def process_batch_response(responses)
64
+ responses.each do |resp|
65
+ saved_response = @batch.map { |r| r[1] }.select { |r| r.id == resp['id'] }.first
66
+ raise Jimson::ClientError::InvalidResponse.new unless !!saved_response
67
+ saved_response.populate!(resp)
68
+ end
69
+ end
70
+
71
+ def process_single_response(data)
72
+ raise Jimson::ClientError::InvalidResponse.new if !valid_response?(data)
73
+
74
+ if !!data['error']
75
+ code = data['error']['code']
76
+ if Jimson::ServerError::CODES.keys.include?(code)
77
+ raise Jimson::ServerError::CODES[code].new
78
+ else
79
+ raise Jimson::ClientError::UnknownServerError.new(code, data['error']['message'])
80
+ end
81
+ end
82
+
83
+ return data['result']
84
+
85
+ rescue Exception, StandardError
86
+ raise new Jimson::ClientError::InternalError.new
87
+ end
88
+
89
+ def valid_response?(data)
90
+ return false if !data.is_a?(Hash)
91
+
92
+ return false if data['jsonrpc'] != JSON_RPC_VERSION
93
+
94
+ return false if !data.has_key?('id')
95
+
96
+ return false if data.has_key?('error') && data.has_key?('result')
97
+
98
+ if data.has_key?('error')
99
+ if !data['error'].is_a?(Hash) || !data['error'].has_key?('code') || !data['error'].has_key?('message')
100
+ return false
101
+ end
102
+
103
+ if !data['error']['code'].is_a?(Fixnum) || !data['error']['message'].is_a?(String)
104
+ return false
105
+ end
106
+ end
107
+
108
+ return true
109
+
110
+ rescue
111
+ return false
112
+ end
113
+
114
+ def push_batch_request(request)
115
+ request.id = self.class.make_id
116
+ response = Jimson::Response.new(request.id)
117
+ @batch << [request, response]
118
+ return response
119
+ end
120
+
121
+ def send_batch
122
+ batch = @batch.map(&:first) # get the requests
123
+ response = send_batch_request(batch)
124
+
125
+ begin
126
+ responses = JSON.parse(response)
127
+ rescue
128
+ raise Jimson::ClientError::InvalidJSON.new(json)
129
+ end
130
+
131
+ process_batch_response(responses)
132
+ @batch = []
133
+ end
134
+
135
+ end
136
+
137
+ class BatchClient
138
+
139
+ def initialize(helper)
140
+ @helper = helper
141
+ end
142
+
143
+ def method_missing(sym, *args, &block)
144
+ request = Jimson::Request.new(sym.to_s, args)
145
+ @helper.push_batch_request(request)
146
+ end
147
+
148
+ end
149
+
150
+ class Client
151
+
152
+ def self.batch(client)
153
+ helper = client.instance_variable_get(:@helper)
154
+ batch_client = BatchClient.new(helper)
155
+ yield batch_client
156
+ helper.send_batch
157
+ end
158
+
159
+ def initialize(url)
160
+ @helper = ClientHelper.new(url)
161
+ end
162
+
163
+ def method_missing(sym, *args, &block)
164
+ @helper.process_call(sym, args)
165
+ end
166
+
167
+ end
168
+ 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,163 @@
1
+ require 'eventmachine'
2
+ require 'evma_httpserver'
3
+ require 'logger'
4
+ require 'json'
5
+ require 'jimson/server_error'
6
+
7
+ module Jimson
8
+ class HttpServer < EM::Connection
9
+ include EM::HttpServer
10
+
11
+ JSON_RPC_VERSION = '2.0'
12
+
13
+ def self.handler=(handler)
14
+ @@handler = handler
15
+ end
16
+
17
+ def process_http_request
18
+ resp = EM::DelegatedHttpResponse.new( self )
19
+ resp.status = 200
20
+ resp.content = process_post(@http_post_content)
21
+ resp.send_response
22
+ end
23
+
24
+ def process_post(content)
25
+ begin
26
+ request = parse_request(@http_post_content)
27
+ if request.is_a?(Array)
28
+ raise Jimson::ServerError::InvalidRequest.new if request.empty?
29
+ response = request.map { |req| handle_request(req) }
30
+ else
31
+ response = handle_request(request)
32
+ end
33
+ rescue Jimson::ServerError::ParseError, Jimson::ServerError::InvalidRequest => e
34
+ response = error_response(e)
35
+ rescue Jimson::ServerError::Generic => e
36
+ response = error_response(e, request)
37
+ rescue StandardError, Exception
38
+ response = error_response(Jimson::ServerError::InternalError.new)
39
+ end
40
+
41
+ response.compact! if response.is_a?(Array)
42
+
43
+ return nil if response.nil? || (response.respond_to?(:empty?) && response.empty?)
44
+
45
+ response.to_json
46
+ end
47
+
48
+ def handle_request(request)
49
+ response = nil
50
+ begin
51
+ if !validate_request(request)
52
+ response = error_response(Jimson::ServerError::InvalidRequest.new)
53
+ else
54
+ response = create_response(request)
55
+ end
56
+ rescue Jimson::ServerError::Generic => e
57
+ response = error_response(e, request)
58
+ end
59
+
60
+ response
61
+ end
62
+
63
+ def validate_request(request)
64
+ required_keys = %w(jsonrpc method)
65
+ required_types = {
66
+ 'jsonrpc' => [String],
67
+ 'method' => [String],
68
+ 'params' => [Hash, Array],
69
+ 'id' => [String, Fixnum, NilClass]
70
+ }
71
+
72
+ return false if !request.is_a?(Hash)
73
+
74
+ required_keys.each do |key|
75
+ return false if !request.has_key?(key)
76
+ end
77
+
78
+ required_types.each do |key, types|
79
+ return false if request.has_key?(key) && !types.any? { |type| request[key].is_a?(type) }
80
+ end
81
+
82
+ return false if request['jsonrpc'] != JSON_RPC_VERSION
83
+
84
+ true
85
+ end
86
+
87
+ def create_response(request)
88
+ params = request['params']
89
+ begin
90
+ if params.is_a?(Hash)
91
+ result = @@handler.send(request['method'], params)
92
+ else
93
+ result = @@handler.send(request['method'], *params)
94
+ end
95
+ rescue NoMethodError
96
+ raise Jimson::ServerError::MethodNotFound.new
97
+ rescue ArgumentError
98
+ raise Jimson::ServerError::InvalidParams.new
99
+ rescue
100
+ raise Jimson::ServerError::ApplicationError.new($!)
101
+ end
102
+
103
+ response = success_response(request, result)
104
+
105
+ # A Notification is a Request object without an "id" member.
106
+ # The Server MUST NOT reply to a Notification, including those
107
+ # that are within a batch request.
108
+ response = nil if !request.has_key?('id')
109
+
110
+ response
111
+ end
112
+
113
+ def error_response(error, request = nil)
114
+ resp = {
115
+ 'jsonrpc' => JSON_RPC_VERSION,
116
+ 'error' => error.to_h,
117
+ }
118
+ if !!request && request.has_key?('id')
119
+ resp['id'] = request['id']
120
+ else
121
+ resp['id'] = nil
122
+ end
123
+
124
+ resp
125
+ end
126
+
127
+ def success_response(request, result)
128
+ {
129
+ 'jsonrpc' => JSON_RPC_VERSION,
130
+ 'result' => result,
131
+ 'id' => request['id']
132
+ }
133
+ end
134
+
135
+ def parse_request(post)
136
+ data = JSON.parse(post)
137
+ rescue
138
+ raise Jimson::ServerError::ParseError.new
139
+ end
140
+
141
+ end
142
+
143
+ class Server
144
+
145
+ attr_accessor :handler, :host, :port, :logger
146
+
147
+ def initialize(handler, host = '0.0.0.0', port = 8999, logger = Logger.new(STDOUT))
148
+ @handler = handler
149
+ @host = host
150
+ @port = port
151
+ @logger = logger
152
+ end
153
+
154
+ def start
155
+ Jimson::HttpServer.handler = @handler
156
+ EM.run do
157
+ EM::start_server(@host, @port, Jimson::HttpServer)
158
+ @logger.info("Server listening on #{@host}:#{@port} with handler '#{@handler}'")
159
+ end
160
+ end
161
+
162
+ end
163
+ end
@@ -0,0 +1,64 @@
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
+ class ApplicationError < Generic
51
+ def initialize(err)
52
+ super(-32099, "The application being served raised an error: #{err}")
53
+ end
54
+ end
55
+
56
+ CODES = {
57
+ -32700 => ParseError,
58
+ -32600 => InvalidRequest,
59
+ -32601 => MethodNotFound,
60
+ -32602 => InvalidParams,
61
+ -32603 => InternalError
62
+ }
63
+ end
64
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jimson-client
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-20 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: json
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: 1.5.1
36
+ type: :runtime
37
+ version_requirements: *id002
38
+ description:
39
+ email:
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files:
45
+ - README.rdoc
46
+ files:
47
+ - VERSION
48
+ - LICENSE.txt
49
+ - CHANGELOG.rdoc
50
+ - README.rdoc
51
+ - Rakefile
52
+ - lib/jimson.rb
53
+ - lib/jimson/client_error.rb
54
+ - lib/jimson/server.rb
55
+ - lib/jimson/server_error.rb
56
+ - lib/jimson/response.rb
57
+ - lib/jimson/request.rb
58
+ - lib/jimson/client.rb
59
+ has_rdoc: true
60
+ homepage: http://www.github.com/chriskite/jimson
61
+ licenses: []
62
+
63
+ post_install_message:
64
+ rdoc_options: []
65
+
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
80
+ requirements: []
81
+
82
+ rubyforge_project:
83
+ rubygems_version: 1.6.2
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: JSON-RPC 2.0 client
87
+ test_files: []
88
+