rack-rpc 0.0.0 → 0.0.1
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/VERSION +1 -1
- data/lib/rack/rpc.rb +5 -2
- data/lib/rack/rpc/endpoint.rb +47 -0
- data/lib/rack/rpc/endpoint/jsonrpc.rb +212 -0
- data/lib/rack/rpc/endpoint/xmlrpc.rb +65 -0
- data/lib/rack/rpc/middleware.rb +25 -0
- data/lib/rack/rpc/server.rb +26 -0
- data/lib/rack/rpc/version.rb +1 -1
- metadata +7 -2
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.1
|
data/lib/rack/rpc.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
-
require 'rack'
|
1
|
+
require 'rack' # @see http://rubygems.org/gems/rack
|
2
2
|
|
3
3
|
module Rack
|
4
4
|
module RPC
|
5
|
-
autoload :
|
5
|
+
autoload :Endpoint, 'rack/rpc/endpoint'
|
6
|
+
autoload :Middleware, 'rack/rpc/middleware'
|
7
|
+
autoload :Server, 'rack/rpc/server'
|
8
|
+
autoload :VERSION, 'rack/rpc/version'
|
6
9
|
end
|
7
10
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Rack; module RPC
|
2
|
+
##
|
3
|
+
# A Rack middleware for RPC endpoints.
|
4
|
+
class Endpoint < Middleware
|
5
|
+
autoload :JSONRPC, 'rack/rpc/endpoint/jsonrpc'
|
6
|
+
autoload :XMLRPC, 'rack/rpc/endpoint/xmlrpc'
|
7
|
+
|
8
|
+
DEFAULT_PATH = '/rpc'
|
9
|
+
|
10
|
+
# @return [Server]
|
11
|
+
attr_reader :server
|
12
|
+
def server
|
13
|
+
@server = @server.call if @server.is_a?(Proc)
|
14
|
+
@server
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# @param [#call] app
|
19
|
+
# @param [Server] server
|
20
|
+
# @param [Hash] options
|
21
|
+
def initialize(app, server, options = {})
|
22
|
+
@server = server
|
23
|
+
super(app, options)
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# @return [String]
|
28
|
+
def path
|
29
|
+
@path ||= options[:path] || DEFAULT_PATH
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# @param [Hash] env
|
34
|
+
# @return [Array]
|
35
|
+
def call(env)
|
36
|
+
return super unless env['PATH_INFO'].eql?(path)
|
37
|
+
return super unless env['REQUEST_METHOD'].eql?('POST')
|
38
|
+
case content_type = env['CONTENT_TYPE']
|
39
|
+
when %r(^application/xml), %r(^text/xml)
|
40
|
+
XMLRPC::Server.new(server).execute(Rack::Request.new(env)).finish
|
41
|
+
when %r(^application/json)
|
42
|
+
JSONRPC::Server.new(server).execute(Rack::Request.new(env)).finish
|
43
|
+
else super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end # Endpoint
|
47
|
+
end; end # Rack::RPC
|
@@ -0,0 +1,212 @@
|
|
1
|
+
require 'json' unless defined?(JSON)
|
2
|
+
|
3
|
+
class Rack::RPC::Endpoint
|
4
|
+
##
|
5
|
+
# @see http://en.wikipedia.org/wiki/JSON-RPC
|
6
|
+
# @see http://groups.google.com/group/json-rpc/web/json-rpc-2-0
|
7
|
+
module JSONRPC
|
8
|
+
CONTENT_TYPE = 'application/json; charset=UTF-8'
|
9
|
+
VERSION = 2.0
|
10
|
+
|
11
|
+
##
|
12
|
+
# @see http://groups.google.com/group/json-rpc/web/json-rpc-2-0
|
13
|
+
class Server
|
14
|
+
##
|
15
|
+
# @param [Rack::RPC::Server] server
|
16
|
+
# @param [Hash{Symbol => Object}] options
|
17
|
+
def initialize(server, options = {})
|
18
|
+
@server, @options = server, options.dup
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# @param [Rack::Request] request
|
23
|
+
# @return [Rack::Response]
|
24
|
+
def execute(request)
|
25
|
+
Rack::Response.new([process(request.body.read)], 200, {
|
26
|
+
'Content-Type' => (request.content_type || CONTENT_TYPE).to_s,
|
27
|
+
})
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# @param [String] input
|
32
|
+
# @return [String]
|
33
|
+
def process(input)
|
34
|
+
begin
|
35
|
+
response = case json = JSON.parse(input)
|
36
|
+
when Array then process_batch(json)
|
37
|
+
when Hash then process_request(json)
|
38
|
+
end
|
39
|
+
rescue JSON::ParserError => exception
|
40
|
+
response = JSONRPC::Response.new
|
41
|
+
response.error = JSONRPC::ParseError.new(:message => exception.to_s)
|
42
|
+
end
|
43
|
+
response.to_json + "\n"
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# @param [Array<Hash>] batch
|
48
|
+
# @return [Array]
|
49
|
+
def process_batch(batch)
|
50
|
+
batch.map { |struct| process_request(struct) }
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# @param [Hash] struct
|
55
|
+
# @return [Hash]
|
56
|
+
def process_request(struct)
|
57
|
+
response = JSONRPC::Response.new
|
58
|
+
begin
|
59
|
+
request = JSONRPC::Request.new(struct)
|
60
|
+
response.id = request.id
|
61
|
+
raise ::TypeError, "invalid request" unless request.valid?
|
62
|
+
method = @server.class.rpc[request.method]
|
63
|
+
raise ::NoMethodError, "undefined method `#{request.method}'" unless method && @server.respond_to?(method)
|
64
|
+
response.result = @server.__send__(method, *request.params)
|
65
|
+
rescue ::TypeError => exception # FIXME
|
66
|
+
response.error = JSONRPC::ClientError.new(:message => exception.to_s)
|
67
|
+
rescue ::NoMethodError => exception
|
68
|
+
response.error = JSONRPC::NoMethodError.new(:message => exception.to_s)
|
69
|
+
rescue ::ArgumentError => exception
|
70
|
+
response.error = JSONRPC::ArgumentError.new(:message => exception.to_s)
|
71
|
+
rescue => exception
|
72
|
+
response.error = JSONRPC::InternalError.new(:message => exception.to_s)
|
73
|
+
end
|
74
|
+
response.to_hash.delete_if { |k, v| v.nil? }
|
75
|
+
end
|
76
|
+
end # Server
|
77
|
+
|
78
|
+
##
|
79
|
+
# Base class for JSON-RPC objects.
|
80
|
+
class Object
|
81
|
+
OPTIONS = {}
|
82
|
+
|
83
|
+
##
|
84
|
+
# @parse [String] input
|
85
|
+
# @return [Object]
|
86
|
+
def self.parse(input)
|
87
|
+
self.new(JSON.parse(input))
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# @param [Hash] options
|
92
|
+
def initialize(options = {})
|
93
|
+
options = self.class.const_get(:OPTIONS).merge(options)
|
94
|
+
options.each do |k, v|
|
95
|
+
instance_variable_set("@#{k}", v)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# @return [String]
|
101
|
+
def to_json
|
102
|
+
to_hash.delete_if { |k, v| v.nil? }.to_json
|
103
|
+
end
|
104
|
+
end # Object
|
105
|
+
|
106
|
+
##
|
107
|
+
# JSON-RPC notification objects.
|
108
|
+
class Notification < Object
|
109
|
+
attr_accessor :version
|
110
|
+
attr_accessor :method
|
111
|
+
attr_accessor :params
|
112
|
+
|
113
|
+
##
|
114
|
+
# @return [Boolean]
|
115
|
+
def valid?
|
116
|
+
true # TODO
|
117
|
+
end
|
118
|
+
|
119
|
+
##
|
120
|
+
# @return [Hash]
|
121
|
+
def to_hash
|
122
|
+
{
|
123
|
+
:jsonrpc => (version || VERSION).to_s,
|
124
|
+
:method => method.to_s,
|
125
|
+
:params => params ? params.to_a : [], # NOTE: named arguments not supported
|
126
|
+
}
|
127
|
+
end
|
128
|
+
end # Notification
|
129
|
+
|
130
|
+
##
|
131
|
+
# JSON-RPC request objects.
|
132
|
+
class Request < Notification
|
133
|
+
attr_accessor :id
|
134
|
+
|
135
|
+
##
|
136
|
+
# @return [Boolean]
|
137
|
+
def valid?
|
138
|
+
super && !id.nil?
|
139
|
+
end
|
140
|
+
|
141
|
+
##
|
142
|
+
# @return [Hash]
|
143
|
+
def to_hash
|
144
|
+
super.merge({
|
145
|
+
:id => id,
|
146
|
+
})
|
147
|
+
end
|
148
|
+
end # Request
|
149
|
+
|
150
|
+
##
|
151
|
+
# JSON-RPC response objects.
|
152
|
+
class Response < Object
|
153
|
+
attr_accessor :version
|
154
|
+
attr_accessor :result
|
155
|
+
attr_accessor :error
|
156
|
+
attr_accessor :id
|
157
|
+
|
158
|
+
##
|
159
|
+
# @return [Hash]
|
160
|
+
def to_hash
|
161
|
+
{
|
162
|
+
:jsonrpc => (version || VERSION).to_s,
|
163
|
+
:result => result,
|
164
|
+
:error => error ? error.to_hash : nil,
|
165
|
+
:id => id,
|
166
|
+
}
|
167
|
+
end
|
168
|
+
end # Response
|
169
|
+
|
170
|
+
##
|
171
|
+
# JSON-RPC error objects.
|
172
|
+
class Error < Object
|
173
|
+
attr_accessor :code
|
174
|
+
attr_accessor :message
|
175
|
+
attr_accessor :data
|
176
|
+
|
177
|
+
##
|
178
|
+
# @return [Hash]
|
179
|
+
def to_hash
|
180
|
+
{
|
181
|
+
:code => code.to_i,
|
182
|
+
:message => message.to_s,
|
183
|
+
:data => data,
|
184
|
+
}
|
185
|
+
end
|
186
|
+
end # Error
|
187
|
+
|
188
|
+
class ParseError < Error
|
189
|
+
OPTIONS = {:code => -32700, :message => "parse error"}
|
190
|
+
end # ParseError
|
191
|
+
|
192
|
+
class ClientError < Error
|
193
|
+
OPTIONS = {:code => -32600, :message => "invalid request"}
|
194
|
+
end # ClientError
|
195
|
+
|
196
|
+
class NoMethodError < Error
|
197
|
+
OPTIONS = {:code => -32601, :message => "undefined method"}
|
198
|
+
end # NoMethodError
|
199
|
+
|
200
|
+
class ArgumentError < Error
|
201
|
+
OPTIONS = {:code => -32602, :message => "invalid arguments"}
|
202
|
+
end # ArgumentError
|
203
|
+
|
204
|
+
class InternalError < Error
|
205
|
+
OPTIONS = {:code => -32603, :message => "internal error"}
|
206
|
+
end # InternalError
|
207
|
+
|
208
|
+
class ServerError < Error
|
209
|
+
OPTIONS = {:code => -32000, :message => "server error"}
|
210
|
+
end # ServerError
|
211
|
+
end # JSONRPC
|
212
|
+
end # Rack::RPC::Endpoint
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'xmlrpc/server' unless defined?(XMLRPC::BasicServer)
|
2
|
+
begin
|
3
|
+
require 'builder/xchar' # @see http://rubygems.org/gems/builder
|
4
|
+
rescue LoadError => e
|
5
|
+
end
|
6
|
+
|
7
|
+
class Rack::RPC::Endpoint
|
8
|
+
##
|
9
|
+
# @see http://en.wikipedia.org/wiki/XML-RPC
|
10
|
+
# @see http://www.xmlrpc.com/spec
|
11
|
+
module XMLRPC
|
12
|
+
CONTENT_TYPE = 'application/xml; charset=UTF-8'
|
13
|
+
|
14
|
+
##
|
15
|
+
# @see http://ruby-doc.org/stdlib/libdoc/xmlrpc/rdoc/classes/XMLRPC/BasicServer.html
|
16
|
+
class Server < ::XMLRPC::BasicServer
|
17
|
+
##
|
18
|
+
# @param [Rack::RPC::Server] server
|
19
|
+
# @param [Hash{Symbol => Object}] options
|
20
|
+
def initialize(server, options = {})
|
21
|
+
super()
|
22
|
+
add_multicall unless options[:multicall] == false
|
23
|
+
add_introspection unless options[:introspection] == false
|
24
|
+
add_capabilities unless options[:capabilities] == false
|
25
|
+
server.class.rpc.each do |rpc_name, method_name|
|
26
|
+
add_handler(rpc_name, nil, nil, &server.method(method_name))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# @param [Rack::Request] request
|
32
|
+
# @return [Rack::Response]
|
33
|
+
def execute(request)
|
34
|
+
Rack::Response.new([process(request.body.read)], 200, {
|
35
|
+
'Content-Type' => (request.content_type || CONTENT_TYPE).to_s,
|
36
|
+
})
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Implements the `system.getCapabilities` standard method, enabling
|
41
|
+
# clients to determine whether a given capability is supported by this
|
42
|
+
# server.
|
43
|
+
#
|
44
|
+
# @param [Hash{Symbol => Object}] options
|
45
|
+
# @option options [Boolean] :faults_interop (true)
|
46
|
+
# whether to indicate support for the XMLRPC-EPI Specification for
|
47
|
+
# Fault Code Interoperability
|
48
|
+
# @return [void]
|
49
|
+
# @see http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
|
50
|
+
def add_capabilities(options = {})
|
51
|
+
add_handler('system.getCapabilities', %w(struct), '') do
|
52
|
+
capabilities = {}
|
53
|
+
unless options[:faults_interop] == false
|
54
|
+
capabilities['faults_interop'] = {
|
55
|
+
'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
|
56
|
+
'specVersion' => 20010516,
|
57
|
+
}
|
58
|
+
end
|
59
|
+
capabilities
|
60
|
+
end
|
61
|
+
self
|
62
|
+
end
|
63
|
+
end # Server
|
64
|
+
end # XMLRPC
|
65
|
+
end # Rack::RPC::Endpoint
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Rack; module RPC
|
2
|
+
##
|
3
|
+
# A Rack middleware base class.
|
4
|
+
class Middleware
|
5
|
+
# @return [#call]
|
6
|
+
attr_reader :app
|
7
|
+
|
8
|
+
# @return [Hash]
|
9
|
+
attr_reader :options
|
10
|
+
|
11
|
+
##
|
12
|
+
# @param [#call] app
|
13
|
+
# @param [Hash] options
|
14
|
+
def initialize(app, options = {})
|
15
|
+
@app, @options = app, options.dup
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# @param [Hash] env
|
20
|
+
# @return [Array]
|
21
|
+
def call(env)
|
22
|
+
@app.call(env)
|
23
|
+
end
|
24
|
+
end # Middleware
|
25
|
+
end; end # Rack::RPC
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Rack; module RPC
|
2
|
+
##
|
3
|
+
# A base class for RPC servers.
|
4
|
+
class Server
|
5
|
+
##
|
6
|
+
# @private
|
7
|
+
def self.rpc(mappings = {})
|
8
|
+
@mappings ||= {}
|
9
|
+
if mappings.empty?
|
10
|
+
@mappings
|
11
|
+
else
|
12
|
+
@mappings.merge!(mappings)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Hash]
|
17
|
+
attr_reader :options
|
18
|
+
|
19
|
+
##
|
20
|
+
# @param [Hash] options
|
21
|
+
def initialize(options = {}, &block)
|
22
|
+
@options = options.dup
|
23
|
+
block.call(self) if block_given?
|
24
|
+
end
|
25
|
+
end # Server
|
26
|
+
end; end # Rack::RPC
|
data/lib/rack/rpc/version.rb
CHANGED
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 0.0.
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Datagraph
|
@@ -86,6 +86,11 @@ files:
|
|
86
86
|
- README
|
87
87
|
- UNLICENSE
|
88
88
|
- VERSION
|
89
|
+
- lib/rack/rpc/endpoint/jsonrpc.rb
|
90
|
+
- lib/rack/rpc/endpoint/xmlrpc.rb
|
91
|
+
- lib/rack/rpc/endpoint.rb
|
92
|
+
- lib/rack/rpc/middleware.rb
|
93
|
+
- lib/rack/rpc/server.rb
|
89
94
|
- lib/rack/rpc/version.rb
|
90
95
|
- lib/rack/rpc.rb
|
91
96
|
has_rdoc: false
|