threez-rack-rpc 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
data/AUTHORS ADDED
@@ -0,0 +1,3 @@
1
+ * Arto Bendiken <arto.bendiken@gmail.com>
2
+ * Josh Huckabee <joshhuckabee@gmail.com>
3
+ * Vincent Landgraf <vincent.landgraf@1und1.de>
data/CREDITS ADDED
File without changes
data/README ADDED
@@ -0,0 +1,149 @@
1
+ JSON-RPC/XML-RPC Server for Rack Applications
2
+ =============================================
3
+
4
+ This is a [Rack][] middleware that facilitates the creation of
5
+ protocol-agnostic RPC servers. The current implementation provides support
6
+ for [JSON-RPC 2.0][] and [XML-RPC][].
7
+
8
+ * <https://github.com/datagraph/rack-rpc>
9
+
10
+ Features
11
+ --------
12
+
13
+ * Handles JSON-RPC and XML-RPC requests with the same code.
14
+ * Compatible with any Rack application and any Rack-based framework.
15
+ * Provides Rails-style controller filtering for your RPC methods.
16
+
17
+ Examples
18
+ --------
19
+
20
+ ### A basic RPC server
21
+
22
+ require 'rack/rpc'
23
+
24
+ class Server < Rack::RPC::Server
25
+ def hello_world
26
+ "Hello, world!"
27
+ end
28
+ rpc 'hello_world' => :hello_world
29
+ end
30
+
31
+ ### Simple filtering
32
+
33
+ require 'rack/rpc'
34
+
35
+ class Server < Rack::RPC::Server
36
+ before_filter :check_auth
37
+
38
+ def hello_world
39
+ "Hello, world!"
40
+ end
41
+ rpc 'hello_world' => :hello_world
42
+
43
+ private
44
+
45
+ def check_auth
46
+ raise "Not authorized" unless authorized
47
+ end
48
+ end
49
+
50
+ ### Filtering via a proc with more options
51
+
52
+ require 'rack/rpc'
53
+
54
+ class Server < Rack::RPC::Server
55
+ before_filter :check_auth, :only => :super_secret_hello_world do
56
+ raise "Not authorized" unless authorized
57
+ end
58
+
59
+ def hello_world
60
+ "Hello, world!"
61
+ end
62
+ rpc 'hello_world' => :hello_world
63
+
64
+ def super_secret_hello_world
65
+ 'super_secret_hello_world'
66
+ end
67
+ rpc 'super_secret_hello_world' => :super_secret_hello_world
68
+ end
69
+
70
+ ### Running the server
71
+
72
+ # config.ru
73
+ use Rack::RPC::Endpoint, Server.new
74
+
75
+ run MyApplication
76
+
77
+ ### Customizing the default RPC path
78
+
79
+ # config.ru
80
+ use Rack::RPC::Endpoint, Server.new, :path => '/api'
81
+
82
+ run MyApplication
83
+
84
+ More on Filters
85
+ ---------------
86
+
87
+ The `:only` and `:except` options for filters can take a single method or an
88
+ array of methods.
89
+
90
+ You can halt execution in a filter by raising an exception. An error
91
+ response will be returned with the exception's message set as the error
92
+ object's message text.
93
+
94
+ Communicationg with the Server
95
+ ------------------------------
96
+
97
+ By default, methods will only be invoked on `POST` requests to "/rpc". The
98
+ default path can be overridden by sending a `:path` option when creating
99
+ your middleware (see example above).
100
+
101
+ The protocol used is determined by the `CONTENT_TYPE` header
102
+ ("application/xml" and "text/xml" for XML and "application/json" for JSON).
103
+
104
+ Dependencies
105
+ ------------
106
+
107
+ * [Rack](http://rubygems.org/gems/rack) (>= 1.0.0)
108
+ * [Builder](http://rubygems.org/gems/builder) (>= 2.1.2)
109
+
110
+ Installation
111
+ ------------
112
+
113
+ The recommended installation method is via [RubyGems](http://rubygems.org/).
114
+ To install the latest official release of the gem, do:
115
+
116
+ % [sudo] gem install rack-rpc
117
+
118
+ Download
119
+ --------
120
+
121
+ To get a local working copy of the development repository, do:
122
+
123
+ % git clone git://github.com/datagraph/rack-rpc.git
124
+
125
+ Alternatively, download the latest development version as a tarball as
126
+ follows:
127
+
128
+ % wget http://github.com/datagraph/rack-rpc/tarball/master
129
+
130
+ Mailing List
131
+ ------------
132
+
133
+ * <http://groups.google.com/group/rack-devel>
134
+
135
+ Authors
136
+ -------
137
+
138
+ * [Arto Bendiken](https://github.com/bendiken) - <http://ar.to/>
139
+ * [Josh Huckabee](https://github.com/jhuckabee) - <http://joshhuckabee.com/>
140
+
141
+ License
142
+ -------
143
+
144
+ This is free and unencumbered public domain software. For more information,
145
+ see <http://unlicense.org/> or the accompanying {file:UNLICENSE} file.
146
+
147
+ [Rack]: http://rack.rubyforge.org/
148
+ [JSON-RPC 2.0]: http://groups.google.com/group/json-rpc/web/json-rpc-2-0
149
+ [XML-RPC]: http://www.xmlrpc.com/
data/UNLICENSE ADDED
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <http://unlicense.org/>
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.8
@@ -0,0 +1,255 @@
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
+ # Store the request so that it can be accessed from the server methods:
26
+ @server.request = request if @server.respond_to?(:request=)
27
+
28
+ request_body = request.body.read
29
+ request_body.force_encoding(Encoding::UTF_8) if request_body.respond_to?(:force_encoding) # Ruby 1.9+
30
+
31
+ Rack::Response.new([process(request_body, request)], 200, {
32
+ 'Content-Type' => (request.content_type || CONTENT_TYPE).to_s,
33
+ })
34
+ end
35
+
36
+ ##
37
+ # @param [String] input
38
+ # @param [Object] context
39
+ # @return [String]
40
+ def process(input, context = nil)
41
+ response = nil
42
+ begin
43
+ response = case (json = JSON.parse(input))
44
+ when Array then process_batch(json, context)
45
+ when Hash then process_request(json, context)
46
+ end
47
+ rescue JSON::ParserError => exception
48
+ response = JSONRPC::Response.new
49
+ response.error = JSONRPC::ParseError.new(:message => exception.to_s)
50
+ end
51
+ response.to_json + "\n"
52
+ end
53
+
54
+ ##
55
+ # @param [Array<Hash>] batch
56
+ # @param [Object] context
57
+ # @return [Array]
58
+ def process_batch(batch, context = nil)
59
+ batch.map { |struct| process_request(struct, context) }
60
+ end
61
+
62
+ ##
63
+ # @param [Hash] struct
64
+ # @param [Object] context
65
+ # @return [Hash]
66
+ def process_request(struct, context = nil)
67
+ response = JSONRPC::Response.new
68
+ begin
69
+ request = JSONRPC::Request.new(struct, context)
70
+ response.id = request.id
71
+
72
+ raise ::TypeError, "invalid JSON-RPC request" unless request.valid?
73
+
74
+ case operator = @server.class[request.method]
75
+ when nil
76
+ raise ::NoMethodError, "undefined operation `#{request.method}'"
77
+ when Class # a Rack::RPC::Operation subclass
78
+ response.result = operator.new(request).execute
79
+ else
80
+ response.result = @server.__send__(operator, *request.params)
81
+ end
82
+
83
+ rescue ::TypeError => exception # FIXME
84
+ response.error = JSONRPC::ClientError.new(:message => exception.to_s)
85
+
86
+ rescue ::NoMethodError => exception
87
+ response.error = JSONRPC::NoMethodError.new(:message => exception.to_s)
88
+
89
+ rescue ::ArgumentError => exception
90
+ response.error = JSONRPC::ArgumentError.new(:message => exception.to_s)
91
+
92
+ rescue ::Rack::RPC::Error => exception
93
+ response.error = JSONRPC::Error.new(:message => exception.to_s,
94
+ :code => exception.code,
95
+ :data => exception.data)
96
+
97
+ rescue => exception
98
+ response.error = JSONRPC::InternalError.new(:message => exception.to_s)
99
+ end
100
+
101
+ response.to_hash.delete_if { |k, v| v.nil? }
102
+ end
103
+ end # Server
104
+
105
+ ##
106
+ # Base class for JSON-RPC objects.
107
+ class Object
108
+ OPTIONS = {}
109
+
110
+ ##
111
+ # @param [String] input
112
+ # @return [Object]
113
+ def self.parse(input)
114
+ self.new(JSON.parse(input))
115
+ end
116
+
117
+ ##
118
+ # An arbitrary context associated with the object.
119
+ #
120
+ # @return [Object]
121
+ attr_reader :context
122
+
123
+ ##
124
+ # @param [Hash] options
125
+ # @param [Object] context
126
+ # an optional context to associate with the object
127
+ def initialize(options = {}, context = nil)
128
+ options = self.class.const_get(:OPTIONS).merge(options)
129
+ options.each do |k, v|
130
+ instance_variable_set("@#{k}", v)
131
+ end
132
+ @context = context if context
133
+ end
134
+
135
+ ##
136
+ # @return [String]
137
+ def to_json
138
+ to_hash.delete_if { |k, v| v.nil? }.to_json
139
+ end
140
+ end # Object
141
+
142
+ ##
143
+ # JSON-RPC notification objects.
144
+ class Notification < Object
145
+ attr_accessor :version
146
+ attr_accessor :method
147
+ attr_accessor :params
148
+
149
+ ##
150
+ # @return [Boolean]
151
+ def valid?
152
+ true # TODO
153
+ end
154
+
155
+ ##
156
+ # @return [Hash]
157
+ def to_hash
158
+ {
159
+ :jsonrpc => (version || VERSION).to_s,
160
+ :method => method.to_s,
161
+ :params => params ? params.to_a : [], # NOTE: named arguments not supported
162
+ }
163
+ end
164
+ end # Notification
165
+
166
+ ##
167
+ # JSON-RPC request objects.
168
+ class Request < Notification
169
+ attr_accessor :id
170
+
171
+ ##
172
+ # @return [Boolean]
173
+ def valid?
174
+ super && !id.nil?
175
+ end
176
+
177
+ ##
178
+ # @return [Hash]
179
+ def to_hash
180
+ super.merge({
181
+ :id => id,
182
+ })
183
+ end
184
+
185
+ ##
186
+ # @return [Array]
187
+ def to_args
188
+ # used from Operation#initialize
189
+ params
190
+ end
191
+ end # Request
192
+
193
+ ##
194
+ # JSON-RPC response objects.
195
+ class Response < Object
196
+ attr_accessor :version
197
+ attr_accessor :result
198
+ attr_accessor :error
199
+ attr_accessor :id
200
+
201
+ ##
202
+ # @return [Hash]
203
+ def to_hash
204
+ {
205
+ :jsonrpc => (version || VERSION).to_s,
206
+ :result => result,
207
+ :error => error ? error.to_hash : nil,
208
+ :id => id,
209
+ }
210
+ end
211
+ end # Response
212
+
213
+ ##
214
+ # JSON-RPC error objects.
215
+ class Error < Object
216
+ attr_accessor :code
217
+ attr_accessor :message
218
+ attr_accessor :data
219
+
220
+ ##
221
+ # @return [Hash]
222
+ def to_hash
223
+ {
224
+ :code => code.to_i,
225
+ :message => message.to_s,
226
+ :data => data,
227
+ }
228
+ end
229
+ end # Error
230
+
231
+ class ParseError < Error
232
+ OPTIONS = {:code => -32700, :message => "parse error"}
233
+ end # ParseError
234
+
235
+ class ClientError < Error
236
+ OPTIONS = {:code => -32600, :message => "invalid request"}
237
+ end # ClientError
238
+
239
+ class NoMethodError < Error
240
+ OPTIONS = {:code => -32601, :message => "undefined method"}
241
+ end # NoMethodError
242
+
243
+ class ArgumentError < Error
244
+ OPTIONS = {:code => -32602, :message => "invalid arguments"}
245
+ end # ArgumentError
246
+
247
+ class InternalError < Error
248
+ OPTIONS = {:code => -32603, :message => "internal error"}
249
+ end # InternalError
250
+
251
+ class ServerError < Error
252
+ OPTIONS = {:code => -32000, :message => "server error"}
253
+ end # ServerError
254
+ end # JSONRPC
255
+ end # Rack::RPC::Endpoint
@@ -0,0 +1,110 @@
1
+ require 'xmlrpc/server' unless defined?(XMLRPC::BasicServer)
2
+ require 'builder' # @see http://rubygems.org/gems/builder
3
+
4
+ class Rack::RPC::Endpoint
5
+ ##
6
+ # @see http://en.wikipedia.org/wiki/XML-RPC
7
+ # @see http://www.xmlrpc.com/spec
8
+ module XMLRPC
9
+ CONTENT_TYPE = 'application/xml; charset=UTF-8'
10
+
11
+ ##
12
+ # @see http://ruby-doc.org/stdlib/libdoc/xmlrpc/rdoc/classes/XMLRPC/BasicServer.html
13
+ class Server < ::XMLRPC::BasicServer
14
+ ##
15
+ # @param [Rack::RPC::Server] server
16
+ # @param [Hash{Symbol => Object}] options
17
+ def initialize(server, options = {})
18
+ @server = server
19
+ super()
20
+ add_multicall unless options[:multicall] == false
21
+ add_introspection unless options[:introspection] == false
22
+ add_capabilities unless options[:capabilities] == false
23
+ server.class.rpc.each do |rpc_name, method_name|
24
+ add_handler(rpc_name, nil, nil, &server.method(method_name))
25
+ end
26
+ end
27
+
28
+ ##
29
+ # @param [Rack::Request] request
30
+ # @return [Rack::Response]
31
+ def execute(request)
32
+ @server.request = request # Store the request so it can be accessed from the server methods
33
+ request_body = request.body.read
34
+ request_body.force_encoding(Encoding::UTF_8) if request_body.respond_to?(:force_encoding) # Ruby 1.9+
35
+ Rack::Response.new([process(request_body)], 200, {
36
+ 'Content-Type' => (request.content_type || CONTENT_TYPE).to_s,
37
+ })
38
+ end
39
+
40
+ ##
41
+ # Process requests and ensure errors are handled properly
42
+ #
43
+ # @param [String] request body
44
+ def process(request_body)
45
+ begin
46
+ super(request_body)
47
+ rescue RuntimeError => e
48
+ error_response(-32500, "application error - #{e.message}")
49
+ end
50
+ end
51
+
52
+ ##
53
+ # Implements the `system.getCapabilities` standard method, enabling
54
+ # clients to determine whether a given capability is supported by this
55
+ # server.
56
+ #
57
+ # @param [Hash{Symbol => Object}] options
58
+ # @option options [Boolean] :faults_interop (true)
59
+ # whether to indicate support for the XMLRPC-EPI Specification for
60
+ # Fault Code Interoperability
61
+ # @return [void]
62
+ # @see http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
63
+ def add_capabilities(options = {})
64
+ add_handler('system.getCapabilities', %w(struct), '') do
65
+ capabilities = {}
66
+ unless options[:faults_interop] == false
67
+ capabilities['faults_interop'] = {
68
+ 'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
69
+ 'specVersion' => 20010516,
70
+ }
71
+ end
72
+ capabilities
73
+ end
74
+ self
75
+ end
76
+
77
+
78
+ ##
79
+ # Create a valid error response for a given code and message
80
+ #
81
+ # @param [Int] error code
82
+ # @param [String] error message
83
+ # @return [String] response xml string
84
+ def error_response(code, message)
85
+ xml = Builder::XmlMarkup.new
86
+ xml.instruct! :xml, :version=>"1.0"
87
+ xml.methodResponse{
88
+ xml.fault {
89
+ xml.value{
90
+ xml.struct{
91
+ xml.member{
92
+ xml.name('faultCode')
93
+ xml.value{
94
+ xml.int(code)
95
+ }
96
+ }
97
+ xml.member{
98
+ xml.name('faultString')
99
+ xml.value{
100
+ xml.string(message)
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ end
108
+ end # Server
109
+ end # XMLRPC
110
+ end # Rack::RPC::Endpoint
@@ -0,0 +1,47 @@
1
+ module Rack::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 # Rack::RPC
@@ -0,0 +1,27 @@
1
+ require "xmlrpc/parser" unless defined?(XMLRPC::FaultException)
2
+
3
+ module Rack::RPC
4
+ ##
5
+ # Represents an RPC Exception service.
6
+ #
7
+ class Error < XMLRPC::FaultException
8
+ attr_reader :data
9
+
10
+ alias code faultCode
11
+ alias message faultString
12
+ alias to_s faultString
13
+
14
+ ##
15
+ # Creates a new rpc related exception. This is useful if one wants to define
16
+ # custom exceptions.
17
+ # @param [Fixnum] code an error code for the exception (used for mapping
18
+ # on the client side)
19
+ # @param [String] message the message that should be send along
20
+ # @param [Object] a data object that may contain additional data on the
21
+ # error (CAUTION: this is not possible with XMLRPC)
22
+ def initialize(code, message, data = nil)
23
+ @data = data
24
+ super(code, message)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ module Rack::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 # Rack::RPC
@@ -0,0 +1,226 @@
1
+ module Rack::RPC
2
+ ##
3
+ # Represents an RPC server operation.
4
+ #
5
+ class Operation
6
+ ##
7
+ # Defines an operand for this operation class.
8
+ #
9
+ # @example
10
+ # class Multiply < Operation
11
+ # operand :x, Numeric
12
+ # operand :y, Numeric
13
+ # end
14
+ #
15
+ # @param [Symbol, #to_sym] name
16
+ # @param [Class] type
17
+ # @param [Hash{Symbol => Object}] options
18
+ # @option options [Boolean] :optional (false)
19
+ # @option options [Boolean] :nullable (false)
20
+ # @return [void]
21
+ def self.operand(name, type = Object, options = {})
22
+ raise TypeError, "expected a Class, but got #{type.inspect}" unless type.is_a?(Class)
23
+ operands[name.to_sym] = options.merge(:type => type)
24
+ end
25
+
26
+ ##
27
+ # Defines the `#prepare` instance method.
28
+ #
29
+ # @yield
30
+ # @return [void]
31
+ def self.prepare(&block)
32
+ self.send(:define_method, :prepare) do
33
+ begin
34
+ begin
35
+ before_prepare if respond_to?(:before_prepare)
36
+ instance_eval(&block)
37
+ ensure
38
+ after_prepare if respond_to?(:after_prepare)
39
+ end
40
+ self
41
+ rescue Exception => error
42
+ after_error(error) if respond_to?(:after_error)
43
+ raise
44
+ end
45
+ end
46
+ end
47
+
48
+ ##
49
+ # Defines the `#execute` instance method.
50
+ #
51
+ # @yield
52
+ # @return [void]
53
+ def self.execute(&block)
54
+ self.send(:define_method, :execute) do
55
+ begin
56
+ before_execute if respond_to?(:before_execute)
57
+ result = instance_eval(&block)
58
+ after_execute if respond_to?(:after_execute)
59
+ result
60
+ rescue Exception => error
61
+ after_error(error) if respond_to?(:after_error)
62
+ raise
63
+ end
64
+ end
65
+ end
66
+
67
+ ##
68
+ # Returns the operand definitions for this operation class.
69
+ #
70
+ # @return [Hash{Symbol => Hash}]
71
+ def self.operands
72
+ @operands ||= {}
73
+ end
74
+
75
+ ##
76
+ # Returns the arity range for this operation class.
77
+ #
78
+ # @return [Range]
79
+ def self.arity
80
+ @arity ||= begin
81
+ if const_defined?(:ARITY)
82
+ const_get(:ARITY)
83
+ else
84
+ min, max = 0, 0
85
+ operands.each do |name, options|
86
+ min += 1 unless options[:optional].eql?(true)
87
+ max += 1
88
+ end
89
+ Range.new(min, max)
90
+ end
91
+ end
92
+ end
93
+
94
+ ##
95
+ # @return [Object]
96
+ attr_reader :context
97
+ def context() @__context__ end
98
+
99
+ ##
100
+ # Initializes a new operation with the given arguments.
101
+ #
102
+ # @param [Hash{Symbol => Object}] args
103
+ def initialize(args = [])
104
+ case args
105
+ when Array then initialize_from_array(args)
106
+ when Hash then initialize_from_hash(args)
107
+ else case
108
+ when args.respond_to?(:to_args)
109
+ initialize_from_array(args.to_args)
110
+ @__context__ = args.context if args.respond_to?(:context)
111
+ else raise ArgumentError, "expected an Array or Hash, but got #{args.inspect}"
112
+ end
113
+ end
114
+
115
+ initialize! if respond_to?(:initialize!)
116
+ end
117
+
118
+ ##
119
+ # @private
120
+ def initialize_from_array(args)
121
+ validate_arity!(args)
122
+
123
+ pos = 0
124
+ self.class.operands.each do |param_name, param_options|
125
+ arg = args[pos]; pos += 1
126
+
127
+ validate_argument!(arg, param_name, param_options)
128
+
129
+ instance_variable_set("@#{param_name}", arg)
130
+ end
131
+ end
132
+ protected :initialize_from_array
133
+
134
+ ##
135
+ # @private
136
+ def initialize_from_hash(args)
137
+ validate_arity!(args)
138
+
139
+ params = self.class.operands
140
+ args.each do |param_name, arg|
141
+ param_options = params[param_name.to_sym]
142
+
143
+ raise ArgumentError, "unknown parameter name #{param_name.inspect}" unless param_options
144
+ validate_argument!(arg, param_name, param_options)
145
+
146
+ instance_variable_set("@#{param_name}", arg)
147
+ end
148
+ end
149
+ protected :initialize_from_hash
150
+
151
+ ##
152
+ # @private
153
+ def validate_arity!(args)
154
+ unless self.class.arity.include?(argc = args.count)
155
+ raise ArgumentError, (argc < self.class.arity.min) ?
156
+ "too few arguments (#{argc} for #{self.class.arity.min})" :
157
+ "too many arguments (#{argc} for #{self.class.arity.max})"
158
+ end
159
+ end
160
+ protected :validate_arity!
161
+
162
+ ##
163
+ # @private
164
+ def validate_argument!(arg, param_name, param_options)
165
+ return if arg.nil? && (param_options[:nullable] || param_options[:optional])
166
+
167
+ if (param_type = param_options[:type]) && !(param_type === arg)
168
+ case param_type
169
+ when Regexp
170
+ raise TypeError, "expected a String matching #{param_type.inspect}, but got #{arg.inspect}"
171
+ else
172
+ raise TypeError, "expected a #{param_type}, but got #{arg.inspect}"
173
+ end
174
+ end
175
+ end
176
+ protected :validate_argument!
177
+
178
+ ##
179
+ # Prepares this operation.
180
+ #
181
+ # @abstract
182
+ # @return [void] `self`
183
+ def prepare
184
+ self
185
+ end
186
+
187
+ ##
188
+ # Executes this operation.
189
+ #
190
+ # @abstract
191
+ # @return [void]
192
+ def execute
193
+ raise NotImplementedError, "#{self.class}#execute"
194
+ end
195
+
196
+ ##
197
+ # Returns the array representation of the arguments to this operation.
198
+ #
199
+ # @return [Array]
200
+ def to_a
201
+ self.class.operands.inject([]) do |result, (param_name, param_options)|
202
+ result << instance_variable_get("@#{param_name}")
203
+ result
204
+ end
205
+ end
206
+
207
+ ##
208
+ # Returns the hash representation of the arguments to this operation.
209
+ #
210
+ # @return [Hash]
211
+ def to_hash
212
+ self.class.operands.inject({}) do |result, (param_name, param_options)|
213
+ result[param_name] = instance_variable_get("@#{param_name}")
214
+ result
215
+ end
216
+ end
217
+
218
+ ##
219
+ # Returns the JSON representation of the arguments to this operation.
220
+ #
221
+ # @return [String] a serialized JSON object
222
+ def to_json
223
+ to_hash.to_json
224
+ end
225
+ end # Operation
226
+ end # Rack::RPC
@@ -0,0 +1,112 @@
1
+ module Rack::RPC
2
+ ##
3
+ # A base class for RPC servers.
4
+ class Server
5
+ ##
6
+ # @private
7
+ def self.[](rpc_method_name)
8
+ @mappings ||= {}
9
+ @mappings[rpc_method_name]
10
+ end
11
+
12
+ ##
13
+ # @private
14
+ def self.rpc(mappings = {})
15
+ @mappings ||= {}
16
+ if mappings.empty?
17
+ @mappings
18
+ else
19
+ # Store the mappings
20
+ @mappings.merge!(mappings)
21
+
22
+ # Wrap each method so we can inject before and after callbacks
23
+ mappings.each do |rpc_method_name, server_method|
24
+ self.send(:alias_method, :"#{server_method}_without_callbacks", server_method.to_sym)
25
+ self.send(:define_method, server_method) do |*args|
26
+ self.class.hooks[:before].each{|command| command.call(self) if command.callable?(server_method)}
27
+ out = self.send(:"#{server_method}_without_callbacks", *args)
28
+ self.class.hooks[:after].each{|command| command.call(self) if command.callable?(server_method)}
29
+ out
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ def self.hooks
36
+ @hooks ||= {:before => [], :after => []}
37
+ end
38
+
39
+ def self.before_filter(method_sym = nil, options = {}, &block)
40
+ setup_hook(:before, method_sym, options, block)
41
+ end
42
+
43
+ def self.after_filter(method_sym = nil, options = {}, &block)
44
+ setup_hook(:after, method_sym, options, block)
45
+ end
46
+
47
+ # @return [Hash]
48
+ attr_reader :options
49
+
50
+ # @return [Rack::Request]
51
+ attr_accessor :request
52
+
53
+ ##
54
+ # @param [Hash] options
55
+ def initialize(options = {}, &block)
56
+ @options = options.dup
57
+ block.call(self) if block_given?
58
+ end
59
+
60
+ private
61
+
62
+ def self.setup_hook(type, method, options, proc)
63
+ hooks[type] << if proc
64
+ ProcCommand.new(proc, options)
65
+ else
66
+ MethodCommand.new(method, options)
67
+ end
68
+ end
69
+
70
+ end # Server
71
+
72
+ class Command
73
+ attr_reader :options
74
+
75
+ def initialize(options)
76
+ @options = options
77
+
78
+ # Convert non-array options to arrays
79
+ [:only, :except].each do |option|
80
+ options[option] = [options[option]] if !options[option].nil? && !options[option].is_a?(Array)
81
+ end
82
+ end
83
+
84
+ def callable?(method)
85
+ options.empty? ||
86
+ (!options[:only].nil? && options[:only].include?(method)) ||
87
+ (!options[:except].nil? && !options[:except].include?(method))
88
+ end
89
+ end
90
+
91
+ class ProcCommand < Command
92
+ def initialize(proc, options)
93
+ @proc = proc.to_proc
94
+ super(options)
95
+ end
96
+
97
+ def call(server)
98
+ server.instance_eval(&@proc)
99
+ end
100
+ end # ProcCommand
101
+
102
+ class MethodCommand < Command
103
+ def initialize(method, options)
104
+ @method = method.to_sym
105
+ super(options)
106
+ end
107
+
108
+ def call(server)
109
+ server.__send__(@method)
110
+ end
111
+ end # MethodCommand
112
+ end # Rack::RPC
@@ -0,0 +1,67 @@
1
+ module Rack::RPC
2
+ ##
3
+ # Represents an RPC service.
4
+ #
5
+ class Service
6
+ ##
7
+ # Defines an operator for this service class.
8
+ #
9
+ # @example
10
+ # class Calculator < Service
11
+ # operator Add
12
+ # operator Subtract
13
+ # operator Multiply
14
+ # operator Divide
15
+ # end
16
+ #
17
+ # @param [Class] klass
18
+ # @param [Hash{Symbol => Object}] options
19
+ # @return [void]
20
+ def self.operator(klass, options = {})
21
+ raise TypeError, "expected a Class, but got #{klass.inspect}" unless klass.is_a?(Class)
22
+ operators[klass] ||= options
23
+ end
24
+
25
+ ##
26
+ # Returns the operator definitions for this service class.
27
+ #
28
+ # @return [Hash{Class => Hash}]
29
+ def self.operators
30
+ @operators ||= {}
31
+ end
32
+
33
+ ##
34
+ # Returns the operator class for the given operator name.
35
+ #
36
+ # @param [Symbol, #to_sym] operator_name
37
+ # @return [Class]
38
+ def self.[](operator_name)
39
+ operator_name = operator_name.to_sym
40
+ operators.find do |klass, options|
41
+ klass_name = klass.name.split('::').last # TODO: optimize this
42
+ return klass if operator_name.eql?(klass_name.to_sym)
43
+ end
44
+ end
45
+
46
+ ##
47
+ # @param [Symbol, #to_sym] method_name
48
+ # @return [Boolean] `true` or `false`
49
+ def respond_to?(method_name)
50
+ super || (self.class[method_name] ? true : false)
51
+ end
52
+
53
+ ##
54
+ # @param [Symbol, #to_sym] method_name
55
+ # @param [Array] args
56
+ # @return [void]
57
+ # @raise [NoMethodError] if `self` doesn't respond to `method_name`
58
+ def method_missing(method_name, *args, &block)
59
+ if (operator = self.class[method_name]).nil?
60
+ super # raises NoMethodError
61
+ else
62
+ operator.new(args).execute
63
+ end
64
+ end
65
+ protected :method_missing
66
+ end # Service
67
+ end # Rack::RPC
@@ -0,0 +1,22 @@
1
+ module Rack; module RPC
2
+ module VERSION
3
+ MAJOR = 0
4
+ MINOR = 0
5
+ TINY = 8
6
+ EXTRA = nil
7
+
8
+ STRING = [MAJOR, MINOR, TINY, EXTRA].compact.join('.')
9
+
10
+ ##
11
+ # @return [String]
12
+ def self.to_s() STRING end
13
+
14
+ ##
15
+ # @return [String]
16
+ def self.to_str() STRING end
17
+
18
+ ##
19
+ # @return [Array(Integer, Integer, Integer)]
20
+ def self.to_a() [MAJOR, MINOR, TINY] end
21
+ end
22
+ end; end
data/lib/rack/rpc.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'rack' # @see http://rubygems.org/gems/rack
2
+
3
+ module Rack
4
+ module RPC
5
+ autoload :Endpoint, 'rack/rpc/endpoint'
6
+ autoload :Middleware, 'rack/rpc/middleware'
7
+ autoload :Operation, 'rack/rpc/operation'
8
+ autoload :Server, 'rack/rpc/server'
9
+ autoload :Service, 'rack/rpc/service'
10
+ autoload :VERSION, 'rack/rpc/version'
11
+ autoload :Error, 'rack/rpc/error'
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: threez-rack-rpc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.8
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Datagraph
9
+ - Vincent Landgraf
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-05-17 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: builder
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: 2.1.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: 2.1.2
31
+ - !ruby/object:Gem::Dependency
32
+ name: rack
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '1.0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: nokogiri
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: 1.4.4
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: 1.4.4
63
+ - !ruby/object:Gem::Dependency
64
+ name: yard
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: 0.6.0
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: 0.6.0
79
+ - !ruby/object:Gem::Dependency
80
+ name: rspec
81
+ requirement: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: 2.1.0
87
+ type: :development
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: 2.1.0
95
+ - !ruby/object:Gem::Dependency
96
+ name: rack-test
97
+ requirement: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: 0.5.6
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: 0.5.6
111
+ description: Rack middleware for serving up RPC endpoints.
112
+ email: vincent.landgraf@1und1.de
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - AUTHORS
118
+ - CREDITS
119
+ - README
120
+ - UNLICENSE
121
+ - VERSION
122
+ - lib/rack/rpc/endpoint/jsonrpc.rb
123
+ - lib/rack/rpc/endpoint/xmlrpc.rb
124
+ - lib/rack/rpc/endpoint.rb
125
+ - lib/rack/rpc/error.rb
126
+ - lib/rack/rpc/middleware.rb
127
+ - lib/rack/rpc/operation.rb
128
+ - lib/rack/rpc/server.rb
129
+ - lib/rack/rpc/service.rb
130
+ - lib/rack/rpc/version.rb
131
+ - lib/rack/rpc.rb
132
+ homepage: https://github.com/threez/rack-rpc
133
+ licenses:
134
+ - Public Domain
135
+ post_install_message:
136
+ rdoc_options: []
137
+ require_paths:
138
+ - lib
139
+ required_ruby_version: !ruby/object:Gem::Requirement
140
+ none: false
141
+ requirements:
142
+ - - ! '>='
143
+ - !ruby/object:Gem::Version
144
+ version: 1.9.2
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ none: false
147
+ requirements:
148
+ - - ! '>='
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ requirements: []
152
+ rubyforge_project:
153
+ rubygems_version: 1.8.24
154
+ signing_key:
155
+ specification_version: 3
156
+ summary: JSON-RPC/XML-RPC server for Rack applications.
157
+ test_files: []
158
+ has_rdoc: false