marilyn-rpc 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/benchmark/client.rb +26 -0
- data/benchmark/server.rb +22 -0
- data/lib/marilyn-rpc.rb +4 -0
- data/lib/marilyn-rpc/client.rb +81 -0
- data/lib/marilyn-rpc/envelope.rb +62 -0
- data/lib/marilyn-rpc/gentleman.rb +64 -0
- data/lib/marilyn-rpc/mails.rb +101 -0
- data/lib/marilyn-rpc/server.rb +43 -0
- data/lib/marilyn-rpc/service.rb +15 -0
- data/lib/marilyn-rpc/service_cache.rb +47 -0
- data/lib/marilyn-rpc/version.rb +3 -0
- data/marilyn-rpc.gemspec +41 -0
- data/spec/envelope_spec.rb +70 -0
- data/spec/gentleman_spec.rb +57 -0
- data/spec/mails_spec.rb +69 -0
- data/spec/server_spec.rb +33 -0
- data/spec/service_cache.rb +45 -0
- data/spec/service_spec.rb +21 -0
- data/spec/spec_helper.rb +2 -0
- metadata +123 -0
data/benchmark/client.rb
ADDED
@@ -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
|
data/benchmark/server.rb
ADDED
@@ -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
|
+
}
|
data/lib/marilyn-rpc.rb
ADDED
@@ -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
|
data/marilyn-rpc.gemspec
ADDED
@@ -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
|
data/spec/mails_spec.rb
ADDED
@@ -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
|
data/spec/server_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
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
|