rpc 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ .rvmrc
2
+ /*.gem
@@ -0,0 +1,7 @@
1
+ = Version 0.2
2
+ * [FEATURE] Full JSON-RPC 2.0 support (added batch, errors and notifications).
3
+
4
+ = Version 0.1
5
+ * [FEATURE] Net::HTTP(S) client.
6
+ * [FEATURE] EM HTTP Request client.
7
+ * [FEATURE] JSON-RPC encoder.
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ source "http://gemcutter.org"
4
+
5
+ group(:examples) do
6
+ gem "rack"
7
+ end
8
+
9
+ group(:em) do
10
+ gem "em-http-request"
11
+ end
12
+
13
+ # group(:amqp) do
14
+ # gem "amq-client"
15
+ # end
16
+
17
+ group(:test) do
18
+ gem "rspec", ">=2.0.0"
19
+ end
@@ -0,0 +1,28 @@
1
+ GEM
2
+ remote: http://gemcutter.org/
3
+ specs:
4
+ addressable (2.2.5)
5
+ diff-lcs (1.1.2)
6
+ em-http-request (0.3.0)
7
+ addressable (>= 2.0.0)
8
+ escape_utils
9
+ eventmachine (>= 0.12.9)
10
+ escape_utils (0.2.3)
11
+ eventmachine (0.12.10)
12
+ rack (1.2.2)
13
+ rspec (2.5.0)
14
+ rspec-core (~> 2.5.0)
15
+ rspec-expectations (~> 2.5.0)
16
+ rspec-mocks (~> 2.5.0)
17
+ rspec-core (2.5.1)
18
+ rspec-expectations (2.5.0)
19
+ diff-lcs (~> 1.1.2)
20
+ rspec-mocks (2.5.0)
21
+
22
+ PLATFORMS
23
+ ruby
24
+
25
+ DEPENDENCIES
26
+ em-http-request
27
+ rack
28
+ rspec (>= 2.0.0)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Jakub Stastny aka botanicus
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,7 @@
1
+ h1. About
2
+
3
+ Generic RPC library for Ruby. Currently it supports JSON-RPC over HTTP, support for AMQP and Redis will follow soon.
4
+
5
+ h1. Usage
6
+
7
+ See examples.
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../../../lib", __FILE__)
5
+
6
+ require "rpc"
7
+
8
+ RPC.logging = true
9
+
10
+ client = RPC::Clients::EmHttpRequest.new("http://127.0.0.1:8081")
11
+
12
+ RPC::Client.new(client) do |client|
13
+ # Get result of an existing method.
14
+ client.server_timestamp do |result, error|
15
+ puts "Server timestamp is #{result}"
16
+ end
17
+
18
+ # Get result of a non-existing method via method_missing.
19
+ client.send(:+, 1) do |result, error|
20
+ puts "Method missing works: #{result}"
21
+ end
22
+
23
+ # Synchronous error handling.
24
+ client.buggy_method do |result, error|
25
+ STDERR.puts "EXCEPTION CAUGHT:"
26
+ STDERR.puts "#{error.class} #{error.message}"
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../../../lib", __FILE__)
5
+
6
+ require "rpc"
7
+
8
+ RPC.logging = true
9
+
10
+ client = RPC::Client.setup("http://127.0.0.1:8081")
11
+
12
+ # Get result of an existing method.
13
+ puts "Server timestamp is #{client.server_timestamp}"
14
+
15
+ # Get result of a non-existing method via method_missing.
16
+ puts "Method missing works: #{client + 1}"
17
+
18
+ # Synchronous error handling.
19
+ begin
20
+ client.buggy_method
21
+ rescue Exception => exception
22
+ STDERR.puts "EXCEPTION CAUGHT: #{exception.inspect}"
23
+ end
24
+
25
+ # Notification isn't supported, because HTTP works in
26
+ # request/response mode, so it does behave in the same
27
+ # manner as RPC via method_missing.
28
+ puts "Sending a notification ..."
29
+ client.notification(:log, "Some shit.")
30
+
31
+ # Batch.
32
+ result = client.batch([[:log, ["Message"], nil], [:a_method, []]])
33
+ puts "Batch result: #{result}"
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../../../lib", __FILE__)
5
+
6
+ require "rpc"
7
+ require "irb"
8
+
9
+ @client = RPC::Client.setup("http://127.0.0.1:8081")
10
+
11
+ puts "~ RPC Client initialised, use @client to access it."
12
+
13
+ IRB.start(__FILE__)
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env rackup --port 8081
2
+ # encoding: utf-8
3
+
4
+ # http://groups.google.com/group/json-rpc/web/json-rpc-over-http
5
+
6
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
7
+
8
+ require "rpc"
9
+ require "rack/request"
10
+
11
+ RPC.logging = true
12
+ # RPC.development = true
13
+
14
+ class RpcRunner
15
+ def server
16
+ @server ||= RPC::Server.new(RemoteObject.new)
17
+ end
18
+
19
+ def call(env)
20
+ request = Rack::Request.new(env)
21
+ command = request.body.read
22
+ binary = self.server.execute(command)
23
+ if binary.match(/NoMethodError/)
24
+ response(404, binary)
25
+ else
26
+ response(200, binary)
27
+ end
28
+ end
29
+
30
+ def response(status, body)
31
+ headers = {
32
+ "Content-Type" => "application/json-rpc",
33
+ "Content-Length" => body.bytesize.to_s}
34
+ [status, headers, [body]]
35
+ end
36
+ end
37
+
38
+ class RemoteObject
39
+ def server_timestamp
40
+ Time.now.to_i
41
+ end
42
+
43
+ def buggy_method
44
+ raise "It doesn't work!"
45
+ end
46
+
47
+ def method_missing(name, *args)
48
+ "[SERVER] received method #{name} with #{args.inspect}"
49
+ end
50
+ end
51
+
52
+ map("/") do
53
+ run RpcRunner.new
54
+ end
@@ -0,0 +1,143 @@
1
+ # encoding: utf-8
2
+
3
+ module RPC
4
+ module Clients
5
+ autoload :NetHttp, "rpc/clients/net-http"
6
+ autoload :EmHttpRequest, "rpc/clients/em-http-request"
7
+ end
8
+
9
+ module Encoders
10
+ autoload :Json, "rpc/encoders/json"
11
+ end
12
+
13
+ def self.logging
14
+ @logging ||= $DEBUG
15
+ end
16
+
17
+ def self.logging=(boolean)
18
+ @logging = boolean
19
+ end
20
+
21
+ def self.log(message)
22
+ STDERR.puts(message) if self.logging
23
+ end
24
+
25
+ def self.development=(boolean)
26
+ @development = boolean
27
+ end
28
+
29
+ def self.development?
30
+ !! @development
31
+ end
32
+
33
+ def self.full_const_get(const_name)
34
+ parts = const_name.sub(/^::/, "").split("::")
35
+ parts.reduce(Object) do |constant, part|
36
+ constant.const_get(part)
37
+ end
38
+ end
39
+
40
+ class Server
41
+ def initialize(subject, encoder = RPC::Encoders::Json::Server.new)
42
+ @subject, @encoder = subject, encoder
43
+ end
44
+
45
+ def execute(encoded_command)
46
+ @encoder.execute(encoded_command, @subject)
47
+ end
48
+ end
49
+
50
+ module ExceptionsMixin
51
+ attr_accessor :server_backtrace
52
+
53
+ # NOTE: We can't use super to get the client backtrace,
54
+ # because backtrace is generated only if there is none
55
+ # yet and because we are redefining the backtrace method,
56
+ # there always will be some backtrace.
57
+ def backtrace
58
+ @backtrace ||= begin
59
+ caller(3) + ["... server ..."] + self.server_backtrace
60
+ end
61
+ end
62
+ end
63
+
64
+ class Client < BasicObject
65
+ def self.setup(uri, client_class = Clients::NetHttp, encoder = Encoders::Json::Client.new)
66
+ client = client_class.new(uri)
67
+ self.new(client, encoder)
68
+ end
69
+
70
+ def initialize(client, encoder = Encoders::Json::Client.new, &block)
71
+ @client, @encoder = client, encoder
72
+
73
+ if block
74
+ @client.run do
75
+ block.call(self)
76
+ end
77
+ else
78
+ @client.connect
79
+ end
80
+ end
81
+
82
+ def notification(*args)
83
+ data = @encoder.notification(*args)
84
+ @client.send(data)
85
+ end
86
+
87
+ def batch(*args)
88
+ data = @encoder.batch(*args)
89
+ @client.send(data)
90
+ end
91
+
92
+ # 1) Sync: it'll return the value.
93
+ # 2) Async: you have to add #subscribe
94
+ def method_missing(method, *args, &callback)
95
+ binary = @encoder.encode(method, *args)
96
+
97
+ if @client.async?
98
+ @client.send(binary) do |encoded_result|
99
+ result = @encoder.decode(encoded_result)
100
+ callback.call(result["result"], get_exception(result["error"]))
101
+ end
102
+ else
103
+ ::Kernel.raise("You can't specify callback for a synchronous client.") if callback
104
+
105
+ encoded_result = @client.send(binary)
106
+ result = @encoder.decode(encoded_result)
107
+
108
+ if result.respond_to?(:merge) # Hash, only one result.
109
+ result_or_raise(result)
110
+ else # Array, multiple results.
111
+ result.map do |result|
112
+ result_or_raise(result)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ def result_or_raise(result)
119
+ if error = result["error"]
120
+ exception = self.get_exception(error)
121
+ ::Kernel.raise(exception)
122
+ else
123
+ result["result"]
124
+ end
125
+ end
126
+
127
+ def get_exception(error)
128
+ return unless error
129
+ exception = error["error"]
130
+ resolved_class = ::RPC.full_const_get(exception["class"])
131
+ klass = resolved_class || ::RuntimeError
132
+ message = resolved_class ? exception["message"] : error["message"]
133
+ instance = klass.new(message)
134
+ instance.extend(::RPC::ExceptionsMixin)
135
+ instance.server_backtrace = exception["backtrace"]
136
+ instance
137
+ end
138
+
139
+ def close_connection
140
+ @client.disconnect
141
+ end
142
+ end
143
+ end
File without changes
File without changes
File without changes
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+
3
+ # https://github.com/eventmachine/em-http-request
4
+
5
+ require "eventmachine"
6
+ require "em-http-request"
7
+
8
+ # Note that we support only HTTP POST. JSON-RPC can be done
9
+ # via HTTP GET as well, but since HTTP POST is the preferred
10
+ # method, I decided to implement only it. More info can is here:
11
+ # http://groups.google.com/group/json-rpc/web/json-rpc-over-http
12
+
13
+ module RPC
14
+ module Clients
15
+ class EmHttpRequest
16
+ HEADERS ||= {"Accept" => "application/json-rpc"}
17
+
18
+ def initialize(uri)
19
+ @client = EventMachine::HttpRequest.new(uri)
20
+ @in_progress = 0
21
+ end
22
+
23
+ def connect
24
+ end
25
+
26
+ def disconnect
27
+ end
28
+
29
+ def run(&block)
30
+ EM.run do
31
+ block.call
32
+
33
+ # Note: There's no way how to stop the
34
+ # reactor when there are no remaining events.
35
+ EM.add_periodic_timer(0.1) do
36
+ EM.stop if @in_progress == 0
37
+ end
38
+ end
39
+ end
40
+
41
+ def send(data, &callback)
42
+ request = @client.post(head: HEADERS, body: data)
43
+ @in_progress += 1
44
+ request.callback do |response|
45
+ callback.call(response.response)
46
+ @in_progress -= 1
47
+ end
48
+ end
49
+
50
+ def async?
51
+ true
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+
3
+ require "uri"
4
+
5
+ # Note that we support only HTTP POST. JSON-RPC can be done
6
+ # via HTTP GET as well, but since HTTP POST is the preferred
7
+ # method, I decided to implement only it. More info can is here:
8
+ # http://groups.google.com/group/json-rpc/web/json-rpc-over-http
9
+
10
+ module Net
11
+ autoload :HTTP, "net/http"
12
+ autoload :HTTPS, "net/https"
13
+ end
14
+
15
+ module RPC
16
+ module Clients
17
+ class NetHttp
18
+ HEADERS ||= {"Accept" => "application/json-rpc"}
19
+
20
+ def initialize(uri)
21
+ @uri = URI.parse(uri)
22
+ klass = Net.const_get(@uri.scheme.upcase)
23
+ @client = klass.new(@uri.host, @uri.port)
24
+ end
25
+
26
+ def connect
27
+ @client.start
28
+ end
29
+
30
+ def disconnect
31
+ @client.finish
32
+ end
33
+
34
+ def run(&block)
35
+ self.connect
36
+ block.call
37
+ self.disconnect
38
+ end
39
+
40
+ def send(data)
41
+ path = @uri.path.empty? ? "/" : @uri.path
42
+ @client.post(path, data, HEADERS).body
43
+ end
44
+
45
+ def async?
46
+ false
47
+ end
48
+ end
49
+ end
50
+ end
File without changes
@@ -0,0 +1,138 @@
1
+ # encoding: utf-8
2
+
3
+ # http://en.wikipedia.org/wiki/JSON-RPC
4
+
5
+ begin
6
+ require "yajl/json_gem"
7
+ rescue LoadError
8
+ require "json"
9
+ end
10
+
11
+ module RPC
12
+ module Encoders
13
+ module Json
14
+ # This library works with JSON-RPC 2.0
15
+ # http://groups.google.com/group/json-rpc/web/json-rpc-2-0
16
+ JSON_RPC_VERSION ||= "2.0"
17
+
18
+ # http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html#ErrorObject
19
+ module Errors
20
+ # @note The exceptions are "eaten", because no client should be able to shut the server down.
21
+ def exception(exception, code = 000, message = "#{exception.class}: #{exception.message}")
22
+ unless RPC.development?
23
+ object = {class: exception.class.to_s, message: exception.message, backtrace: exception.backtrace}
24
+ self.error(message, code, object)
25
+ else
26
+ raise exception
27
+ end
28
+ end
29
+
30
+ def error(message, code, object)
31
+ error = {name: "JSONRPCError", code: code, message: message, error: object}
32
+ RPC.log "ERROR #{message} (#{code}) #{error[:error].inspect}"
33
+ error
34
+ end
35
+ end
36
+
37
+ class Request
38
+ attr_reader :data
39
+ def initialize(method, params, id = self.generate_id)
40
+ @data = {jsonrpc: JSON_RPC_VERSION, method: method, params: params}
41
+ @data.merge!(id: id) unless id.nil?
42
+ end
43
+
44
+ def generate_id
45
+ rand(999_999_999_999)
46
+ end
47
+ end
48
+
49
+ class Client
50
+ include Errors
51
+
52
+ def encode(method, *args)
53
+ data = Request.new(method, args).data
54
+ RPC.log "CLIENT ENCODE #{data.inspect}"
55
+ JSON.generate(data)
56
+ end
57
+
58
+ # Notifications are calls which don't require response.
59
+ # They look just the same, but they don't have any id.
60
+ def notification(method, *args)
61
+ data = Request.new(method, args, nil).data
62
+ RPC.log "CLIENT ENCODE NOTIFICATION #{data.inspect}"
63
+ JSON.generate(data)
64
+ end
65
+
66
+ # Provide list of requests and notifications to run on the server.
67
+ #
68
+ # @example
69
+ # ["list", ["/"], ["clear", "logs", nil]]
70
+ def batch(requests)
71
+ data = requests.map { |request| Request.new(*request).data }
72
+ RPC.log "CLIENT ENCODE BATCH #{data.inspect}"
73
+ JSON.generate(data)
74
+ end
75
+
76
+ # TODO: support batch
77
+ def decode(binary)
78
+ object = JSON.parse(binary)
79
+ RPC.log "CLIENT DECODE #{object.inspect}"
80
+ object
81
+ rescue JSON::ParserError => error
82
+ self.exception(error, -32600, "Invalid Request.")
83
+ end
84
+ end
85
+
86
+ class Server
87
+ include Errors
88
+
89
+ def decode(binary)
90
+ object = JSON.parse(binary)
91
+ RPC.log "SERVER DECODE #{object.inspect}"
92
+ object
93
+ rescue JSON::ParserError => error
94
+ # This is supposed to result in HTTP 500.
95
+ raise self.exception(error, -32700, "Parse error.")
96
+ end
97
+
98
+ def execute(encoded_result, subject)
99
+ result = self.decode(encoded_result)
100
+
101
+ if result.respond_to?(:merge) # Hash, only one result.
102
+ self.encode(result_or_error(subject, result))
103
+ else # Array, multiple results.
104
+ self.encode(
105
+ result.map do |result|
106
+ result_or_error(subject, result)
107
+ end
108
+ )
109
+ end
110
+ end
111
+
112
+ def result_or_error(subject, command)
113
+ method, args = command["method"], command["params"]
114
+ result = subject.send(method, *args)
115
+ self.response(result, nil, command["id"])
116
+ rescue NoMethodError => error
117
+ error = self.exception(error, -32601, "Method not found.")
118
+ self.response(nil, error, command["id"])
119
+ rescue ArgumentError => error
120
+ error = self.exception(error, -32602, "Invalid params.")
121
+ self.response(nil, error, command["id"])
122
+ rescue Exception => exception
123
+ error = self.exception(exception)
124
+ self.response(nil, error, command["id"])
125
+ end
126
+
127
+ def response(result, error, id)
128
+ {jsonrpc: JSON_RPC_VERSION, result: result, error: error, id: id}
129
+ end
130
+
131
+ def encode(response)
132
+ RPC.log "SERVER ENCODE: #{response.inspect}"
133
+ JSON.generate(response)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
File without changes
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env gem build
2
+ # encoding: utf-8
3
+
4
+ require "base64"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "rpc"
8
+ s.version = "0.2"
9
+ s.authors = ["Jakub Stastny aka botanicus"]
10
+ s.homepage = "http://github.com/ruby-amqp/rpc"
11
+ s.summary = "Generic RPC library for Ruby."
12
+ s.description = "#{s.summary} Currently it supports JSON-RPC over HTTP, support for AMQP and Redis will follow soon."
13
+ s.cert_chain = nil
14
+ s.email = Base64.decode64("c3Rhc3RueUAxMDFpZGVhcy5jeg==\n")
15
+ s.has_rdoc = true
16
+
17
+ # files
18
+ s.files = `git ls-files`.split("\n")
19
+ s.require_paths = ["lib"]
20
+
21
+ # Ruby version
22
+ s.required_ruby_version = ::Gem::Requirement.new("~> 1.9")
23
+
24
+ begin
25
+ require "changelog"
26
+ rescue LoadError
27
+ warn "You have to have changelog gem installed for post install message"
28
+ else
29
+ s.post_install_message = CHANGELOG.new.version_changes
30
+ end
31
+
32
+ # RubyForge
33
+ s.rubyforge_project = "rpc"
34
+ end
metadata CHANGED
@@ -2,14 +2,14 @@
2
2
  name: rpc
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: "0.1"
5
+ version: "0.2"
6
6
  platform: ruby
7
7
  authors:
8
8
  - Jakub Stastny aka botanicus
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain:
12
- date: 2011-04-13 00:00:00 +01:00
12
+ date: 2011-05-04 00:00:00 +02:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -21,14 +21,32 @@ extensions: []
21
21
 
22
22
  extra_rdoc_files: []
23
23
 
24
- files: []
25
-
24
+ files:
25
+ - .gitignore
26
+ - CHANGELOG
27
+ - Gemfile
28
+ - Gemfile.lock
29
+ - LICENSE
30
+ - README.textile
31
+ - examples/em-http-request-json/client.rb
32
+ - examples/net-http-json/client.rb
33
+ - examples/net-http-json/console.rb
34
+ - examples/server.ru
35
+ - lib/rpc.rb
36
+ - lib/rpc/clients/amqp/coolio.rb
37
+ - lib/rpc/clients/amqp/eventmachine.rb
38
+ - lib/rpc/clients/amqp/socket.rb
39
+ - lib/rpc/clients/em-http-request.rb
40
+ - lib/rpc/clients/net-http.rb
41
+ - lib/rpc/clients/redis.rb
42
+ - lib/rpc/encoders/json.rb
43
+ - lib/rpc/encoders/xml.rb
44
+ - rpc.gemspec
26
45
  has_rdoc: true
27
46
  homepage: http://github.com/ruby-amqp/rpc
28
47
  licenses: []
29
48
 
30
- post_install_message: "[\e[32mVersion 0.1\e[0m] [FEATURE] Net::HTTP client.\n\
31
- [\e[32mVersion 0.1\e[0m] [FEATURE] JSON-RPC encoder.\n"
49
+ post_install_message: "[\e[32mVersion 0.2\e[0m] [FEATURE] Full JSON-RPC 2.0 support (added batch, errors and notifications).\n"
32
50
  rdoc_options: []
33
51
 
34
52
  require_paths: