marilyn-rpc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,26 @@
1
+ $:.push('../lib')
2
+ require "marilyn-rpc"
3
+ client1 = MarilynRPC::NativeClient.connect_tcp('localhost', 8483)
4
+ TestService1 = client1.for(:test)
5
+ client2 = MarilynRPC::NativeClient.connect_unix("tmp.socket")
6
+ TestService2 = client2.for(:test)
7
+
8
+ require "benchmark"
9
+ n = 10000
10
+ Benchmark.bm(10) do |b|
11
+ b.report("tcp add") do
12
+ n.times { TestService1.add(1, 2) }
13
+ end
14
+ b.report("tcp time") do
15
+ n.times { TestService1.time.to_f }
16
+ end
17
+ b.report("unix add") do
18
+ n.times { TestService2.add(1, 2) }
19
+ end
20
+ b.report("unix time") do
21
+ n.times { TestService2.time.to_f }
22
+ end
23
+ end
24
+
25
+ client1.disconnect
26
+ client2.disconnect
@@ -0,0 +1,22 @@
1
+ require "rubygems"
2
+ $:.push('../lib')
3
+ require "marilyn-rpc"
4
+ require "eventmachine"
5
+
6
+ class TestService < MarilynRPC::Service
7
+ register :test
8
+
9
+ def time
10
+ Time.now
11
+ end
12
+
13
+ def add(a, b)
14
+ a + b
15
+ end
16
+ end
17
+
18
+
19
+ EM.run {
20
+ EM.start_server "localhost", 8483, MarilynRPC::Server
21
+ EM.start_unix_domain_server("tmp.socket", MarilynRPC::Server)
22
+ }
@@ -0,0 +1,4 @@
1
+ %w(version gentleman envelope mails service
2
+ server service_cache client).each do |file|
3
+ require File.join(File.dirname(__FILE__), "marilyn-rpc", file)
4
+ end
@@ -0,0 +1,81 @@
1
+ require 'socket'
2
+
3
+ module MarilynRPC
4
+ class NativeClientProxy
5
+ # Creates a new Native client proxy, were the calls get send to the remote
6
+ # side.
7
+ # @param [Object] path the path that is used to identify the service
8
+ # @param [Socekt] socket the socket to use for communication
9
+ def initialize(path, socket)
10
+ @path, @socket = path, socket
11
+ end
12
+
13
+ # Handler for calls to the remote system
14
+ def method_missing(method, *args, &block)
15
+ # since this client can't multiplex, we set the tag to nil
16
+ @socket.write(MarilynRPC::MailFactory.build_call(nil, @path, method, args))
17
+
18
+ # read the answer of the server back in
19
+ answer = MarilynRPC::Envelope.new
20
+ # read the header to have the size
21
+ answer.parse!(@socket.read(4))
22
+ # so now that we know the site, read the rest of the envelope
23
+ answer.parse!(@socket.read(answer.size))
24
+
25
+ # returns the result part of the mail or raise the exception if there is
26
+ # one
27
+ mail = MarilynRPC::MailFactory.unpack(answer)
28
+ if mail.is_a? MarilynRPC::CallResponseMail
29
+ mail.result
30
+ else
31
+ raise mail.exception
32
+ end
33
+ end
34
+ end
35
+
36
+ # The client that will handle the socket to the remote. The native client is
37
+ # written in pure ruby.
38
+ # @example Using the native client
39
+ # require "marilyn-rpc"
40
+ # client = MarilynRPC::NativeClient.connect_tcp('localhost', 8483)
41
+ # TestService = client.for(:test)
42
+ # TestService.add(1, 2)
43
+ # TestService.time.to_f
44
+ #
45
+ class NativeClient
46
+ # Create a native client for the socket.
47
+ # @param [Socket] socket the socket to manage
48
+ def initialize(socket)
49
+ @socket = socket
50
+ end
51
+
52
+ # Disconnect the client from the remote.
53
+ def disconnect
54
+ @socket.close
55
+ end
56
+
57
+ # Creates a new Proxy Object for the connection.
58
+ # @param [Object] path the path were the service is registered on the remote
59
+ # site
60
+ # @return [MarilynRPC::NativeClientProxy] the proxy obejct that will serve
61
+ # all calls
62
+ def for(path)
63
+ NativeClientProxy.new(path, @socket)
64
+ end
65
+
66
+ # Connect to a unix domain socket.
67
+ # @param [String] path the path to the socket file.
68
+ # @return [MarilynRPC::NativeClient] the cónnected client
69
+ def self.connect_unix(path)
70
+ new(UNIXSocket.new(path))
71
+ end
72
+
73
+ # Connect to a tcp socket.
74
+ # @param [String] host the host to cennect to (e.g. 'localhost')
75
+ # @param [Integer] port the port to connect to (e.g. 8000)
76
+ # @return [MarilynRPC::NativeClient] the cónnected client
77
+ def self.connect_tcp(host, port)
78
+ new(TCPSocket.open(host, port))
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,62 @@
1
+ # This class handles the envelope parsing and encoding which will be used by the
2
+ # server to handle multiple writes into the envelope.
3
+ class MarilynRPC::Envelope
4
+ # size of the envelope content
5
+ attr_reader :size
6
+
7
+ # create a new envelope instance
8
+ # @param [String] content the content of the new envelope
9
+ def initialize(content = nil)
10
+ self.content = content
11
+ end
12
+
13
+ # parses the passed data
14
+ # @param [String] data parses the parsed data
15
+ # @return [String,nil] returns data that is not part of this string
16
+ # (in case the) parser gets more data than the length of the envelope.
17
+ # In case there are no data it will return nil.
18
+ def parse!(data)
19
+ @buffer += data
20
+ overhang = nil
21
+
22
+ # parse the length field of the
23
+ if @size.nil? && @buffer.size >= 4
24
+ # extract 4 bytes length header
25
+ @size = @buffer.slice!(0...4).unpack("N").first
26
+ end
27
+
28
+ # envelope is complete and contains overhang
29
+ if !@size.nil? && @buffer.size > @size
30
+ overhang = @buffer.slice!(@size, @buffer.size)
31
+ end
32
+
33
+ overhang
34
+ end
35
+
36
+ # returns the content of the envelope
37
+ # @note should only be requested when the message if {finished?}.
38
+ # @return [String] the content of the envelope
39
+ def content
40
+ @buffer
41
+ end
42
+
43
+ # sets the content of the envelope. If `nil` was passed an empty string will
44
+ # be set.
45
+ # @param [String] data the new content
46
+ def content=(content)
47
+ @buffer = content || ""
48
+ @size = content.nil? ? nil : content.size
49
+ end
50
+
51
+ # encodes the envelope to be send over the wire
52
+ # @return [String] encoded envelope
53
+ def encode
54
+ [@size].pack("N") + @buffer
55
+ end
56
+
57
+ # checks if the complete envelope was allready parsed
58
+ # @return [Boolean] `true` if the message was parsed
59
+ def finished?
60
+ @buffer.size == @size
61
+ end
62
+ end
@@ -0,0 +1,64 @@
1
+ # The gentleman is a proxy onject that should help to create async responses on
2
+ # the server (service) side. There are two ways to use the gentleman. See the
3
+ # examples.
4
+ #
5
+ # @example Use the gentleman als passed block
6
+ # MarilynRPC::Gentleman.proxy do |helper|
7
+ # EM.system('ls', &helper)
8
+ #
9
+ # lambda do |output,status|
10
+ # output if status.exitstatus == 0
11
+ # end
12
+ # end
13
+ #
14
+ # @example Use the gentleman for responses that are objects
15
+ # conn = EM::Protocols::HttpClient2.connect 'google.com', 80
16
+ # req = conn.get('/')
17
+ # MarilynRPC::Gentleman.new(conn) do |response|
18
+ # response.content
19
+ # end
20
+ #
21
+ # The Gentleman has to be returned by the service method.
22
+ #
23
+ # @attr [Object] connection the connection where the response should be send
24
+ # @attr [Proc] callback the callback that will be called when the deferable
25
+ # was successful
26
+ # @attr [Object] tag the tag that should be used for the response
27
+ class MarilynRPC::Gentleman
28
+ attr_accessor :connection, :callback, :tag
29
+
30
+ # create a new proxy object using a deferable or the passed block.
31
+ # @param [EventMachine::Deferrable] deferable
32
+ def initialize(deferable = nil, &callback)
33
+ @callback = callback
34
+ if deferable
35
+ unless deferable.respond_to? :callback
36
+ raise ArgumentError.new("Wrong type, expected object that responds to #callback!")
37
+ end
38
+ gentleman = self
39
+ deferable.callback { |*args| gentleman.handle(*args) }
40
+ end
41
+ end
42
+
43
+ # Creates a anonymous Gentleman proxy where the helper is exposed to, be able
44
+ # to use the Gentleman in situations where only a callback can be passed.
45
+ def self.proxy(&block)
46
+ gentleman = MarilynRPC::Gentleman.new
47
+ gentleman.callback = block.call(gentleman.helper)
48
+ gentleman
49
+ end
50
+
51
+ # The handler that will send the response to the remote system
52
+ # @param [Object] args the arguments that should be handled by the callback,
53
+ # the reponse of the callback will be send as result
54
+ def handle(*args)
55
+ mail = MarilynRPC::CallResponseMail.new(self.tag, self.callback.call(*args))
56
+ connection.send_mail(mail)
57
+ end
58
+
59
+ # The helper that will be called by the deferable to call {handle} later
60
+ def helper
61
+ gentleman = self
62
+ lambda { |*args| gentleman.handle(*args) }
63
+ end
64
+ end
@@ -0,0 +1,101 @@
1
+ module MarilynRPC
2
+ # Helper that gets mixed into the mail classes to make common things easyer
3
+ module MailHelper
4
+ # generate a new serialize id which can be used in a mail
5
+ # @param [Integer] a number between 0 and 254
6
+ # @param [String] returns an 1 byte string as type id
7
+ def self.type(nr)
8
+ [nr].pack("c")
9
+ end
10
+
11
+ # extracts the real data and ignores the type information
12
+ # @param [String] data the data to extract the mail from
13
+ # @return [String] the extracted data
14
+ def only_data(data)
15
+ data.slice(1, data.size)
16
+ end
17
+
18
+ # serialize the data using marilyns default serializer
19
+ # @param [Object] data the data to encode
20
+ # @param [String] the serialized data
21
+ def serialize(data)
22
+ Marshal.dump(data)
23
+ end
24
+
25
+ # deserializes the passed data to the original objects
26
+ # @param [String] data the serialized data
27
+ # @return [Object] the deserialized object
28
+ def deserialize(data)
29
+ Marshal.load(data)
30
+ end
31
+ end
32
+
33
+ class CallRequestMail < Struct.new(:tag, :path, :method, :args)
34
+ include MarilynRPC::MailHelper
35
+ TYPE = MarilynRPC::MailHelper.type(1)
36
+
37
+ def encode
38
+ TYPE + serialize([self.tag, self.path, self.method, self.args])
39
+ end
40
+
41
+ def decode(data)
42
+ self.tag, self.path, self.method, self.args = deserialize(only_data(data))
43
+ end
44
+ end
45
+
46
+ class CallResponseMail < Struct.new(:tag, :result)
47
+ include MarilynRPC::MailHelper
48
+ TYPE = MarilynRPC::MailHelper.type(2)
49
+
50
+ def encode
51
+ TYPE + serialize([self.tag, self.result])
52
+ end
53
+
54
+ def decode(data)
55
+ self.tag, self.result = deserialize(only_data(data))
56
+ end
57
+ end
58
+
59
+ class ExceptionMail < Struct.new(:exception)
60
+ include MarilynRPC::MailHelper
61
+ TYPE = MarilynRPC::MailHelper.type(3)
62
+
63
+ def encode
64
+ TYPE + serialize(self.exception)
65
+ end
66
+
67
+ def decode(data)
68
+ self.exception = deserialize(only_data(data))
69
+ end
70
+ end
71
+
72
+ # Helper to destiguish between the different mails
73
+ module MailFactory
74
+ # Parses the envelop and generate the correct mail.
75
+ # @param [MarilynRPC::Envelope] envelope the envelope which contains a mail
76
+ # @return [MarilynRPC::CallRequestMail, MarilynRPC::CallResponseMail,
77
+ # MarilynRPC::ExceptionMail] the mail object that was extracted
78
+ def self.unpack(envelope)
79
+ data = envelope.content
80
+ type = data.slice(0, 1)
81
+ case type
82
+ when MarilynRPC::CallRequestMail::TYPE
83
+ mail = MarilynRPC::CallRequestMail.new
84
+ when MarilynRPC::CallResponseMail::TYPE
85
+ mail = MarilynRPC::CallResponseMail.new
86
+ when MarilynRPC::ExceptionMail::TYPE
87
+ mail = MarilynRPC::ExceptionMail.new
88
+ else
89
+ raise ArgumentError.new("The passed type #{type.inspect} is unknown!")
90
+ end
91
+ mail.decode(data)
92
+ mail
93
+ end
94
+
95
+ # builds the binary data for a method call
96
+ def self.build_call(tag, path, method_name, args)
97
+ mail = MarilynRPC::CallRequestMail.new(tag, path, method_name, args)
98
+ MarilynRPC::Envelope.new(mail.encode).encode
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,43 @@
1
+ module MarilynRPC::Server
2
+ # Initialize the first recieving envelope for the connection and create the
3
+ # service cache since each connection gets it's own service instance.
4
+ def post_init
5
+ @envelope = MarilynRPC::Envelope.new
6
+ @cache = MarilynRPC::ServiceCache.new
7
+ end
8
+
9
+ # Handler for the incoming data. EventMachine compatible.
10
+ # @param [String] data the data that should be parsed into envelopes
11
+ def receive_data(data)
12
+ overhang = @envelope.parse!(data)
13
+
14
+ # was massage parsed successfully?
15
+ if @envelope.finished?
16
+ begin
17
+ # grep the request
18
+ answer = @cache.call(MarilynRPC::MailFactory.unpack(@envelope))
19
+ if answer.is_a? MarilynRPC::CallResponseMail
20
+ send_mail(answer)
21
+ else
22
+ answer.connection = self # pass connection for async responses
23
+ end
24
+ rescue => exception
25
+ send_mail(MarilynRPC::ExceptionMail.new(exception))
26
+ end
27
+
28
+ # initialize the next envelope
29
+ @envelope = MarilynRPC::Envelope.new
30
+ receive_data(overhang) if overhang # reenter the data loop
31
+ end
32
+ end
33
+
34
+ # Send a response mail back on the wire of buffer
35
+ # @param [MarilynRPC::ExceptionMail, MarilynRPC::CallResponseMail] mail the
36
+ # mail that should be send to the client
37
+ def send_mail(mail)
38
+ send_data(MarilynRPC::Envelope.new(mail.encode).encode)
39
+ end
40
+
41
+ # Handler for client disconnect
42
+ def unbind; end
43
+ end
@@ -0,0 +1,15 @@
1
+ class MarilynRPC::Service
2
+ # registers the class where is was called as a service
3
+ # @param [String] path the path of the service
4
+ def self.register(path)
5
+ @@registry ||= {}
6
+ @@registry[path] = self
7
+ end
8
+
9
+ # returns all services, that where registered
10
+ # @return [Hash<String, Object>] all registered services with path as key and
11
+ # the registered service as object
12
+ def self.registry
13
+ @@registry || {}
14
+ end
15
+ end
@@ -0,0 +1,47 @@
1
+ class MarilynRPC::ServiceCache
2
+ # creates the service cache
3
+ def initialize
4
+ @services = {}
5
+ end
6
+
7
+ # call a service in the service cache
8
+ # @param [MarilynRPC::CallRequestMail] mail the mail request object, that
9
+ # should be handled
10
+ # @return [MarilynRPC::CallResponseMail, MarilynRPC::Gentleman] either a
11
+ # Gentleman if the response is async or an direct response.
12
+ def call(mail)
13
+ # check if the correct mail object was send
14
+ unless mail.is_a?(MarilynRPC::CallRequestMail)
15
+ raise ArgumentError.new("Expected CallRequestMail Object!")
16
+ end
17
+
18
+ # call the service instance using the argument of the mail
19
+ # puts "call #{mail.method}@#{mail.path} with #{mail.args.inspect}"
20
+ result = lookup(mail.path).send(mail.method, *mail.args)
21
+ # puts "result => #{result.inspect}"
22
+
23
+ # no direct result, register callback
24
+ if result.is_a? MarilynRPC::Gentleman
25
+ result.tag = mail.tag # set the correct mail tag for the answer
26
+ result
27
+ else
28
+ # make response
29
+ MarilynRPC::CallResponseMail.new(mail.tag, result)
30
+ end
31
+ end
32
+
33
+ # get the service from the cache or the service registry
34
+ # @param [Object] path the path to the service (using the regestry)
35
+ # @return [Object] the service object or raises an ArgumentError
36
+ def lookup(path)
37
+ # lookup the service in the cache
38
+ if service = @services[path]
39
+ return service
40
+ # it's not in the cache, so try lookup in the service registry
41
+ elsif service = MarilynRPC::Service.registry[path]
42
+ return (@services[path] = service.new)
43
+ else
44
+ raise ArgumentError.new("Service #{mail.path} unknown!")
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,3 @@
1
+ module MarilynRPC
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,41 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push('lib')
3
+ require "marilyn-rpc/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "marilyn-rpc"
7
+ s.version = MarilynRPC::VERSION.dup
8
+ s.date = "2011-06-04"
9
+ s.summary = "Simple, beautiful event-based RPC"
10
+ s.email = "vilandgr+github@googlemail.com"
11
+ s.homepage = "https://github.com/threez/marilyn-rpc"
12
+ s.authors = ['Vincent Landgraf']
13
+
14
+ s.description = <<-EOF
15
+ A simple, beautiful event-based (EventMachine) RPC service and client library
16
+ EOF
17
+
18
+ dependencies = [
19
+ [:runtime, "eventmachine", "~> 0.12.10"],
20
+ [:development, "rspec", "~> 2.4"],
21
+ ]
22
+
23
+ s.files = Dir['**/*']
24
+ s.test_files = Dir['test/**/*'] + Dir['spec/**/*']
25
+ s.executables = Dir['bin/*'].map { |f| File.basename(f) }
26
+ s.require_paths = ["lib"]
27
+
28
+
29
+ ## Make sure you can build the gem on older versions of RubyGems too:
30
+ s.rubygems_version = "1.3.7"
31
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
32
+ s.specification_version = 3 if s.respond_to? :specification_version
33
+
34
+ dependencies.each do |type, name, version|
35
+ if s.respond_to?("add_#{type}_dependency")
36
+ s.send("add_#{type}_dependency", name, version)
37
+ else
38
+ s.add_dependency(name, version)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,70 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe MarilynRPC::Envelope do
4
+ before(:each) do
5
+ @envelope = MarilynRPC::Envelope.new
6
+ end
7
+
8
+ it "should be possible to parse really small envelope pieces" do
9
+ size = 100
10
+ content = "X" * size
11
+ data = [size].pack("N") + content
12
+ data.each_byte do |byte|
13
+ overhang = @envelope.parse!(byte.chr)
14
+ overhang.should == nil
15
+ end
16
+ @envelope.finished?.should == true
17
+ @envelope.content.should == content
18
+ end
19
+
20
+ it "should be possible to parse complete envelopes" do
21
+ size = 100
22
+ content = "X" * size
23
+ overhang = @envelope.parse!([size].pack("N") + content)
24
+ overhang.should == nil
25
+ @envelope.finished?.should == true
26
+ @envelope.content.should == content
27
+ end
28
+
29
+ it "shold be possible to parse an empty envelope" do
30
+ overhang = @envelope.parse!([0].pack("N"))
31
+ overhang.should == nil
32
+ @envelope.content.should == ""
33
+ @envelope.finished?.should == true
34
+ end
35
+
36
+ it "should be possible to detect overhangs correctly" do
37
+ content = "X" * 120
38
+ overhang = @envelope.parse!([100].pack("N") + content)
39
+ overhang.should == "X" * 20
40
+ @envelope.finished?.should == true
41
+ @envelope.content.should == "X" * 100
42
+ end
43
+
44
+ it "should be possible to lead an envelope from the overhang" do
45
+ content = ([20].pack("N") + ("X" * 20)) * 2
46
+ overhang = @envelope.parse!(content)
47
+ overhang.should == ([20].pack("N") + ("X" * 20))
48
+ @envelope.finished?.should == true
49
+ @envelope.content.should == "X" * 20
50
+ @envelope = MarilynRPC::Envelope.new
51
+ overhang = @envelope.parse!(overhang)
52
+ overhang.should == nil
53
+ @envelope.finished?.should == true
54
+ @envelope.content.should == "X" * 20
55
+ end
56
+
57
+ it "should be possible to encode a envelope correctly" do
58
+ content = "Hallo Welt"
59
+ @envelope.content = content
60
+ @envelope.encode.should == [content.size].pack("N") + content
61
+ end
62
+
63
+ it "should be possible to create a envelope using the initalizer" do
64
+ size = 100
65
+ content = "X" * size
66
+ @envelope = MarilynRPC::Envelope.new(content)
67
+ @envelope.finished?.should == true
68
+ @envelope.content.should == content
69
+ end
70
+ end
@@ -0,0 +1,57 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe MarilynRPC::Gentleman do
4
+ before(:each) do
5
+ module MarilynRPC
6
+ def self.serialize(obj)
7
+ obj
8
+ end
9
+ end
10
+
11
+ class DeferObjectMock
12
+ attr_accessor :callback
13
+
14
+ def callback(&block)
15
+ @callback = block
16
+ end
17
+
18
+ def call(*args)
19
+ @callback.call(*args)
20
+ end
21
+ end
22
+
23
+ class ConnectionMock
24
+ attr_accessor :data
25
+ def send_mail(obj)
26
+ @data = obj
27
+ end
28
+ end
29
+ end
30
+
31
+ it "should be possible to defer a process to a gentleman" do
32
+ deferable = DeferObjectMock.new
33
+
34
+ g = MarilynRPC::Gentleman.new(deferable) do |a, b|
35
+ a + b
36
+ end
37
+ g.connection = ConnectionMock.new
38
+ deferable.call(1, 2)
39
+ g.connection.data.result.should == 3
40
+ end
41
+
42
+ it "should be possible to create a gentleman helper" do
43
+ callback = nil
44
+
45
+ g = MarilynRPC::Gentleman.proxy do |helper|
46
+ callback = helper
47
+
48
+ lambda do |a, b|
49
+ a + b
50
+ end
51
+ end
52
+
53
+ g.connection = ConnectionMock.new
54
+ callback.call(1, 2)
55
+ g.connection.data.result.should == 3
56
+ end
57
+ end
@@ -0,0 +1,69 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "MarilynRPC Mails" do
4
+ describe MarilynRPC::CallRequestMail do
5
+ it "should be possible to serialize and deserialize a request" do
6
+ tag = Time.now.to_f
7
+ mail = MarilynRPC::CallRequestMail.new(
8
+ tag, "/user", :find_by_name, ["mr.x"])
9
+ data = mail.encode
10
+ data.should include("find_by_name")
11
+ mail = MarilynRPC::CallRequestMail.new()
12
+ mail.decode(data)
13
+ mail.tag.should == tag
14
+ mail.path.should == "/user"
15
+ mail.method.should == :find_by_name
16
+ mail.args.should == ["mr.x"]
17
+ end
18
+ end
19
+
20
+ describe MarilynRPC::CallResponseMail do
21
+ before(:each) do
22
+ class User < Struct.new(:name, :gid, :uid); end
23
+ end
24
+
25
+ it "should be possible to serialize and deserialize a request" do
26
+ tag = Time.now.to_f
27
+ result = [
28
+ User.new("mr.x", 1, 1),
29
+ User.new("mr.xxx", 1, 2)
30
+ ]
31
+ mail = MarilynRPC::CallResponseMail.new(tag, result)
32
+ data = mail.encode
33
+ data.should include("mr.xxx")
34
+ mail = MarilynRPC::CallResponseMail.new
35
+ mail.decode(data)
36
+ mail.tag.should == tag
37
+ mail.result.should == result
38
+ end
39
+ end
40
+
41
+ describe MarilynRPC::ExceptionMail do
42
+ it "should be possible to serialize and deserialize a request" do
43
+ exception = nil
44
+ begin
45
+ raise Exception.new "TestError"
46
+ rescue Exception => ex
47
+ exception = ex
48
+ end
49
+ mail = MarilynRPC::ExceptionMail.new(exception)
50
+ data = mail.encode
51
+ data.should include("TestError")
52
+ mail = MarilynRPC::ExceptionMail.new
53
+ mail.decode(data)
54
+ mail.exception.message.should == "TestError"
55
+ mail.exception.backtrace.size.should > 1
56
+ end
57
+ end
58
+
59
+ describe MarilynRPC::MailFactory do
60
+ it "it should be possible to unpack mails encapsulated in envelopes" do
61
+ tag = Time.now.to_f
62
+ mail = MarilynRPC::CallRequestMail.new(
63
+ tag, "/user", :find_by_name, ["mr.x"])
64
+ envelope = MarilynRPC::Envelope.new(mail.encode)
65
+ mail = MarilynRPC::MailFactory.unpack(envelope)
66
+ mail.should be_a(MarilynRPC::CallRequestMail)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,33 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe MarilynRPC::Server do
4
+ before(:each) do
5
+ class ConnectionStub
6
+ include MarilynRPC::Server
7
+ attr_accessor :data
8
+
9
+ def initialize()
10
+ @data = ""
11
+ end
12
+
13
+ def send_data(data)
14
+ @data += data
15
+ end
16
+ end
17
+
18
+ @server = ConnectionStub.new
19
+ end
20
+
21
+ it "should be possible to send multiple letters to the server" do
22
+ @server.post_init
23
+ @server.receive_data(MarilynRPC::Envelope.new("Test1").encode)
24
+ envelope = MarilynRPC::Envelope.new
25
+ envelope.parse!(@server.data)
26
+ mail = MarilynRPC::ExceptionMail.new
27
+ mail.decode(envelope.content)
28
+ mail.exception.message.should == "The passed type \"T\" is unknown!"
29
+ @server.receive_data(MarilynRPC::Envelope.new("Test2").encode)
30
+ @server.receive_data(MarilynRPC::Envelope.new("Test3").encode)
31
+ @server.unbind
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe MarilynRPC::ServiceCache do
4
+ before(:each) do
5
+ class DeferObjectMock
6
+ attr_accessor :callback
7
+
8
+ def callback(&block)
9
+ @callback = block
10
+ end
11
+
12
+ def call(*args)
13
+ @callback.call(*args)
14
+ end
15
+ end
16
+
17
+ class TestService < MarilynRPC::Service
18
+ register "/test"
19
+
20
+ def sync_method(a, b)
21
+ a + b
22
+ end
23
+
24
+ def async_method(a, b)
25
+ calc = DeferObjectMock.new
26
+ MarilynRPC::Gentleman.new(calc) { |result| result }
27
+ end
28
+ end
29
+ @cache = MarilynRPC::ServiceCache.new
30
+ end
31
+
32
+ it "should be possible to call a sync method" do
33
+ mail = MarilynRPC::CallRequestMail.new(1, "/test", :sync_method, [1, 2])
34
+ answer = @cache.call(mail)
35
+ answer.should be_a(MarilynRPC::CallResponseMail)
36
+ answer.result.should == 3
37
+ end
38
+
39
+ it "should be possible to call an async method" do
40
+ mail = MarilynRPC::CallRequestMail.new(1, "/test", :async_method, [1, 2])
41
+ answer = @cache.call(mail)
42
+ answer.should be_a(MarilynRPC::Gentleman)
43
+ answer.tag.should == 1
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe MarilynRPC::Service do
4
+ before(:each) do
5
+ class TestService < MarilynRPC::Service
6
+ register "/test"
7
+ end
8
+
9
+ class ExtendedTestService < MarilynRPC::Service
10
+ register "/test/extended"
11
+ end
12
+ end
13
+
14
+ it "should have to registered services" do
15
+ MarilynRPC::Service.registry.size.should == 2
16
+ MarilynRPC::Service.registry.should == {
17
+ "/test/extended" => ExtendedTestService,
18
+ "/test" => TestService
19
+ }
20
+ end
21
+ end
@@ -0,0 +1,2 @@
1
+ $:.push(File.join(File.dirname(__FILE__), "..", "lib"))
2
+ require "marilyn-rpc"
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: marilyn-rpc
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Vincent Landgraf
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-06-04 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: eventmachine
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 59
30
+ segments:
31
+ - 0
32
+ - 12
33
+ - 10
34
+ version: 0.12.10
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rspec
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 11
46
+ segments:
47
+ - 2
48
+ - 4
49
+ version: "2.4"
50
+ type: :development
51
+ version_requirements: *id002
52
+ description: |
53
+ A simple, beautiful event-based (EventMachine) RPC service and client library
54
+
55
+ email: vilandgr+github@googlemail.com
56
+ executables: []
57
+
58
+ extensions: []
59
+
60
+ extra_rdoc_files: []
61
+
62
+ files:
63
+ - benchmark/client.rb
64
+ - benchmark/server.rb
65
+ - lib/marilyn-rpc/client.rb
66
+ - lib/marilyn-rpc/envelope.rb
67
+ - lib/marilyn-rpc/gentleman.rb
68
+ - lib/marilyn-rpc/mails.rb
69
+ - lib/marilyn-rpc/server.rb
70
+ - lib/marilyn-rpc/service.rb
71
+ - lib/marilyn-rpc/service_cache.rb
72
+ - lib/marilyn-rpc/version.rb
73
+ - lib/marilyn-rpc.rb
74
+ - marilyn-rpc.gemspec
75
+ - spec/envelope_spec.rb
76
+ - spec/gentleman_spec.rb
77
+ - spec/mails_spec.rb
78
+ - spec/server_spec.rb
79
+ - spec/service_cache.rb
80
+ - spec/service_spec.rb
81
+ - spec/spec_helper.rb
82
+ has_rdoc: true
83
+ homepage: https://github.com/threez/marilyn-rpc
84
+ licenses: []
85
+
86
+ post_install_message:
87
+ rdoc_options: []
88
+
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ hash: 3
97
+ segments:
98
+ - 0
99
+ version: "0"
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ hash: 3
106
+ segments:
107
+ - 0
108
+ version: "0"
109
+ requirements: []
110
+
111
+ rubyforge_project:
112
+ rubygems_version: 1.3.7
113
+ signing_key:
114
+ specification_version: 3
115
+ summary: Simple, beautiful event-based RPC
116
+ test_files:
117
+ - spec/envelope_spec.rb
118
+ - spec/gentleman_spec.rb
119
+ - spec/mails_spec.rb
120
+ - spec/server_spec.rb
121
+ - spec/service_cache.rb
122
+ - spec/service_spec.rb
123
+ - spec/spec_helper.rb