jimson-client 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,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
+