protoplasm-em-server 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in protoplasm.gemspec
4
+ gemspec :name => 'protoplasm-em-server'
5
+ gemspec :name => 'protoplasm-blocking-client'
6
+ gem 'bundler_push_host'
7
+ gem 'geminabox'
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Protoplasm
2
+
3
+ Protoplasm makes is easy to define an RPC server/client which is backed by protobuf through Beefcake.
4
+
5
+ ## Defining your service endpoints
6
+
7
+ The current service model is very simple. You can send only one type of protobuf object (the request object). This object must have an enum and a series of optional command fields to allow it to send commands. Here is an example of how to create a request object type:
8
+
9
+ ```ruby
10
+ class Command
11
+ include Beefcake::Message
12
+ module Type
13
+ PING = 1
14
+ UPCASE = 2
15
+ end
16
+ required :type, Type, 1
17
+ optional :ping_command, PingCommand, 2
18
+ optional :upcase_command, UpcaseCommand, 3
19
+ end
20
+ ```
21
+
22
+ In this case, your request object would be able to accept one, and only one subcommand object. Those types are `PingCommand`, `UpcaseCommand` and `EvenCommand`.
23
+
24
+ So, in order to mark this `Command` class as your request class, you'd do the following:
25
+
26
+ ```ruby
27
+ module Types
28
+ include Protoplasm::Types
29
+
30
+ # .. your actual classes would go here
31
+
32
+ request_class Command
33
+ request_type_field :type
34
+ end
35
+ ```
36
+
37
+ ## Defining your response objects
38
+
39
+ Every subcommand can choose to relay back no objects, one object, or stream any number of objects. Those objects must all be of the same type.
40
+
41
+ To define which objects you expect back, you must add the following.
42
+
43
+ ```ruby
44
+ module Types
45
+ rpc_map Command::Type::PING, :ping_command, nil
46
+ rpc_map Command::Type::UPCASE, :upcase_command, UpcaseResponse
47
+ rpc_map Command::Type::EVEN, :even_command, EvenResponse, :streaming => true
48
+ ```
49
+
50
+ In this case, this would define the ping command as returning no object, the upcase command returns a single object, of type `UpcaseResponse`, and the even command return any number of `EvenResponse` objects.
51
+
52
+ ## Server implementation
53
+
54
+ Currently there is a single server implementation `EMServer`, which defines a non-blocking EventMachine based server. To create an `EMServer`, you subclass `Protoplasm::EMServer` and setup handlers for each of your command types. For example, a worker server could look like this:
55
+
56
+ ```ruby
57
+ class Server < Protoplasm::EMServer
58
+ def process_ping_command(ping_command)
59
+ # do nothing
60
+ end
61
+
62
+ def process_upcase_command(upcase_command)
63
+ send_response(:response => cmd.word.upcase)
64
+ end
65
+
66
+ def process_even_command(even_command)
67
+ (1..even_command.top).each do |num|
68
+ send_response(:num => num) if num % 2 == 0
69
+ end
70
+ finish_streaming
71
+ end
72
+ end
73
+ ```
74
+
75
+ This server then could be started with `Server.start(3000)` which would start on port 3000 and process requests.
76
+
77
+ ## Client
78
+
79
+ Currently there is a single client implementation: `BlockingClient`. It defines a blocking `TCPSocket` based client. To create a client for this example, you would do the following.
80
+
81
+ ```ruby
82
+ class Client < Protoplasm::BlockingClient
83
+ def initialize(host, port)
84
+ super(Types, host, port)
85
+ end
86
+
87
+ def ping
88
+ send_request(:ping_command)
89
+ end
90
+
91
+ def upcase(word)
92
+ send_request(:upcase_command, :word => word).response
93
+ end
94
+
95
+ def evens(top)
96
+ send_request(:even_command, :top => top) { |resp| yield resp.num }
97
+ end
98
+ end
99
+ ```
100
+
101
+ Look at the full example under `test/test_helper.rb`.
data/Rakefile ADDED
@@ -0,0 +1,73 @@
1
+ require 'bundler'
2
+ require 'rake/testtask'
3
+ require './lib/protoplasm/version'
4
+
5
+ task :test do
6
+ Rake::TestTask.new do |t|
7
+ Dir['test/*_test.rb'].each{|f| require File.expand_path(f)}
8
+ end
9
+ end
10
+
11
+ def version
12
+ Protoplasm::VERSION
13
+ end
14
+
15
+ def version_tag
16
+ "v#{version}"
17
+ end
18
+
19
+ def tag_version
20
+ system("git tag -a -m \"Version #{version}\" #{version_tag}") or raise("Cannot tag version")
21
+ Bundler.ui.confirm "Tagged #{version_tag}"
22
+ yield
23
+ rescue
24
+ Bundler.ui.error "Untagged #{version_tag} due to error"
25
+ system("git tag -d #{version_tag}") or raise("Cannot untag version")
26
+ raise
27
+ end
28
+
29
+ desc "Release client & server (#{version})"
30
+ task :release do
31
+ tag_version do
32
+ Rake::Task["em_server:release_without_tagging"].invoke
33
+ Rake::Task["blocking_client:release_without_tagging"].invoke
34
+ end
35
+ end
36
+
37
+ desc "Install client & server (#{version})"
38
+ task :install do
39
+ Rake::Task["em_server:install"].invoke
40
+ Rake::Task["blocking_client:install"].invoke
41
+ end
42
+
43
+ desc "Build client & server (#{version})"
44
+ task :build do
45
+ Rake::Task["em_server:build"].invoke
46
+ Rake::Task["blocking_client:build"].invoke
47
+ end
48
+
49
+ namespace :em_server do
50
+ helper = Bundler::GemHelper.new(File.dirname(__FILE__), "protoplasm-em-server")
51
+ helper.install
52
+ helper.instance_eval do
53
+ task :release_without_tagging do
54
+ guard_clean
55
+ built_gem_path = build_gem
56
+ git_push
57
+ rubygem_push(built_gem_path)
58
+ end
59
+ end
60
+ end
61
+
62
+ namespace :blocking_client do
63
+ helper = Bundler::GemHelper.new(File.dirname(__FILE__), "protoplasm-blocking-client")
64
+ helper.install
65
+ helper.instance_eval do
66
+ task :release_without_tagging do
67
+ guard_clean
68
+ built_gem_path = build_gem
69
+ git_push
70
+ rubygem_push(built_gem_path)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,57 @@
1
+ require 'socket'
2
+
3
+ module Protoplasm
4
+ class BlockingClient
5
+ def self.for_types(types)
6
+ cls = Class.new(self)
7
+ cls.class_eval do
8
+ (class << self; self; end).send(:define_method, :_types) { types }
9
+ end
10
+ cls
11
+ end
12
+
13
+ private
14
+ def host_port
15
+ raise "Must be implemented by the client class"
16
+ end
17
+
18
+ def _socket
19
+ host, port = host_port
20
+ @_socket ||= TCPSocket.open(host, port)
21
+ end
22
+
23
+ def send_request(field, *args, &blk)
24
+ s = ''
25
+ type = self.class._types.request_type_for_field(field)
26
+ cmd_class = type.command_class.fields.values.find{|f| f.name == field}
27
+ cmd = self.class._types.request_class.new(self.class._types.request_type_field => type.type, type.field => cmd_class.type.new(*args))
28
+ cmd.encode(s)
29
+ socket = _socket
30
+ socket.write([0, s.size].pack("CQ"))
31
+ socket.write s
32
+ socket.flush
33
+ fetch_objects = true
34
+ obj = nil
35
+ while fetch_objects
36
+ response_code = socket.readpartial(1).unpack("C").first
37
+ case response_code
38
+ when Types::Response::NORMAL
39
+ fetch_objects = !type.void?
40
+ if fetch_objects
41
+ len_buf = ''
42
+ socket.readpartial(8 - len_buf.size, len_buf) while len_buf.size != 8
43
+ len = len_buf.unpack("Q").first
44
+ buf = ''
45
+ socket.readpartial(len - buf.size, buf) until buf.size == len
46
+ obj = type.response_class.decode(buf)
47
+ yield obj if block_given?
48
+ end
49
+ fetch_objects = false unless type.streaming?
50
+ when Types::Response::STOP_STREAMING
51
+ fetch_objects = false
52
+ end
53
+ end
54
+ obj
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,81 @@
1
+ require 'eventmachine'
2
+
3
+ module Protoplasm
4
+ class EMServer < EventMachine::Connection
5
+ def self.for_types(types)
6
+ cls = Class.new(self)
7
+ cls.class_eval do
8
+ (class << self; self; end).send(:define_method, :_types) { types }
9
+ end
10
+ cls
11
+ end
12
+
13
+
14
+ def self.start(port)
15
+ if EM.reactor_running?
16
+ EM::start_server("0.0.0.0", port, self) do |srv|
17
+ yield srv if block_given?
18
+ end
19
+ else
20
+ begin
21
+ EM.run do
22
+ start(port)
23
+ end
24
+ rescue Interrupt
25
+ end
26
+ end
27
+ end
28
+
29
+ def post_init
30
+ @_response_types = []
31
+ @data = ''
32
+ end
33
+
34
+ def receive_data(data)
35
+ @data << data
36
+ data_ready
37
+ end
38
+
39
+ def finish_streaming
40
+ @_response_types.shift
41
+ send_data [Types::Response::STOP_STREAMING].pack("C")
42
+ end
43
+
44
+ def send_void
45
+ @_response_types.shift
46
+ send_data [Types::Response::NORMAL].pack("C")
47
+ end
48
+
49
+ def data_ready
50
+ @control = @data.slice!(0, 1).unpack("C").first unless @control
51
+ case @control
52
+ when Types::Request::NORMAL
53
+ @size = @data.slice!(0, 8).unpack("Q").first unless @size
54
+ if @data.size >= @size
55
+ buf = @data.slice!(0, @size)
56
+ @size, @control = nil, nil
57
+ obj = self.class._types.request_class.decode(buf)
58
+ type = self.class._types.request_type_for_request(obj)
59
+ @_response_types << type
60
+ EM.next_tick do
61
+ send(:"process_#{type.field}", obj.send(type.field))
62
+ end
63
+ data_ready unless @data.empty?
64
+ end
65
+ else
66
+ # illegal char
67
+ close_connection
68
+ end
69
+ end
70
+
71
+ def send_response(*args)
72
+ type = @_response_types.first
73
+ @_response_types.shift unless type.streaming?
74
+ obj = type.response_class.new(*args)
75
+ s = ''
76
+ obj.encode(s)
77
+ send_data [Types::Response::NORMAL, s.size].pack("CQ")
78
+ send_data s
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,62 @@
1
+ require 'beefcake'
2
+
3
+ module Protoplasm
4
+ module Types
5
+ module Request
6
+ NORMAL = 0
7
+ end
8
+
9
+ module Response
10
+ NORMAL = 0
11
+ STOP_STREAMING = 10
12
+ end
13
+
14
+ def self.included(cls)
15
+ cls.extend(ClassMethods)
16
+ end
17
+
18
+ class RequestResponseType < Struct.new(:request_class, :response_class, :type, :field, :streaming)
19
+
20
+ alias_method :streaming?, :streaming
21
+
22
+ def command_class
23
+ request_class
24
+ end
25
+
26
+ def void?
27
+ response_class.nil?
28
+ end
29
+ end
30
+
31
+ module ClassMethods
32
+ def request_class(request_class = nil)
33
+ request_class ? @request_class = request_class : @request_class
34
+ end
35
+
36
+ def request_type(request_obj)
37
+ request_obj.send(@request_type_field)
38
+ end
39
+
40
+ def request_type_field(field = nil)
41
+ field ? @request_type_field = field : @request_type_field
42
+ end
43
+
44
+ def rpc_map(type, field, response_class, opts = nil)
45
+ @response_map_by_field ||= {}
46
+ @response_map_by_type ||= {}
47
+ streaming = opts && opts.key?(:streaming) ? opts[:streaming] : false
48
+ rrt = RequestResponseType.new(@request_class, response_class, type, field, streaming)
49
+ @response_map_by_field[field] = rrt
50
+ @response_map_by_type[type] = rrt
51
+ end
52
+
53
+ def request_type_for_field(field)
54
+ @response_map_by_field[field]
55
+ end
56
+
57
+ def request_type_for_request(req)
58
+ @response_map_by_type[req.send(@request_type_field)]
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,3 @@
1
+ module Protoplasm
2
+ VERSION = "0.1.0"
3
+ end
data/lib/protoplasm.rb ADDED
@@ -0,0 +1,9 @@
1
+ require "protoplasm/version"
2
+
3
+ # Protoplasm
4
+ # This defines BlockingClient and EMServer.
5
+ module Protoplasm
6
+ autoload :BlockingClient, "protoplasm/client/blocking_client"
7
+ autoload :EMServer, "protoplasm/server/em_server"
8
+ autoload :Types, "protoplasm/types/types"
9
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "protoplasm/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "protoplasm-blocking-client"
7
+ s.version = Protoplasm::VERSION
8
+ s.authors = ["Josh Hull"]
9
+ s.email = ["joshbuddy@gmail.com"]
10
+ s.homepage = "https://github.com/bazaarlabs/protoplasm"
11
+ s.summary = %q{A blocking client for a Protoplasm server}
12
+ s.description = %q{A blocking client for a Protoplasm server.}
13
+
14
+ s.rubyforge_project = "protoplasm-blocking-client"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency "beefcake", "~> 0.3.7"
22
+
23
+ s.add_development_dependency 'rake'
24
+ s.add_development_dependency 'minitest', "~> 2.6.1"
25
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "protoplasm/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "protoplasm-em-server"
7
+ s.version = Protoplasm::VERSION
8
+ s.authors = ["Josh Hull"]
9
+ s.email = ["joshbuddy@gmail.com"]
10
+ s.homepage = "https://github.com/bazaarlabs/protoplasm"
11
+ s.summary = %q{A protoplasm server backed by EventMachine}
12
+ s.description = %q{A protoplasm server backed by EventMachine.}
13
+
14
+ s.rubyforge_project = "protoplasm-em-server"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency "eventmachine"
22
+ s.add_dependency "beefcake", "~> 0.3.7"
23
+
24
+ s.add_development_dependency 'rake'
25
+ s.add_development_dependency 'minitest', "~> 2.6.1"
26
+ end
@@ -0,0 +1,46 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe "Protoplasm test server" do
4
+ it "should ping" do
5
+ with_proto_server(ProtoplasmTest::EMServer) do |port|
6
+ client = ProtoplasmTest::Client.new('127.0.0.1', port)
7
+ client.ping
8
+ pass
9
+ end
10
+ end
11
+
12
+ it "should upcase" do
13
+ with_proto_server(ProtoplasmTest::EMServer) do |port|
14
+ client = ProtoplasmTest::Client.new('127.0.0.1', port)
15
+ assert_equal "LOWER", client.upcase('lower')
16
+ end
17
+ end
18
+
19
+ it "should give you even numbers" do
20
+ with_proto_server(ProtoplasmTest::EMServer) do |port|
21
+ client = ProtoplasmTest::Client.new('127.0.0.1', port)
22
+ nums = []
23
+ client.evens(10) do |resp|
24
+ nums << resp
25
+ end
26
+ assert_equal [2, 4, 6, 8], nums
27
+ end
28
+ end
29
+
30
+ it "should allow multiple calls" do
31
+ with_proto_server(ProtoplasmTest::EMServer) do |port|
32
+ client = ProtoplasmTest::Client.new('127.0.0.1', port)
33
+ client.ping
34
+ assert_equal "LOWER", client.upcase('lower')
35
+ assert_equal "UPPER", client.upcase('upper')
36
+ nums = []
37
+ client.evens(10) do |resp|
38
+ nums << resp
39
+ end
40
+ assert_equal [2, 4, 6, 8], nums
41
+ client.ping
42
+ assert_equal "LOWER", client.upcase('lower')
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,122 @@
1
+ require 'rubygems'
2
+ require 'minitest/autorun'
3
+ require 'protoplasm'
4
+ require 'timeout'
5
+
6
+ class ProtoplasmTest
7
+ module Types
8
+ include Protoplasm::Types
9
+
10
+ class PingCommand
11
+ include Beefcake::Message
12
+ end
13
+
14
+ class UpcaseCommand
15
+ include Beefcake::Message
16
+ required :word, :string, 1
17
+ end
18
+
19
+ class EvenCommand
20
+ include Beefcake::Message
21
+ required :top, :uint32, 1
22
+ end
23
+
24
+ class UpcaseResponse
25
+ include Beefcake::Message
26
+ required :response, :string, 1
27
+ end
28
+
29
+ class EvenResponse
30
+ include Beefcake::Message
31
+ required :num, :uint32, 1
32
+ end
33
+
34
+ class Command
35
+ include Beefcake::Message
36
+ module Type
37
+ PING = 1
38
+ UPCASE = 2
39
+ EVEN = 3
40
+ end
41
+ required :type, Type, 1
42
+ optional :ping_command, PingCommand, 2
43
+ optional :upcase_command, UpcaseCommand, 3
44
+ optional :even_command, EvenCommand, 4
45
+ end
46
+
47
+ request_class Command
48
+ request_type_field :type
49
+ rpc_map Command::Type::PING, :ping_command, nil
50
+ rpc_map Command::Type::UPCASE, :upcase_command, UpcaseResponse
51
+ rpc_map Command::Type::EVEN, :even_command, EvenResponse, :streaming => true
52
+ end
53
+
54
+ class EMServer < Protoplasm::EMServer.for_types(Types)
55
+ def process_upcase_command(cmd)
56
+ send_response(:response => cmd.word.upcase)
57
+ end
58
+
59
+ def process_even_command(cmd)
60
+ spit_out_even(1, cmd.top)
61
+ end
62
+
63
+ def process_ping_command(cmd)
64
+ send_void
65
+ end
66
+
67
+ def spit_out_even(cur, top)
68
+ EM.next_tick do
69
+ if cur == top
70
+ finish_streaming
71
+ else
72
+ if cur % 2 == 0
73
+ send_response(:num => cur)
74
+ end
75
+ spit_out_even(cur + 1, top)
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ class Client < Protoplasm::BlockingClient.for_types(Types)
82
+ def initialize(host, port)
83
+ @host, @port = host, port
84
+ end
85
+
86
+ def ping
87
+ send_request(:ping_command)
88
+ end
89
+
90
+ def upcase(word)
91
+ send_request(:upcase_command, :word => word).response
92
+ end
93
+
94
+ def evens(top)
95
+ send_request(:even_command, :top => top) { |resp| yield resp.num }
96
+ end
97
+
98
+ def host_port
99
+ [@host, @port]
100
+ end
101
+ end
102
+ end
103
+
104
+ class MiniTest::Spec
105
+ def with_proto_server(cls)
106
+ port = 19866
107
+ pid = fork { cls.start(port) }
108
+ begin
109
+ Timeout.timeout(10.0) {
110
+ begin
111
+ TCPSocket.open("127.0.0.1", port).close
112
+ rescue
113
+ sleep(0.1)
114
+ retry
115
+ end
116
+ }
117
+ yield port
118
+ ensure
119
+ Process.kill("INT", pid) if pid
120
+ end
121
+ end
122
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: protoplasm-em-server
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Josh Hull
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-11-29 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: &70267574080920 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70267574080920
25
+ - !ruby/object:Gem::Dependency
26
+ name: beefcake
27
+ requirement: &70267574077460 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 0.3.7
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70267574077460
36
+ - !ruby/object:Gem::Dependency
37
+ name: rake
38
+ requirement: &70267574063560 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70267574063560
47
+ - !ruby/object:Gem::Dependency
48
+ name: minitest
49
+ requirement: &70267574051180 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 2.6.1
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70267574051180
58
+ description: A protoplasm server backed by EventMachine.
59
+ email:
60
+ - joshbuddy@gmail.com
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - .gitignore
66
+ - Gemfile
67
+ - README.md
68
+ - Rakefile
69
+ - lib/protoplasm.rb
70
+ - lib/protoplasm/client/blocking_client.rb
71
+ - lib/protoplasm/server/em_server.rb
72
+ - lib/protoplasm/types/types.rb
73
+ - lib/protoplasm/version.rb
74
+ - protoplasm-blocking-client.gemspec
75
+ - protoplasm-em-server.gemspec
76
+ - test/protoplasm_test.rb
77
+ - test/test_helper.rb
78
+ homepage: https://github.com/bazaarlabs/protoplasm
79
+ licenses: []
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ segments:
91
+ - 0
92
+ hash: -1390660265886085013
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ segments:
100
+ - 0
101
+ hash: -1390660265886085013
102
+ requirements: []
103
+ rubyforge_project: protoplasm-em-server
104
+ rubygems_version: 1.8.10
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: A protoplasm server backed by EventMachine
108
+ test_files:
109
+ - test/protoplasm_test.rb
110
+ - test/test_helper.rb