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,31 @@
1
+ module Sinatra
2
+ module RPC
3
+ # All the classes defined in this module represent serialization
4
+ # mechanisms for RPC requests/responses.
5
+ module Serializer
6
+
7
+ # Find the right Serializer::Base subclass for the given
8
+ # Content-Type HTTP request header.
9
+ #
10
+ # @param content_type [String] the value of the Content-Type header
11
+ # @return [Class] a Serializer class that can be used to
12
+ # satisfy the request
13
+ def find(content_type)
14
+ @registry[content_type] or @registry[nil]
15
+ end
16
+
17
+ # Add a serializer for a list of content types to the
18
+ # internal registry of Serializer classes.
19
+ def register(serializer_class, content_types)
20
+ @registry ||= {}
21
+ content_types.each do |c|
22
+ @registry[c] = serializer_class
23
+ end
24
+ end
25
+
26
+ extend self
27
+ end
28
+ end
29
+ end
30
+
31
+ require "sinatra/rpc/serializer/xmlrpc"
@@ -0,0 +1,52 @@
1
+ module Sinatra
2
+ module RPC
3
+ module Serializer
4
+ # The base class for all Serializer instances.
5
+ class Base
6
+
7
+ class << self
8
+ attr_reader :response_content_type
9
+
10
+ # Set the list of content types supported by this serializer.
11
+ # @param content_types [*String] the list of supported content types;
12
+ # if set to `nil`, this serializer is used as a default in case the
13
+ # content type is not specified in the request.
14
+ def content_types(*content_types)
15
+ Sinatra::RPC::Serializer.register self, content_types
16
+ @response_content_type = content_types.compact.first
17
+ end
18
+ end
19
+
20
+ # The content type that should be set in responses. By default
21
+ # it is the first from the list of content types defined by the class.
22
+ # @return [String] the content type to set in the response header.
23
+ def content_type
24
+ self.class.response_content_type
25
+ end
26
+
27
+ # An hash of options to set with the response content type.
28
+ # For example, {charset: 'utf-8'} is used in XML-RPC.
29
+ # The default implementation returns an empty hash.
30
+ def content_type_options
31
+ {}
32
+ end
33
+
34
+ # Parse an incoming RPC request. This method must be implemented by
35
+ # subclasses.
36
+ # @param request [String] the body of the HTTP POST request.
37
+ # @return [Array] an array of the form ['handler.rpcMethod', [arg1, arg2, ...]]
38
+ def parse(request)
39
+ raise NotImplementedError
40
+ end
41
+
42
+ # Convert the response object to a string to be used in the body of
43
+ # the HTTP response. Must be implemented by subclasses.
44
+ # @param response [Object] any response object
45
+ # @return [String] a string representation of the response
46
+ def dump(response)
47
+ raise NotImplementedError
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,37 @@
1
+ require 'sinatra/rpc/serializer/base'
2
+ require 'xmlrpc/marshal'
3
+
4
+ module Sinatra
5
+ module RPC
6
+ module Serializer
7
+ # This class handles XML-RPC calls.
8
+ class XMLRPC < Base
9
+ content_types nil, 'text/xml'
10
+
11
+ # This initializer creates an internal XMLRPC::Marshal instance.
12
+ def initialize
13
+ @xmlrpc = ::XMLRPC::Marshal.new
14
+ end
15
+
16
+ # The charset is set to UTF-8.
17
+ # (see Base#content_type_options)
18
+ def content_type_options
19
+ {charset: 'utf-8'}
20
+ end
21
+
22
+ # (see Base#parse)
23
+ def parse(request)
24
+ @xmlrpc.load_call(request)
25
+ end
26
+
27
+ # (see Base#dump)
28
+ def dump(response)
29
+ if Sinatra::RPC::Fault === response
30
+ response = ::XMLRPC::FaultException.new(response.code, response.message)
31
+ end
32
+ @xmlrpc.dump_response(response)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,107 @@
1
+ require 'method_source'
2
+ module Sinatra
3
+ module RPC
4
+ module Utils
5
+ # Returns the camelcase version of the given string.
6
+ #
7
+ # @param string [String] the string to convert
8
+ # @param uppercase_first_letter [Boolean] set to true if the first letter of the
9
+ # result needs to be uppercase
10
+ # @return [String] the converted string
11
+ # @example
12
+ #
13
+ # Sinatra::RPC::Utils.camelize 'my_test_method', false # => 'myTestMethod'
14
+ # Sinatra::RPC::Utils.camelize 'test_class' # => 'TestClass'
15
+ #
16
+ def camelize(string, uppercase_first_letter = true)
17
+ tokens = string.to_s.split /_+/
18
+ first_token = (uppercase_first_letter ? tokens.first.capitalize : tokens.first.downcase)
19
+ ([first_token] + tokens[1..-1].map(&:capitalize)).join
20
+ end
21
+
22
+ # Converts a camelcase string to its underscore version.
23
+ #
24
+ # @param string [String] the string to convert
25
+ # @return [String] the converted string
26
+ def underscore(string)
27
+ word = string.to_s.dup
28
+ word.gsub!(/::/, '/')
29
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
30
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
31
+ word.tr!("-", "_")
32
+ word.downcase!
33
+ word
34
+ end
35
+
36
+ # Extract the documentation for a given object method.
37
+ #
38
+ # @param method [Method] a method object
39
+ # @return [String] the method help, without initial comment characters (#)
40
+ def method_help(method)
41
+ method.comment.gsub(/^#\s/m, '').strip
42
+ end
43
+
44
+ # Extract the signature for a given object method, as a list of string starting
45
+ # with the return type. The information is retrieved only by parsing the documentation,
46
+ # and the value [['nil']] is returned if no documentation is available.
47
+ #
48
+ # @param method [Method] a method object
49
+ # @return [String] the method signature as a list of strings
50
+ def method_signature(method)
51
+ help = method_help(method)
52
+ params = []
53
+ ret = nil
54
+ help.each_line do |l|
55
+ case l
56
+ when /@param[\s\w]+\[(\w+).*\]/
57
+ params << $1.downcase
58
+ when /@return[\s\w]+\[(\w+).*\]/
59
+ ret = $1.downcase
60
+ end
61
+ end
62
+ ret ||= 'nil'
63
+ [[ret] + params]
64
+ end
65
+
66
+ # Return a hash with all the methods in an object that can be exposed as
67
+ # RPC methods as keys. The values are themselves hashes containing the
68
+ # object, the name of the Ruby method, a method description and its signature.
69
+ #
70
+ # @example
71
+ #
72
+ # # A simple class.
73
+ # class MyClass
74
+ # # A simple method.
75
+ # # @param folks [String] people to greet
76
+ # # @return [String] the greeting
77
+ # def greet(folks); "hi, #{folks}!"; end
78
+ # end
79
+ #
80
+ # Sinatra::RPC::Utils.rpc_methods 'myclass', MyClass.new
81
+ # # => {'myclass.myMethod' => {
82
+ # # handler: <MyClass instance>,
83
+ # # method: :my_method,
84
+ # # help: "A simple method.\n@param folks [String] ...",
85
+ # # signature: [['string', 'string']]
86
+ # # }
87
+ def rpc_methods(namespace = nil, object)
88
+ public_methods = object.class.instance_methods - Object.instance_methods
89
+ method_index = {}
90
+ public_methods.each do |method_name|
91
+ method = object.class.instance_method(method_name)
92
+ rpc_name = camelize method_name, false
93
+ key = [namespace, rpc_name].compact.join '.'
94
+ method_index[key] = {
95
+ handler: object,
96
+ method: method_name,
97
+ help: method_help(method),
98
+ signature: method_signature(method)
99
+ }
100
+ end
101
+ method_index
102
+ end
103
+
104
+ extend self
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,5 @@
1
+ module Sinatra
2
+ module RPC
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sinatra/rpc/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sinatra-rpc"
8
+ spec.version = Sinatra::RPC::VERSION
9
+ spec.authors = ["Andrea Bernardo Ciddio"]
10
+ spec.email = ["bcandrea@gmail.com"]
11
+ spec.description = %q{A Sinatra extension module providing RPC server functionality}
12
+ spec.summary = %q{The Sinatra::RPC module provides an extension that can be used to build RPC endpoints.}
13
+ spec.homepage = "https://github.com/bcandrea/sinatra-rpc"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "method_source", "~> 0.8.0"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec"
26
+ spec.add_development_dependency "yard"
27
+ spec.add_development_dependency "coveralls"
28
+ spec.add_development_dependency "rack-test"
29
+ spec.add_development_dependency "sinatra"
30
+ end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sinatra::RPC::Fault do
4
+ it 'should generate exception classes' do
5
+ Sinatra::RPC::Fault.register :very_bad_request, 400
6
+ Sinatra::RPC::VeryBadRequestFault::CODE.should == 400
7
+ RuntimeError.should === Sinatra::RPC::VeryBadRequestFault.new
8
+ Sinatra::RPC::Fault.should === Sinatra::RPC::VeryBadRequestFault.new
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sinatra::RPC::Handler::Echo do
4
+
5
+ before(:each) do
6
+ @echo = Sinatra::RPC::Handler::Echo.new
7
+ end
8
+
9
+ it 'returns the passed in argument' do
10
+ @echo.echo("a string").should == "a string"
11
+ end
12
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sinatra::RPC::Handler::Introspection do
4
+
5
+ before(:each) do
6
+ index = {
7
+ 'ns.myMethod' => {
8
+ handler: nil,
9
+ method: :my_method,
10
+ help: "This is a test method.",
11
+ signature: [['nil']]
12
+ },
13
+ 'ns.greet' => {
14
+ handler: nil,
15
+ method: :greet,
16
+ help: %q{
17
+ Full method doc for greet.
18
+ @param folks [String] people to greet
19
+ @return [String] the greeting
20
+ }.strip,
21
+ signature: [['string', 'string']]
22
+ },
23
+ 'ns.bye' => {
24
+ handler: nil,
25
+ method: :bye,
26
+ help: %q{
27
+ Partially documented.
28
+ @param people [String] the people
29
+ }.strip,
30
+ signature: [['nil', 'string']]
31
+ },
32
+ 'ns.soLong' => {
33
+ handler: nil,
34
+ method: :so_long,
35
+ help: %q{
36
+ Partially documented.
37
+ @return [String] the result
38
+ }.strip,
39
+ signature: [['string']]
40
+ },
41
+ 'ns.multi' => {
42
+ handler: nil,
43
+ method: :multi,
44
+ help: %q{
45
+ Multiple types.
46
+ @param arg [String, Class] the arg
47
+ @return [Array] the result
48
+ }.strip,
49
+ signature: [['array', 'string']]
50
+ },
51
+ }
52
+ @settings = double('settings', rpc_method_index: index)
53
+ @app = double('app', settings: @settings)
54
+ @intro = Sinatra::RPC::Handler::Introspection.new @app
55
+ end
56
+
57
+ it 'should list all the methods in the correct order' do
58
+ @intro.list_methods.should == %w{ ns.bye ns.greet ns.multi ns.myMethod ns.soLong }
59
+ end
60
+
61
+ it 'should get the method signatures' do
62
+ @intro.method_signature('ns.multi').should == [['array', 'string']]
63
+ @intro.method_signature('ns.greet').should == [['string', 'string']]
64
+ end
65
+
66
+ it 'should get the method help' do
67
+ @intro.method_help('ns.myMethod').should == "This is a test method."
68
+ @intro.method_help('ns.soLong').should == %q{
69
+ Partially documented.
70
+ @return [String] the result
71
+ }.strip
72
+ end
73
+
74
+ end
@@ -0,0 +1,148 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sinatra::RPC::Helpers do
4
+
5
+ before(:all) do
6
+ @registry = Sinatra::RPC::Serializer.instance_variable_get('@registry').dup
7
+ module HelpersTest
8
+ class Serializer1 < Sinatra::RPC::Serializer::Base
9
+ content_types 'application/x-app1', 'application/x-app2'
10
+ end
11
+
12
+ class Serializer2 < Sinatra::RPC::Serializer::Base
13
+ content_types nil, 'application/x-app3'
14
+ end
15
+
16
+ class MyClass
17
+ def my_method(text)
18
+ "this is #{text}"
19
+ end
20
+ end
21
+
22
+ class MyApp
23
+ include Sinatra::RPC::Helpers
24
+ end
25
+ end
26
+ end
27
+
28
+ after(:all) do
29
+ Sinatra::RPC::Serializer.instance_variable_set('@registry', @registry)
30
+ end
31
+
32
+ before(:each) do
33
+ @app = HelpersTest::MyApp.new
34
+ end
35
+
36
+ context '#select_serializer' do
37
+ it 'should generate the correct serializer instance' do
38
+ @app.select_serializer(nil).class.should == HelpersTest::Serializer2
39
+ @app.select_serializer('application/x-app2').class.should == HelpersTest::Serializer1
40
+ end
41
+ end
42
+
43
+ context '#call_rpc_method' do
44
+
45
+ before(:each) do
46
+ @app = HelpersTest::MyApp.new
47
+ @handler = HelpersTest::MyClass.new
48
+ @settings = double(:settings,
49
+ rpc_method_index: {
50
+ 'myClass.myMethod' => {
51
+ method: :my_method,
52
+ handler: @handler,
53
+ help: '',
54
+ signature: [['string', 'string']]
55
+ }
56
+ }
57
+ )
58
+ @app.stub(:settings) {@settings}
59
+ end
60
+
61
+ it 'should call the correct method' do
62
+ @app.call_rpc_method('myClass.myMethod', 'some text').should == 'this is some text'
63
+ end
64
+
65
+ it 'should raise a NotFound exception if the method does not exist' do
66
+ expect {
67
+ @app.call_rpc_method 'myClass.someMethod', [42]
68
+ }.to raise_error(Sinatra::RPC::NotFound)
69
+
70
+ expect {
71
+ @app.call_rpc_method 'anotherMethod', ['arg1', 'arg2']
72
+ }.to raise_error(Sinatra::RPC::NotFound)
73
+ end
74
+ end
75
+
76
+ context '#handle_rpc' do
77
+ before(:each) do
78
+ @app = HelpersTest::MyApp.new
79
+ @serializer = double('serializer',
80
+ parse: ['myClass.myMethod', ['some text']],
81
+ dump: '<the serialized object>',
82
+ content_type: 'test/content-type',
83
+ content_type_options: {})
84
+ @request_body = double('body', read: '<serialized request>')
85
+ @request = double('request', body: @request_body, env: {})
86
+ @app.stub(:content_type)
87
+ @app.stub(:select_serializer) {@serializer}
88
+ @app.stub(:call_rpc_method) { 'this is some text' }
89
+ @app.stub(:halt)
90
+ end
91
+
92
+ it 'should handle a successful RPC call' do
93
+ @app.handle_rpc(@request).should == '<the serialized object>'
94
+ end
95
+
96
+ it 'should reject empty requests with a 400 error' do
97
+ @request_body.stub(:read) {''}
98
+ @app.should_receive(:halt).with(400).once
99
+ @app.handle_rpc(@request)
100
+ end
101
+
102
+ it 'should reply with a 404 when the method does not exist' do
103
+ @app.should_receive(:call_rpc_method).
104
+ with('myClass.myMethod', ['some text']).ordered.once.
105
+ and_raise(Sinatra::RPC::NotFound)
106
+ @app.should_receive(:halt).with(404).ordered.once
107
+ @app.handle_rpc(@request)
108
+ end
109
+
110
+ context 'with errors' do
111
+
112
+ before(:each) do
113
+ @serializer.stub(:dump) do |ex|
114
+ ex.message
115
+ end
116
+ end
117
+
118
+ it 'should transmit back an RPC fault' do
119
+ Sinatra::RPC::Fault.register(:this_is_a_test, 1234)
120
+ ex = Sinatra::RPC::ThisIsATestFault.new 'Problems!'
121
+ @app.should_receive(:call_rpc_method).
122
+ with('myClass.myMethod', ['some text']).ordered.once.and_raise(ex)
123
+ @serializer.should_receive(:dump).with(ex).ordered.once
124
+ @app.handle_rpc(@request).should == 'Problems!'
125
+ end
126
+
127
+ it 'should wrap argument errors' do
128
+
129
+ @app.should_receive(:call_rpc_method).
130
+ with('myClass.myMethod', ['some text']).ordered.once.
131
+ and_raise(ArgumentError.new('bad argument'))
132
+ @serializer.should_receive(:dump).with(
133
+ kind_of(Sinatra::RPC::BadRequestFault)).ordered.once
134
+ @app.handle_rpc(@request).should == 'bad argument'
135
+ end
136
+
137
+ it 'should wrap generic errors' do
138
+ @app.should_receive(:call_rpc_method).
139
+ with('myClass.myMethod', ['some text']).ordered.once.
140
+ and_raise(RuntimeError.new('a runtime error'))
141
+ @serializer.should_receive(:dump).with(
142
+ kind_of(Sinatra::RPC::GenericFault)).ordered.once
143
+ @app.handle_rpc(@request).should == "RuntimeError: a runtime error"
144
+ end
145
+ end
146
+ end
147
+
148
+ end