sinatra-rpc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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