rpc 0.1 → 0.2

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.
@@ -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: