marilyn-rpc 0.0.1

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