protobuf 1.0.1 → 1.1.0.beta0
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.
- data/.gitignore +3 -0
- data/.yardopts +5 -0
- data/Gemfile.lock +25 -10
- data/bin/rpc_server +38 -33
- data/lib/protobuf.rb +22 -3
- data/lib/protobuf/common/logger.rb +6 -8
- data/lib/protobuf/compiler/visitors.rb +8 -9
- data/lib/protobuf/descriptor/descriptor_builder.rb +6 -6
- data/lib/protobuf/ext/eventmachine.rb +2 -4
- data/lib/protobuf/message/message.rb +1 -3
- data/lib/protobuf/rpc/buffer.rb +6 -6
- data/lib/protobuf/rpc/client.rb +59 -21
- data/lib/protobuf/rpc/connector.rb +10 -9
- data/lib/protobuf/rpc/connectors/base.rb +23 -8
- data/lib/protobuf/rpc/connectors/common.rb +155 -0
- data/lib/protobuf/rpc/connectors/em_client.rb +23 -192
- data/lib/protobuf/rpc/connectors/eventmachine.rb +36 -44
- data/lib/protobuf/rpc/connectors/socket.rb +58 -1
- data/lib/protobuf/rpc/error.rb +6 -14
- data/lib/protobuf/rpc/server.rb +72 -99
- data/lib/protobuf/rpc/servers/evented_runner.rb +32 -0
- data/lib/protobuf/rpc/servers/evented_server.rb +29 -0
- data/lib/protobuf/rpc/servers/socket_runner.rb +17 -0
- data/lib/protobuf/rpc/servers/socket_server.rb +145 -0
- data/lib/protobuf/rpc/service.rb +50 -51
- data/lib/protobuf/rpc/stat.rb +2 -2
- data/lib/protobuf/version.rb +1 -1
- data/protobuf.gemspec +9 -4
- data/spec/helper/all.rb +1 -7
- data/spec/helper/server.rb +45 -5
- data/spec/helper/silent_constants.rb +40 -0
- data/spec/proto/test_service.rb +0 -1
- data/spec/proto/test_service_impl.rb +4 -3
- data/spec/spec_helper.rb +19 -6
- data/spec/unit/enum_spec.rb +4 -4
- data/spec/unit/rpc/client_spec.rb +32 -42
- data/spec/unit/rpc/connector_spec.rb +11 -16
- data/spec/unit/rpc/connectors/base_spec.rb +14 -3
- data/spec/unit/rpc/connectors/common_spec.rb +132 -0
- data/spec/unit/rpc/connectors/{eventmachine/client_spec.rb → eventmachine_client_spec.rb} +0 -0
- data/spec/unit/rpc/connectors/socket_spec.rb +49 -0
- data/spec/unit/rpc/servers/evented_server_spec.rb +18 -0
- data/spec/unit/rpc/servers/socket_server_spec.rb +57 -0
- metadata +86 -16
- data/spec/unit/rpc/server_spec.rb +0 -27
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'protobuf/rpc/server'
|
2
|
+
|
3
|
+
module Protobuf
|
4
|
+
module Rpc
|
5
|
+
class EventedServer < EventMachine::Connection
|
6
|
+
include Protobuf::Rpc::Server
|
7
|
+
include Protobuf::Logger::LogMethods
|
8
|
+
|
9
|
+
# Initialize a new read buffer for storing client request info
|
10
|
+
def post_init
|
11
|
+
log_debug '[server] Post init, new read buffer created'
|
12
|
+
|
13
|
+
@stats = Protobuf::Rpc::Stat.new(:SERVER, true)
|
14
|
+
@stats.client = Socket.unpack_sockaddr_in(get_peername)
|
15
|
+
|
16
|
+
@buffer = Protobuf::Rpc::Buffer.new(:read)
|
17
|
+
@did_respond = false
|
18
|
+
end
|
19
|
+
|
20
|
+
# Receive a chunk of data, potentially flushed to handle_client
|
21
|
+
def receive_data(data)
|
22
|
+
log_debug '[server] receive_data: %s' % data
|
23
|
+
@buffer << data
|
24
|
+
handle_client if @buffer.flushed?
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Protobuf
|
2
|
+
module Rpc
|
3
|
+
class SocketRunner
|
4
|
+
|
5
|
+
def self.stop
|
6
|
+
Protobuf::Rpc::SocketServer.stop
|
7
|
+
Protobuf::Logger.info 'Shutdown complete'
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.run(server)
|
11
|
+
Protobuf::Logger.info "SocketServer Running"
|
12
|
+
Protobuf::Rpc::SocketServer.run(server.host, server.port, server.backlog, server.threshold) if !Protobuf::Rpc::SocketServer.running?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'protobuf/rpc/server'
|
2
|
+
|
3
|
+
module Protobuf
|
4
|
+
module Rpc
|
5
|
+
class SocketServer
|
6
|
+
include Protobuf::Rpc::Server
|
7
|
+
include Protobuf::Logger::LogMethods
|
8
|
+
|
9
|
+
class << self
|
10
|
+
|
11
|
+
def cleanup?
|
12
|
+
# every 10 connections run a cleanup routine after closing the response
|
13
|
+
@threads.size > (@thread_threshold - 1) && (@threads.size % @thread_threshold) == 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def cleanup_threads
|
17
|
+
log_debug "[#{log_signature}] Thread cleanup - #{@threads.size} - start"
|
18
|
+
|
19
|
+
@threads = @threads.select do |t|
|
20
|
+
if t[:thread].alive?
|
21
|
+
true
|
22
|
+
else
|
23
|
+
t[:thread].join
|
24
|
+
@working.delete(t[:socket])
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
log_debug "[#{log_signature}] Thread cleanup - #{@threads.size} - complete"
|
30
|
+
end
|
31
|
+
|
32
|
+
def log_signature
|
33
|
+
@log_signature ||= "server-#{self}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def new_worker(socket)
|
37
|
+
Thread.new(socket) do |sock|
|
38
|
+
Protobuf::Rpc::SocketServer::Worker.new(sock) do |s|
|
39
|
+
s.close
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# TODO make listen/backlog part of config
|
45
|
+
def run(host = "127.0.0.1", port = 9399, backlog = 100, thread_threshold = 100)
|
46
|
+
log_debug "[#{log_signature}] Run"
|
47
|
+
@running = true
|
48
|
+
@threads = []
|
49
|
+
@thread_threshold = thread_threshold
|
50
|
+
@server = TCPServer.new(host, port)
|
51
|
+
@server.listen(backlog)
|
52
|
+
@working = []
|
53
|
+
@listen_fds = [@server]
|
54
|
+
|
55
|
+
while running?
|
56
|
+
log_debug "[#{log_signature}] Waiting for connections"
|
57
|
+
|
58
|
+
if ready_cnxns = IO.select(@listen_fds, [], [], 20)
|
59
|
+
cnxns = ready_cnxns.first
|
60
|
+
cnxns.each do |client|
|
61
|
+
case
|
62
|
+
when !running? then
|
63
|
+
# no-op
|
64
|
+
when client == @server then
|
65
|
+
log_debug "[#{log_signature}] Accepted new connection"
|
66
|
+
client, sockaddr = @server.accept
|
67
|
+
@listen_fds << client
|
68
|
+
else
|
69
|
+
if !@working.include?(client)
|
70
|
+
@working << @listen_fds.delete(client)
|
71
|
+
log_debug "[#{log_signature}] Working"
|
72
|
+
@threads << { :thread => new_worker(client),
|
73
|
+
:socket => client }
|
74
|
+
|
75
|
+
cleanup_threads if cleanup?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
else
|
80
|
+
# Run a cleanup if select times out while waiting
|
81
|
+
cleanup_threads if @threads.size > 1
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
rescue
|
86
|
+
# Closing the server causes the loop to raise an exception here
|
87
|
+
raise if running?
|
88
|
+
end
|
89
|
+
|
90
|
+
def running?
|
91
|
+
@running
|
92
|
+
end
|
93
|
+
|
94
|
+
def stop
|
95
|
+
@running = false
|
96
|
+
@server.close
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
class Worker
|
102
|
+
include Protobuf::Rpc::Server
|
103
|
+
include Protobuf::Logger::LogMethods
|
104
|
+
|
105
|
+
def initialize(sock, &complete_cb)
|
106
|
+
@did_response = false
|
107
|
+
@socket = sock
|
108
|
+
@request = Protobuf::Socketrpc::Request.new
|
109
|
+
@response = Protobuf::Socketrpc::Response.new
|
110
|
+
@buffer = Protobuf::Rpc::Buffer.new(:read)
|
111
|
+
@stats = Protobuf::Rpc::Stat.new(:SERVER, true)
|
112
|
+
@complete_cb = complete_cb
|
113
|
+
log_debug "[#{log_signature}] Post init, new read buffer created"
|
114
|
+
|
115
|
+
@stats.client = Socket.unpack_sockaddr_in(@socket.getpeername)
|
116
|
+
@buffer << read_data
|
117
|
+
log_debug "[#{log_signature}] handling request"
|
118
|
+
handle_client if @buffer.flushed?
|
119
|
+
end
|
120
|
+
|
121
|
+
def log_signature
|
122
|
+
@log_signature ||= "server-#{self.class}-#{object_id}"
|
123
|
+
end
|
124
|
+
|
125
|
+
def read_data
|
126
|
+
size_io = StringIO.new
|
127
|
+
|
128
|
+
while((size_reader = @socket.getc) != "-")
|
129
|
+
size_io << size_reader
|
130
|
+
end
|
131
|
+
str_size_io = size_io.string
|
132
|
+
|
133
|
+
"#{str_size_io}-#{@socket.read(str_size_io.to_i)}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def send_data(data)
|
137
|
+
log_debug "[#{log_signature}] sending data : %s" % data
|
138
|
+
@socket.write(data)
|
139
|
+
@socket.flush
|
140
|
+
@complete_cb.call(@socket)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
data/lib/protobuf/rpc/service.rb
CHANGED
@@ -4,10 +4,9 @@ require 'protobuf/rpc/error'
|
|
4
4
|
|
5
5
|
module Protobuf
|
6
6
|
module Rpc
|
7
|
-
|
8
7
|
# Object to encapsulate the request/response types for a given service method
|
9
8
|
#
|
10
|
-
RpcMethod = Struct.new
|
9
|
+
RpcMethod = Struct.new("RpcMethod", :service, :method, :request_type, :response_type)
|
11
10
|
|
12
11
|
class Service
|
13
12
|
include Protobuf::Logger::LogMethods
|
@@ -27,34 +26,32 @@ module Protobuf
|
|
27
26
|
|
28
27
|
# You MUST add the method name to this list if you are adding
|
29
28
|
# instance methods below, otherwise stuff will definitely break
|
30
|
-
NON_RPC_METHODS = %w( rpcs call_rpc on_rpc_failed rpc_failed request response method_missing async_responder on_send_response send_response
|
29
|
+
NON_RPC_METHODS = %w( rpcs call_rpc on_rpc_failed rpc_failed request response method_missing async_responder on_send_response send_response log_signature )
|
31
30
|
|
32
31
|
# Override methods being added to the class
|
33
32
|
# If the method isn't already a private instance method, or it doesn't start with rpc_,
|
34
33
|
# or it isn't in the reserved method list (NON_RPC_METHODS),
|
35
34
|
# We want to remap the method such that we can wrap it in before and after behavior,
|
36
35
|
# most notably calling call_rpc against the method. See call_rpc for more info.
|
37
|
-
def method_added
|
36
|
+
def method_added(old)
|
38
37
|
new_method = :"rpc_#{old}"
|
39
38
|
return if private_instance_methods.include?(new_method) or old =~ /^rpc_/ or NON_RPC_METHODS.include?(old.to_s)
|
40
39
|
|
41
40
|
alias_method new_method, old
|
42
41
|
private new_method
|
43
42
|
|
44
|
-
|
45
|
-
|
46
|
-
call_rpc old.to_sym, pb_request
|
47
|
-
end
|
48
|
-
rescue ArgumentError => e
|
49
|
-
# Wrap a known issue where an instance method was defined in the class without
|
50
|
-
# it being ignored with NON_RPC_METHODS.
|
51
|
-
raise ArgumentError, "#{e.message} (Note: This could mean that you need to add the method #{old} to the NON_RPC_METHODS list)"
|
43
|
+
define_method(old) do |pb_request|
|
44
|
+
call_rpc(old.to_sym, pb_request)
|
52
45
|
end
|
46
|
+
rescue ArgumentError => e
|
47
|
+
# Wrap a known issue where an instance method was defined in the class without
|
48
|
+
# it being ignored with NON_RPC_METHODS.
|
49
|
+
raise ArgumentError, "#{e.message} (Note: This could mean that you need to add the method #{old} to the NON_RPC_METHODS list)"
|
53
50
|
end
|
54
51
|
|
55
52
|
# Generated service classes should call this method on themselves to add rpc methods
|
56
53
|
# to the stack with a given request and response type
|
57
|
-
def rpc
|
54
|
+
def rpc(method, request_type, response_type)
|
58
55
|
rpcs[self] ||= {}
|
59
56
|
rpcs[self][method] = RpcMethod.new self, method, request_type, response_type
|
60
57
|
end
|
@@ -68,7 +65,7 @@ module Protobuf
|
|
68
65
|
# See Client#initialize and ClientConnection::DEFAULT_OPTIONS
|
69
66
|
# for all available options.
|
70
67
|
#
|
71
|
-
def client
|
68
|
+
def client(options={})
|
72
69
|
configure
|
73
70
|
Client.new({
|
74
71
|
:service => self,
|
@@ -83,17 +80,17 @@ module Protobuf
|
|
83
80
|
# so that any Clients using the Service.client sugar
|
84
81
|
# will not have to configure the location each time.
|
85
82
|
#
|
86
|
-
def configure
|
83
|
+
def configure(config={})
|
87
84
|
locations[self] ||= {}
|
88
|
-
locations[self][:host] = config[:host] if config.key?
|
89
|
-
locations[self][:port] = config[:port] if config.key?
|
85
|
+
locations[self][:host] = config[:host] if config.key?(:host)
|
86
|
+
locations[self][:port] = config[:port] if config.key?(:port)
|
90
87
|
end
|
91
88
|
|
92
89
|
# Shorthand call to configure, passing a string formatted as hostname:port
|
93
90
|
# e.g. 127.0.0.1:9933
|
94
91
|
# e.g. localhost:0
|
95
92
|
#
|
96
|
-
def located_at
|
93
|
+
def located_at(location)
|
97
94
|
return if location.nil? or location.downcase.strip !~ /[a-z0-9.]+:\d+/
|
98
95
|
host, port = location.downcase.strip.split ':'
|
99
96
|
configure :host => host, :port => port.to_i
|
@@ -117,6 +114,10 @@ module Protobuf
|
|
117
114
|
end
|
118
115
|
|
119
116
|
end
|
117
|
+
|
118
|
+
def log_signature
|
119
|
+
@log_signature ||= "service-#{self.class}"
|
120
|
+
end
|
120
121
|
|
121
122
|
# If a method comes through that hasn't been found, and it
|
122
123
|
# is defined in the rpcs method list, we know that the rpc
|
@@ -129,7 +130,7 @@ module Protobuf
|
|
129
130
|
log_error exc.message
|
130
131
|
raise exc
|
131
132
|
else
|
132
|
-
log_error
|
133
|
+
log_error "-------------- [#{log_signature}] %s#%s not rpc method, passing to super" % [self.class.name, m.to_s]
|
133
134
|
super m, params
|
134
135
|
end
|
135
136
|
end
|
@@ -141,7 +142,7 @@ module Protobuf
|
|
141
142
|
|
142
143
|
# Callback register for the server when a service
|
143
144
|
# method calls rpc_failed. Called by Service#rpc_failed.
|
144
|
-
def on_rpc_failed
|
145
|
+
def on_rpc_failed(&rpc_failure_cb)
|
145
146
|
@rpc_failure_cb = rpc_failure_cb
|
146
147
|
end
|
147
148
|
|
@@ -149,14 +150,11 @@ module Protobuf
|
|
149
150
|
# NOTE: This shortcuts the @async_responder paradigm. There is
|
150
151
|
# not any way to get around this currently (and I'm not sure you should want to).
|
151
152
|
#
|
152
|
-
def rpc_failed
|
153
|
-
|
154
|
-
|
155
|
-
log_error exc.message
|
156
|
-
raise exc
|
157
|
-
end
|
153
|
+
def rpc_failed(message="RPC Failed while executing service method #{@current_method}")
|
154
|
+
error_message = 'Unable to invoke rpc_failed, no failure callback is setup.'
|
155
|
+
log_and_raise_error(error_message) if @rpc_failure_cb.nil?
|
158
156
|
error = message.is_a?(String) ? RpcFailed.new(message) : message
|
159
|
-
log_warn
|
157
|
+
log_warn "[#{log_signature}] RPC Failed: %s" % error.message
|
160
158
|
@rpc_failure_cb.call(error)
|
161
159
|
end
|
162
160
|
|
@@ -164,7 +162,7 @@ module Protobuf
|
|
164
162
|
# when it is appropriate to generate a response to the client.
|
165
163
|
# Used in conjunciton with Service#send_response.
|
166
164
|
#
|
167
|
-
def on_send_response
|
165
|
+
def on_send_response(&responder)
|
168
166
|
@responder = responder
|
169
167
|
end
|
170
168
|
|
@@ -175,15 +173,17 @@ module Protobuf
|
|
175
173
|
# will timeout since no data will be sent.
|
176
174
|
#
|
177
175
|
def send_response
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
raise exc
|
182
|
-
end
|
183
|
-
@responder.call @response
|
176
|
+
error_message = "Unable to send response, responder is nil. It appears you aren't inside of an RPC request/response cycle."
|
177
|
+
log_and_raise_error(error_message) if @responder.nil?
|
178
|
+
@responder.call(@response)
|
184
179
|
end
|
185
180
|
|
186
181
|
private
|
182
|
+
|
183
|
+
def log_and_raise_error(error_message)
|
184
|
+
log_error(error_message)
|
185
|
+
raise error_message
|
186
|
+
end
|
187
187
|
|
188
188
|
# Call the rpc method that was previously privatized.
|
189
189
|
# call_rpc allows us to wrap the normal method call with
|
@@ -202,38 +202,36 @@ module Protobuf
|
|
202
202
|
# by calling self.send_response without any arguments. The rpc
|
203
203
|
# server is setup to handle synchronous and asynchronous responses.
|
204
204
|
#
|
205
|
-
def call_rpc
|
205
|
+
def call_rpc(method, pb_request)
|
206
206
|
@current_method = method
|
207
207
|
|
208
208
|
# Allows the service to set whether or not
|
209
209
|
# it would like to asynchronously respond to the connected client(s)
|
210
210
|
@async_responder = false
|
211
211
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
end
|
222
|
-
|
212
|
+
# Setup the request
|
213
|
+
@request = rpcs[method].request_type.new
|
214
|
+
@request.parse_from_string(pb_request.request_proto)
|
215
|
+
rescue
|
216
|
+
exc = BadRequestProto.new 'Unable to parse request: %s' % $!.message
|
217
|
+
log_error exc.message
|
218
|
+
log_error $!.backtrace.join("\n")
|
219
|
+
raise exc
|
220
|
+
else # when no Exception was thrown
|
223
221
|
# Setup the response
|
224
222
|
@response = rpcs[method].response_type.new
|
225
223
|
|
226
|
-
log_debug
|
224
|
+
log_debug "[#{log_signature}] calling service method %s#%s" % [self.class, method]
|
227
225
|
# Call the aliased rpc method (e.g. :rpc_find for :find)
|
228
226
|
__send__("rpc_#{method}".to_sym)
|
229
|
-
log_debug
|
227
|
+
log_debug "[#{log_signature}] completed service method %s#%s" % [self.class, method]
|
230
228
|
|
231
229
|
# Pass the populated response back to the server
|
232
230
|
# Note this will only get called if the rpc method didn't explode (by design)
|
233
231
|
if @async_responder
|
234
|
-
log_debug
|
232
|
+
log_debug "[#{log_signature}] async request, not sending response (yet)"
|
235
233
|
else
|
236
|
-
log_debug
|
234
|
+
log_debug "[#{log_signature}] trigger server send_response"
|
237
235
|
send_response
|
238
236
|
end
|
239
237
|
end
|
@@ -241,4 +239,5 @@ module Protobuf
|
|
241
239
|
end
|
242
240
|
|
243
241
|
end
|
244
|
-
|
242
|
+
|
243
|
+
end
|
data/lib/protobuf/rpc/stat.rb
CHANGED
@@ -51,12 +51,12 @@ module Protobuf
|
|
51
51
|
end
|
52
52
|
|
53
53
|
def log_stats
|
54
|
-
Protobuf::Logger.info
|
54
|
+
Protobuf::Logger.info(self.to_s)
|
55
55
|
end
|
56
56
|
|
57
57
|
def to_s
|
58
58
|
[
|
59
|
-
@type == :SERVER ?
|
59
|
+
@type == :SERVER ? "[SRV-#{self.class}]" : "[CLT-#{self.class}]",
|
60
60
|
rpc,
|
61
61
|
elapsed_time,
|
62
62
|
sizes,
|
data/lib/protobuf/version.rb
CHANGED
data/protobuf.gemspec
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# encoding: UTF-8
|
2
2
|
$:.push File.expand_path("./lib", File.dirname(__FILE__))
|
3
3
|
require "protobuf/version"
|
4
4
|
|
@@ -8,7 +8,7 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.date = %q{2011-12-07}
|
9
9
|
|
10
10
|
s.authors = ['BJ Neilsen', 'Brandon Dewitt']
|
11
|
-
s.email = ["bj.neilsen@gmail.com", "brandonsdewitt@gmail.com"]
|
11
|
+
s.email = ["bj.neilsen@gmail.com", "brandonsdewitt+protobuf@gmail.com"]
|
12
12
|
s.homepage = %q{https://github.com/localshred/protobuf}
|
13
13
|
s.summary = 'Ruby implementation for Protocol Buffers. Works with other protobuf rpc implementations (e.g. Java, Python, C++).'
|
14
14
|
s.description = s.summary + "\n\nThis gem has diverged from https://github.com/macks/ruby-protobuf. All credit for serialization and rprotoc work most certainly goes to the original authors. All RPC implementation code (client/server/service) was written and is maintained by this author. Attempts to reconcile the original codebase with the current RPC implementation went unsuccessful."
|
@@ -19,7 +19,12 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.require_paths = ["lib"]
|
20
20
|
|
21
21
|
s.add_dependency 'eventmachine', '~> 0.12.10'
|
22
|
-
|
22
|
+
s.add_dependency 'eventually', '~> 0.1.0'
|
23
|
+
s.add_dependency 'json_pure', '~> 1.6.4'
|
24
|
+
|
23
25
|
s.add_development_dependency 'rake', '~> 0.8.7'
|
24
|
-
s.add_development_dependency 'rspec', '~> 2.
|
26
|
+
s.add_development_dependency 'rspec', '~> 2.8.0'
|
27
|
+
s.add_development_dependency 'yard', '~> 0.7.4'
|
28
|
+
s.add_development_dependency 'redcarpet', '~> 1.17.2'
|
29
|
+
s.add_development_dependency 'simplecov', '~> 0.5.4'
|
25
30
|
end
|