sinatra-rpc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8fcab4c48c8b4a2f1ed7b7dfbd1c3671020b1ae9
4
+ data.tar.gz: a7857d2902d7bb8780f36287738e6b11122d157a
5
+ SHA512:
6
+ metadata.gz: c17ffccd1215344c8fa82c37a1f02996f42310e8c2e410776e81a4abbfd81c3fa968b5f44da3555c3ca035f2d24e4db67bee843af1b35475de7582ee37ce2f22
7
+ data.tar.gz: cd9e615a3eec9dc3941bcbc842d6b009a64eb45dfcaa4f20d116d9e6ddaed3ab27385d2d2fe863b7065e60f640b2d087a2f73f2748ba667788c0e2d42d837cc2
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1 @@
1
+ 2.0.0-p247
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
@@ -0,0 +1 @@
1
+ --markup=markdown
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sinatra-rpc.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Andrea Bernardo Ciddio
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
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
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,113 @@
1
+ [![Build Status](https://travis-ci.org/bcandrea/sinatra-rpc.png?branch=master)](https://travis-ci.org/bcandrea/sinatra-rpc)
2
+ [![Coverage Status](https://coveralls.io/repos/bcandrea/sinatra-rpc/badge.png)](https://coveralls.io/r/bcandrea/sinatra-rpc)
3
+
4
+ # Sinatra::Rpc
5
+
6
+ A simple [Sinatra extension module](http://www.sinatrarb.com/extensions.html) providing the functionality of an
7
+ [RPC server](http://wikipedia.org/wiki/Remote_procedure_call).
8
+
9
+ This module allows exposure of all the public methods of any object via RPC. The only supported serialization
10
+ method is [XML-RPC](http://wikipedia.org/wiki/XML-RPC) at the moment.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ gem 'sinatra-rpc'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install sinatra-rpc
25
+
26
+ ## Usage
27
+
28
+ ### Minimal example
29
+
30
+ The most basic example involves the definition of a _handler_ class first:
31
+
32
+ ```ruby
33
+ class MyHandler
34
+ # A greeting method.
35
+ # @param people [String] the people to greet
36
+ # @return [String] the greeting
37
+ def hello(people)
38
+ "Hello, #{people}!"
39
+ end
40
+ end
41
+ ```
42
+
43
+ The class does not need to include any module or implement a specific API; however, its methods need to be
44
+ properly documented (following the [YARD](http://yardoc.org) conventions) to take advantage of the built-in
45
+ introspection (more on that later).
46
+
47
+ Once the handler is defined, it can be added to a standard Sinatra application by registering the
48
+ `Sinatra::RPC` extension.
49
+
50
+ ```ruby
51
+ require 'spec_helper'
52
+ require 'sinatra/base'
53
+
54
+ class MyApp < Sinatra::Base
55
+ register Sinatra::RPC
56
+ add_rpc_handler MyHandler
57
+
58
+ post '/RPC2' do
59
+ handle_rpc request
60
+ end
61
+ end
62
+ ```
63
+
64
+ This application class will respond to XMLRPC POST requests sent to the '/RPC2' path. It can be easily tested
65
+ with the Ruby [built-in XMLRPC client](http://www.ruby-doc.org/stdlib/libdoc/xmlrpc/rdoc/XMLRPC/Client.html):
66
+
67
+ ```ruby
68
+ require 'xmlrpc/client'
69
+ cli = XMLRPC::Client.new_from_uri 'http://myserver/RPC2'
70
+ cli.http_header_extra = {"accept-encoding" => "identity"}
71
+ cli.call 'hello', 'World' # => this call should return 'Hello, World!'
72
+ ```
73
+
74
+ (the extra header is needed because of a bug in Ruby 2.0.0 and 2.1.0, see https://bugs.ruby-lang.org/issues/8182).
75
+
76
+ ### Namespacing and multiple handlers
77
+
78
+ Of course multiple objects can be registered as handlers. The `add_rpc_handler` method takes an optional
79
+ namespace parameter that can be used to group and organize them.
80
+
81
+ ```ruby
82
+ require 'spec_helper'
83
+ require 'sinatra/base'
84
+
85
+ class MyApp < Sinatra::Base
86
+ register Sinatra::RPC
87
+ add_rpc_handler MyHandler
88
+ add_rpc_handler 'customHandler', CustomHandlerClass.new(:some_argument)
89
+
90
+ post '/RPC2' do
91
+ handle_rpc request
92
+ end
93
+ end
94
+ ```
95
+
96
+ As you can see, handler instances can be passed as well as classes.
97
+
98
+ ### Echo server and introspection
99
+
100
+ The RPC server implements the commonly adopted introspection interface for XML-RPC: the `system.listMethods`,
101
+ `system.methodHelp` and `system.methodSignature` methods are automatically available. The metadata is only extracted
102
+ from the YARD-style comments in the handler classes, so expect inaccurate results if the code is not completely
103
+ documented.
104
+
105
+ Another facility is a simple `test.echo` method, which just return the passed argument.
106
+
107
+ ## Contributing
108
+
109
+ 1. Fork it
110
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
111
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
112
+ 4. Push to the branch (`git push origin my-new-feature`)
113
+ 5. Create new Pull Request
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,99 @@
1
+ require "sinatra/rpc/version"
2
+ require "sinatra/rpc/helpers"
3
+ require "sinatra/rpc/fault"
4
+ require "sinatra/rpc/handler/echo"
5
+ require "sinatra/rpc/handler/introspection"
6
+
7
+ module Sinatra
8
+ # This extension provides the functionality of an RPC server.
9
+ # The resulting server will handle all POST requests to /RPC2 and dispatch methods
10
+ # to the underlying handler objects. For example, calling the 'myHandler.myMethod'
11
+ # method will actually execute
12
+ #
13
+ # my_handler.my_method
14
+ #
15
+ # on the target handler. RPC methods are usually camelcased, so an automatic
16
+ # conversion to and from standard method names with underscores is performed at registration.
17
+ #
18
+ # @example Application class
19
+ # require "sinatra/base"
20
+ # require "sinatra/rpc"
21
+ #
22
+ # class MyApp < Sinatra::Base
23
+ # register Sinatra::RPC
24
+ #
25
+ # # Map custom error codes to Ruby exceptions: this will
26
+ # # generate a class named SomeErrorFault
27
+ # register_rpc_fault :some_error, 399
28
+ #
29
+ # # Add a new sub-handler in the 'myHandler' namespace
30
+ # add_rpc_handler 'myHandler', MyHandlerClass.new(1, 2, 3, 4)
31
+ #
32
+ # # The class name is enough if there is a no-arg constructor
33
+ # add_rpc_handler 'otherHandler', OtherHandler
34
+ #
35
+ # # If the handler namespace is omitted, all the methods are added directly
36
+ # # to the server (empty) namespace
37
+ # add_rpc_handler MyDefaultRPCHandler
38
+ #
39
+ # # Define the RPC endpoint (it must be a POST request)
40
+ # post '/RPC2' do
41
+ # handle_rpc(request)
42
+ # end
43
+ # end
44
+ module RPC
45
+
46
+ # (see Fault.register)
47
+ # @example
48
+ # require "sinatra/base"
49
+ # require "sinatra/rpc"
50
+ #
51
+ # class MyApp < Sinatra::Base
52
+ # register Sinatra::RPC
53
+ # register_rpc_fault :some_error, 399
54
+ # end
55
+ def register_rpc_fault(fault_name, error_code)
56
+ Fault.register fault_name, error_code
57
+ end
58
+
59
+ # Add a new RPC handler object. If specified, the namespace is used as a
60
+ # prefix for all the RPC method calls. All the public methods exposed by the
61
+ # handler object will be made available as RPC methods (with a camelcase name).
62
+ #
63
+ # @param namespace [String] the (optional) namespace for all the exposed methods
64
+ # @param handler [Object, Class] a handler instance, or its class (if a no-arg
65
+ # constructor is available)
66
+ # @example
67
+ # require "sinatra/base"
68
+ # require "sinatra/rpc"
69
+ #
70
+ # class MyApp < Sinatra::Base
71
+ # register Sinatra::RPC
72
+ # add_rpc_handler 'list', MyListInterface
73
+ # add_rpc_handler BaseObject.new(some_status)
74
+ # end
75
+ def add_rpc_handler(namespace = nil, handler)
76
+ handler = handler.new if Class === handler
77
+ settings.rpc_method_index.merge! Utils.rpc_methods namespace, handler
78
+ end
79
+
80
+ # A custom exception raised when a call is made to a non-existent handler or
81
+ # method.
82
+ class NotFound < RuntimeError; end
83
+
84
+ # Callback executed when the app registers this extension module. Here we set
85
+ # the default property values and register standard error codes and handlers.
86
+ def self.registered(app)
87
+ app.helpers Helpers
88
+
89
+ # Initialize the method index
90
+ app.set(:rpc_method_index, {})
91
+
92
+ # Register the echo handler class
93
+ app.add_rpc_handler 'test', Handler::Echo
94
+
95
+ # Register the introspection handler class
96
+ app.add_rpc_handler 'system', Handler::Introspection.new(app)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,39 @@
1
+ require "sinatra/rpc/utils"
2
+ module Sinatra
3
+ module RPC
4
+
5
+ # This module is used to generate all custom RPC errors.
6
+ module Fault
7
+ # Generate a new fault class. The class will be a subclass of RuntimeError,
8
+ # and always include the Fault module.
9
+ #
10
+ # @param fault_name [String, Symbol] An identifier for the fault; if
11
+ # the name is e.g. 'bad_request', a new class named BadRequestFault
12
+ # is generated
13
+ # @param error_code [Integer] A unique numeric code for this fault
14
+ # @example
15
+ # Sinatra::RPC::Fault.register :bad_request, 400
16
+ # Sinatra::RPC::BadRequestFault::CODE # => 400
17
+ # raise Sinatra::RPC::BadRequestFault, "Bad request"
18
+ # RuntimeError === Sinatra::RPC::BadRequestFault.new # => true
19
+ # Sinatra::RPC::Fault === Sinatra::RPC::BadRequestFault.new # => true
20
+ def self.register(fault_name, error_code)
21
+ fault_class = Class.new(RuntimeError) do
22
+ include Sinatra::RPC::Fault
23
+ def code
24
+ self.class.const_get 'CODE'
25
+ end
26
+ end
27
+
28
+ fault_class.const_set 'CODE', error_code
29
+
30
+ class_name = "#{Sinatra::RPC::Utils.camelize fault_name}Fault"
31
+ Sinatra::RPC.const_set(class_name, fault_class)
32
+ end
33
+
34
+ # Register some generic fault codes (can be overridden)
35
+ register :generic, -1
36
+ register :bad_request, 100
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ module Sinatra
2
+ module RPC
3
+ module Handler
4
+ # A simple test handler. Its only purpose is to provide a method that
5
+ # returns the passed string.
6
+ class Echo
7
+
8
+ # A simple echo method. It returns the passed string.
9
+ # @param object [String] the string to return
10
+ # @return [String] the string itself
11
+ def echo(string)
12
+ string
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,44 @@
1
+ module Sinatra
2
+ module RPC
3
+ module Handler
4
+ # The instrospection handler can be used to display metadata about the
5
+ # RPC server. It adds the `listMethods`, `methodSignature` and `methodHelp` RPC methods to
6
+ # the `system` namespace.
7
+ class Introspection
8
+
9
+ # The initializer requires a reference the current application.
10
+ #
11
+ # @param app [Sinatra::Base] the current Sinatra application
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ # List the available methods.
17
+ # @return [Array] the array of methods exposed by this RPC server.
18
+ def list_methods
19
+ index.keys.sort
20
+ end
21
+
22
+ # Return the signature of the given method.
23
+ # @param method_name [String] the method name in the form `handler.methodName`.
24
+ # @return [Array] a list of the form [return, param1, param2, ...].
25
+ def method_signature(method_name)
26
+ index[method_name][:signature]
27
+ end
28
+
29
+ # Return a help for the given method.
30
+ # @param method_name [String] the method name in the form `handler.methodName`.
31
+ # @return [String] a description of the method.
32
+ def method_help(method_name)
33
+ index[method_name][:help]
34
+ end
35
+
36
+ private
37
+
38
+ def index
39
+ @app.settings.rpc_method_index
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,70 @@
1
+ require 'sinatra/rpc/serializer'
2
+ module Sinatra
3
+ module RPC
4
+ # Some methods to include in the app class.
5
+ module Helpers
6
+
7
+ # Generate a serializer instance suitable for the incoming RPC request.
8
+ # (see Sinatra::RPC::Serializer.find)
9
+ def select_serializer(content_type)
10
+ Sinatra::RPC::Serializer.find(content_type).new
11
+ end
12
+
13
+ # Execute an RPC method with the given name and arguments.
14
+ #
15
+ # @param method [String] the RPC method name, e.g. 'system.listMethods'
16
+ # @param arguments [Array] the list of arguments
17
+ # @return [Object] the return value of the method call on the target handler
18
+ def call_rpc_method(method, arguments)
19
+ m = settings.rpc_method_index[method]
20
+ raise Sinatra::RPC::NotFound if m.nil?
21
+ m[:handler].send m[:method], *arguments
22
+ end
23
+
24
+ # Handle RPC requests. This method should be called inside a POST definition.
25
+ # @param request the incoming HTTP request object
26
+ # @example
27
+ # class MyApp < Sinatra:Base
28
+ # register Sinatra::RPC
29
+ # add_rpc_handler MyHandlerClass
30
+ #
31
+ # post '/RPC2' do
32
+ # handle_rpc(request)
33
+ # end
34
+ # end
35
+ def handle_rpc(request)
36
+ # The request/response serializer can be XML-RPC (the default)
37
+ # or any serializer implemented as a subclass of Sinatra::RPC::Serializer::Base.
38
+ # The serializer class is chosen by reading the 'Content-Type' header in the request.
39
+ serializer = select_serializer(request.env['CONTENT_TYPE'])
40
+
41
+ body = request.body.read
42
+
43
+ # An empty request is not acceptable in RPC.
44
+ if body.empty?
45
+ halt 400
46
+ end
47
+
48
+ # Generate the response.
49
+ resp = begin
50
+ # Parse the contents of the request.
51
+ method, arguments = serializer.parse body
52
+
53
+ # Execute the method call.
54
+ call_rpc_method(method, arguments)
55
+ rescue Sinatra::RPC::NotFound
56
+ halt 404
57
+ rescue Sinatra::RPC::Fault => ex
58
+ ex
59
+ rescue ArgumentError => ex
60
+ Sinatra::RPC::BadRequestFault.new(ex.message)
61
+ rescue Exception => ex
62
+ Sinatra::RPC::GenericFault.new("#{ex.class.name}: #{ex.message}")
63
+ end
64
+
65
+ content_type(serializer.content_type, serializer.content_type_options)
66
+ serializer.dump(resp)
67
+ end
68
+ end
69
+ end
70
+ end