threez-rack-rpc 0.0.8

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/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