ibsciss-middleware 0.3.0
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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.travis.yml +11 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +208 -0
- data/Rakefile +9 -0
- data/lib/middleware.rb +2 -0
- data/lib/middleware/builder.rb +147 -0
- data/lib/middleware/runner.rb +69 -0
- data/middleware.gemspec +22 -0
- data/spec/middleware/builder_spec.rb +191 -0
- data/spec/middleware/runner_spec.rb +192 -0
- data/spec/spec_helper.rb +89 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 19c42633d1c333e95219851b18162d3dd8962401
|
4
|
+
data.tar.gz: 862b2a2464ff93be9ce68f9cc6c48ba215d610cf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8b1a8954e9053c362c10b8fe3ad606cdb22a294e8fcbfb8588d37ddeb589763d54af33d0c41e19eb529e6f3ef92840df38f58ec893816c94280bfbe84e37a215
|
7
|
+
data.tar.gz: b4a05f87ea90f0c1d2051df9057278ad2c54a0ff4a86f0eee57fdcd41535e09992b5ef0cb421879c011a95af015b926f4c8fcc4627a7d0bbb8b33a360c116dd2
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
## 0.3.0
|
2
|
+
- Apply https://github.com/Ibsciss/ruby-middleware/commit/01f75d8e4137b39ea907f13756f21cba4edffaa7
|
3
|
+
- Apply https://github.com/Ibsciss/ruby-middleware/commit/00aa09353f7ee2b801bdb446418a936640ae56d7
|
4
|
+
- Refactor test suite to use the new rspec expect syntaxe
|
5
|
+
- Update dependencies version
|
6
|
+
|
7
|
+
## 0.2.0 (unreleased)
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
## 0.1.0 (March 16, 2012)
|
12
|
+
|
13
|
+
- Initial release, almost directly extracted from [Vagrant](http://vagrantup.com)
|
14
|
+
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Mitchell Hashimoto
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
# Middleware
|
2
|
+
|
3
|
+
`middleware` is a library which provides a generalized implementation
|
4
|
+
of the middleware pattern for Ruby. The middleware pattern is a useful
|
5
|
+
abstraction tool in various cases, but is specifically useful for splitting
|
6
|
+
large sequential chunks of logic into small pieces.
|
7
|
+
|
8
|
+
This library was created by Mitchell Hashimoto, original repository: https://github.com/mitchellh/middleware
|
9
|
+
|
10
|
+
## Installing
|
11
|
+
|
12
|
+
Middleware is distributed as a RubyGem, so simply gem install:
|
13
|
+
|
14
|
+
```console
|
15
|
+
$ gem install ibsciss-middleware
|
16
|
+
```
|
17
|
+
|
18
|
+
## A Basic Example
|
19
|
+
|
20
|
+
Below is a basic example of the library in use. If you don't understand
|
21
|
+
what middleware is, please read below. This example is simply meant to give
|
22
|
+
you a quick idea of what the library looks like.
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
# Basic middleware that just prints the inbound and
|
26
|
+
# outbound steps.
|
27
|
+
class Trace
|
28
|
+
def initialize(app, value)
|
29
|
+
@app = app
|
30
|
+
@value = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def call(env)
|
34
|
+
puts "--> #{@value}"
|
35
|
+
@app.call(env)
|
36
|
+
puts "<-- #{@value}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Build the actual middleware stack which runs a sequence
|
41
|
+
# of slightly different versions of our middleware.
|
42
|
+
stack = Middleware::Builder.new do |b|
|
43
|
+
b.use Trace, "A"
|
44
|
+
b.use Trace, "B"
|
45
|
+
b.use Trace, "C"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Run it!
|
49
|
+
stack.call(nil)
|
50
|
+
```
|
51
|
+
|
52
|
+
And the output:
|
53
|
+
|
54
|
+
```
|
55
|
+
--> A
|
56
|
+
--> B
|
57
|
+
--> C
|
58
|
+
<-- C
|
59
|
+
<-- B
|
60
|
+
<-- A
|
61
|
+
```
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
## Middleware
|
66
|
+
|
67
|
+
### What is it?
|
68
|
+
|
69
|
+
Middleware is a reusable chunk of logic that is called to perform some
|
70
|
+
action. The middleware itself is responsible for calling up the next item
|
71
|
+
in the middleware chain using a recursive-like call. This allows middleware
|
72
|
+
to perform logic both _before_ and _after_ something is done.
|
73
|
+
|
74
|
+
The canonical middleware example is in web request processing, and middleware
|
75
|
+
is used heavily by both [Rack](#) and [Rails](#).
|
76
|
+
In web processing, the first middleware is called with some information about
|
77
|
+
the web request, such as HTTP headers, request URL, etc. The middleware is
|
78
|
+
responsible for calling the next middleware, and may modify the request along
|
79
|
+
the way. When the middlewares begin returning, the state now has the HTTP
|
80
|
+
response, so that the middlewares can then modify the response.
|
81
|
+
|
82
|
+
Cool? Yeah! And this pattern is generally usable in a wide variety of
|
83
|
+
problems.
|
84
|
+
|
85
|
+
### Middleware Classes
|
86
|
+
|
87
|
+
One method of creating middleware, and by far the most common, is to define
|
88
|
+
a class that duck types to the following interface:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
class MiddlewareExample
|
92
|
+
def initialize(app); end
|
93
|
+
def call(env); end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
Therefore, a basic middleware example follows:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
class Trace
|
101
|
+
def initialize(app)
|
102
|
+
@app = app
|
103
|
+
end
|
104
|
+
|
105
|
+
def call(env)
|
106
|
+
puts "Trace up"
|
107
|
+
@app.call(env)
|
108
|
+
puts "Trace down"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
A basic description of the two methods that a middleware must implement:
|
114
|
+
|
115
|
+
* **initialize(app)** - The first argument sent will always be the next middleware to call, called
|
116
|
+
`app` for historical reasons. This should be stored away for later.
|
117
|
+
|
118
|
+
* **call(env)** - This is what is actually invoked to do work. `env` is just some
|
119
|
+
state sent in (defined by the caller, but usually a Hash). This call should also
|
120
|
+
call `app.call(env)` at some point to move on.
|
121
|
+
|
122
|
+
### Middleware Lambdas
|
123
|
+
|
124
|
+
A middleware can also be a simple lambda. The downside of using a lambda is that
|
125
|
+
it only has access to the state on the initial call, there is no "post" step for
|
126
|
+
lambdas. A basic example, in the context of a web request:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
lambda { |env| puts "You requested: #{env["http.request_url"]}" }
|
130
|
+
```
|
131
|
+
|
132
|
+
## Middleware Stacks
|
133
|
+
|
134
|
+
Middlewares on their own are useful as small chunks of logic, but their real
|
135
|
+
power comes from building them up into a _stack_. A stack of middlewares are
|
136
|
+
executed in the order given.
|
137
|
+
|
138
|
+
### Basic Building and Running
|
139
|
+
|
140
|
+
The middleware library comes with a `Builder` class which provides a nice DSL
|
141
|
+
for building a stack of middlewares:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
stack = Middleware::Builder.new do |d|
|
145
|
+
d.use Trace
|
146
|
+
d.use lambda { |env| puts "LAMBDA!" }
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
This `stack` variable itself is now a valid middleware and has the same interface,
|
151
|
+
so to execute the stack, just call `call` on it:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
stack.call
|
155
|
+
```
|
156
|
+
|
157
|
+
The call method takes an optional parameter which is the state to pass into the
|
158
|
+
initial middleware.
|
159
|
+
|
160
|
+
### Manipulating a Stack
|
161
|
+
|
162
|
+
Stacks also provide a set of methods for manipulating the middleware stack. This
|
163
|
+
lets you insert, replace, and delete middleware after a stack has already been
|
164
|
+
created. Given the `stack` variable created above, we can manipulate it as
|
165
|
+
follows. Please imagine that each example runs with the original `stack` variable,
|
166
|
+
so that the order of the examples doesn't actually matter:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
# Insert a new item after the Trace middleware
|
170
|
+
stack.insert_after(Trace, SomeOtherMiddleware)
|
171
|
+
|
172
|
+
# Replace the lambda
|
173
|
+
stack.replace(1, SomeOtherMiddleware)
|
174
|
+
|
175
|
+
# Delete the lambda
|
176
|
+
stack.delete(1)
|
177
|
+
```
|
178
|
+
|
179
|
+
### Passing Additional Constructor Arguments
|
180
|
+
|
181
|
+
When using middleware in a stack, you can also pass in additional constructor
|
182
|
+
arguments. Given the following middleware:
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
class Echo
|
186
|
+
def initialize(app, message)
|
187
|
+
@app = app
|
188
|
+
@message = message
|
189
|
+
end
|
190
|
+
|
191
|
+
def call(env)
|
192
|
+
puts @message
|
193
|
+
@app.call(env)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
We can initialize `Echo` with a proper message as follows:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
Middleware::Builder.new do
|
202
|
+
use Echo, "Hello, World!"
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
Then when the stack is called, it will output "Hello, World!"
|
207
|
+
|
208
|
+
Note that you can also pass blocks in using the `use` method.
|
data/Rakefile
ADDED
data/lib/middleware.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
module Middleware
|
2
|
+
# This provides a DSL for building up a stack of middlewares.
|
3
|
+
#
|
4
|
+
# This code is based heavily off of `Rack::Builder` and
|
5
|
+
# `ActionDispatch::MiddlewareStack` in Rack and Rails, respectively.
|
6
|
+
#
|
7
|
+
# # Usage
|
8
|
+
#
|
9
|
+
# Building a middleware stack is very easy:
|
10
|
+
#
|
11
|
+
# app = Middleware::Builder.new do |b|
|
12
|
+
# b.use A
|
13
|
+
# b.use B
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# # Call the middleware
|
17
|
+
# app.call(7)
|
18
|
+
#
|
19
|
+
class Builder
|
20
|
+
# Initializes the builder. An optional block can be passed which
|
21
|
+
# will either yield the builder or be evaluated in the context of the instance.
|
22
|
+
#
|
23
|
+
# Example:
|
24
|
+
#
|
25
|
+
# Builder.new do |b|
|
26
|
+
# b.use A
|
27
|
+
# b.use B
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# Builder.new do
|
31
|
+
# use A
|
32
|
+
# use B
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# @param [Hash] opts Options hash
|
36
|
+
# @option opts [Class] :runner_class The class to wrap the middleware stack
|
37
|
+
# in which knows how to run them.
|
38
|
+
# @yield [] Evaluated in this instance which allows you to use methods
|
39
|
+
# like {#use} and such.
|
40
|
+
def initialize(opts=nil, &block)
|
41
|
+
opts ||= {}
|
42
|
+
@runner_class = opts[:runner_class] || Runner
|
43
|
+
|
44
|
+
if block_given?
|
45
|
+
if block.arity == 1
|
46
|
+
yield self
|
47
|
+
else
|
48
|
+
instance_eval(&block)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns a mergeable version of the builder. If `use` is called with
|
54
|
+
# the return value of this method, then the stack will merge, instead
|
55
|
+
# of being treated as a separate single middleware.
|
56
|
+
def flatten
|
57
|
+
lambda do |env|
|
58
|
+
self.call(env)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Adds a middleware class to the middleware stack. Any additional
|
63
|
+
# args and a block, if given, are saved and passed to the initializer
|
64
|
+
# of the middleware.
|
65
|
+
#
|
66
|
+
# @param [Class] middleware The middleware class
|
67
|
+
def use(middleware, *args, &block)
|
68
|
+
if middleware.kind_of?(Builder)
|
69
|
+
# Merge in the other builder's stack into our own
|
70
|
+
self.stack.concat(middleware.stack)
|
71
|
+
else
|
72
|
+
self.stack << [middleware, args, block]
|
73
|
+
end
|
74
|
+
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
# Inserts a middleware at the given index or directly before the
|
79
|
+
# given middleware object.
|
80
|
+
def insert(index, middleware, *args, &block)
|
81
|
+
index = self.index(index) unless index.is_a?(Integer)
|
82
|
+
raise "no such middleware to insert before: #{index.inspect}" unless index
|
83
|
+
stack.insert(index, [middleware, args, block])
|
84
|
+
end
|
85
|
+
|
86
|
+
alias_method :insert_before, :insert
|
87
|
+
|
88
|
+
# Inserts a middleware after the given index or middleware object.
|
89
|
+
def insert_after(index, middleware, *args, &block)
|
90
|
+
index = self.index(index) unless index.is_a?(Integer)
|
91
|
+
raise "no such middleware to insert after: #{index.inspect}" unless index
|
92
|
+
insert(index + 1, middleware, *args, &block)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Replaces the given middleware object or index with the new
|
96
|
+
# middleware.
|
97
|
+
def replace(index, middleware, *args, &block)
|
98
|
+
if index.is_a?(Integer)
|
99
|
+
delete(index)
|
100
|
+
insert(index, middleware, *args, &block)
|
101
|
+
else
|
102
|
+
insert_before(index, middleware, *args, &block)
|
103
|
+
delete(index)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Deletes the given middleware object or index
|
108
|
+
def delete(index)
|
109
|
+
index = self.index(index) unless index.is_a?(Integer)
|
110
|
+
stack.delete_at(index)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Runs the builder stack with the given environment.
|
114
|
+
def call(env=nil)
|
115
|
+
to_app.call(env)
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
|
120
|
+
# Returns the numeric index for the given middleware object.
|
121
|
+
#
|
122
|
+
# @param [Object] object The item to find the index for
|
123
|
+
# @return [Integer]
|
124
|
+
def index(object)
|
125
|
+
stack.each_with_index do |item, i|
|
126
|
+
return i if item[0] == object
|
127
|
+
end
|
128
|
+
|
129
|
+
nil
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns the current stack of middlewares. You probably won't
|
133
|
+
# need to use this directly, and it's recommended that you don't.
|
134
|
+
#
|
135
|
+
# @return [Array]
|
136
|
+
def stack
|
137
|
+
@stack ||= []
|
138
|
+
end
|
139
|
+
|
140
|
+
# Converts the builder stack to a runnable action sequence.
|
141
|
+
#
|
142
|
+
# @return [Object] A callable object
|
143
|
+
def to_app
|
144
|
+
@runner_class.new(stack.dup)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Middleware
|
2
|
+
# This is a basic runner for middleware stacks. This runner does
|
3
|
+
# the default expected behavior of running the middleware stacks
|
4
|
+
# in order, then reversing the order.
|
5
|
+
class Runner
|
6
|
+
# A middleware which does nothing
|
7
|
+
EMPTY_MIDDLEWARE = lambda { |env| env }
|
8
|
+
|
9
|
+
# Build a new middleware runner with the given middleware
|
10
|
+
# stack.
|
11
|
+
#
|
12
|
+
# Note: This class usually doesn't need to be used directly.
|
13
|
+
# Instead, take a look at using the {Builder} class, which is
|
14
|
+
# a much friendlier way to build up a middleware stack.
|
15
|
+
#
|
16
|
+
# @param [Array] stack An array of the middleware to run.
|
17
|
+
def initialize(stack)
|
18
|
+
# We need to take the stack of middleware and initialize them
|
19
|
+
# all so they call the proper next middleware.
|
20
|
+
@kickoff = build_call_chain(stack)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Run the middleware stack with the given state bag.
|
24
|
+
#
|
25
|
+
# @param [Object] env The state to pass into as the initial
|
26
|
+
# environment data. This is usual a hash of some sort.
|
27
|
+
def call(env)
|
28
|
+
# We just call the kickoff middleware, which is responsible
|
29
|
+
# for properly calling the next middleware, and so on and so
|
30
|
+
# forth.
|
31
|
+
@kickoff.call(env)
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
# This takes a stack of middlewares and initializes them in a way
|
37
|
+
# that each middleware properly calls the next middleware.
|
38
|
+
def build_call_chain(stack)
|
39
|
+
# We need to instantiate the middleware stack in reverse
|
40
|
+
# order so that each middleware can have a reference to
|
41
|
+
# the next middleware it has to call. The final middleware
|
42
|
+
# is always the empty middleware, which does nothing but return.
|
43
|
+
stack.reverse.inject(EMPTY_MIDDLEWARE) do |next_middleware, current_middleware|
|
44
|
+
# Unpack the actual item
|
45
|
+
klass, args, block = current_middleware
|
46
|
+
|
47
|
+
# Default the arguments to an empty array. Otherwise in Ruby 1.8
|
48
|
+
# a `nil` args will actually pass `nil` into the class. Not what
|
49
|
+
# we want!
|
50
|
+
args ||= []
|
51
|
+
|
52
|
+
if klass.is_a?(Class)
|
53
|
+
# If the klass actually is a class, then instantiate it with
|
54
|
+
# the app and any other arguments given.
|
55
|
+
klass.new(next_middleware, *args, &block)
|
56
|
+
elsif klass.respond_to?(:call)
|
57
|
+
# Make it a lambda which calls the item then forwards up
|
58
|
+
# the chain.
|
59
|
+
lambda do |env|
|
60
|
+
klass.call(env)
|
61
|
+
next_middleware.call(env)
|
62
|
+
end
|
63
|
+
else
|
64
|
+
raise "Invalid middleware, doesn't respond to `call`: #{klass.inspect}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/middleware.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.authors = ["Mitchell Hashimoto", "Arnaud Lemaire"]
|
5
|
+
gem.email = ["mitchell.hashimoto@gmail.com", "alemaire@ibsciss.com"]
|
6
|
+
gem.description = %q{Generalized implementation of the rack middleware abstraction for Ruby.}
|
7
|
+
gem.summary = %q{Generalized implementation of the rack middleware abstraction for Ruby (chain of responsibility design pattern).}
|
8
|
+
gem.homepage = "https://github.com/ibsciss/ruby-middleware"
|
9
|
+
gem.license = "MIT"
|
10
|
+
|
11
|
+
gem.add_development_dependency "rake", "~> 1.6"
|
12
|
+
gem.add_development_dependency "rspec-core", "~> 3.2"
|
13
|
+
gem.add_development_dependency "rspec-expectations", "~> 3.2"
|
14
|
+
gem.add_development_dependency "rspec-mocks", "~> 3.2"
|
15
|
+
|
16
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
gem.files = `git ls-files`.split("\n")
|
18
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
gem.name = "ibsciss-middleware"
|
20
|
+
gem.require_paths = ["lib"]
|
21
|
+
gem.version = '0.3.0'
|
22
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
require "middleware"
|
2
|
+
|
3
|
+
describe Middleware::Builder do
|
4
|
+
let(:data) { { :data => [] } }
|
5
|
+
let(:instance) { described_class.new }
|
6
|
+
|
7
|
+
# This returns a proc that can be used with the builder
|
8
|
+
# that simply appends data to an array in the env.
|
9
|
+
def appender_proc(data)
|
10
|
+
Proc.new { |env| env[:data] << data }
|
11
|
+
end
|
12
|
+
|
13
|
+
context "initialized with a block" do
|
14
|
+
context "without explicit receiver" do
|
15
|
+
it "instance evals the block" do
|
16
|
+
data = {}
|
17
|
+
proc = Proc.new { |env| env[:data] = true }
|
18
|
+
|
19
|
+
app = described_class.new do
|
20
|
+
use proc
|
21
|
+
end
|
22
|
+
|
23
|
+
app.call(data)
|
24
|
+
|
25
|
+
expect(data[:data]).to be_truthy
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "with explicit receiver" do
|
30
|
+
it "yields self to the block" do
|
31
|
+
data = {}
|
32
|
+
proc = Proc.new { |env| env[:data] = true }
|
33
|
+
|
34
|
+
app = described_class.new do |b|
|
35
|
+
b.use proc
|
36
|
+
end
|
37
|
+
|
38
|
+
app.call(data)
|
39
|
+
|
40
|
+
expect(data[:data]).to be_truthy
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "basic `use`" do
|
46
|
+
it "should add items to the stack and make them callable" do
|
47
|
+
data = {}
|
48
|
+
proc = Proc.new { |env| env[:data] = true }
|
49
|
+
|
50
|
+
instance.use proc
|
51
|
+
instance.call(data)
|
52
|
+
|
53
|
+
expect(data[:data]).to be_truthy
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should be able to add multiple items" do
|
57
|
+
data = {}
|
58
|
+
proc1 = Proc.new { |env| env[:one] = true }
|
59
|
+
proc2 = Proc.new { |env| env[:two] = true }
|
60
|
+
|
61
|
+
instance.use proc1
|
62
|
+
instance.use proc2
|
63
|
+
instance.call(data)
|
64
|
+
|
65
|
+
expect(data[:one]).to be_truthy
|
66
|
+
expect(data[:two]).to be_truthy
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should be able to add another builder" do
|
70
|
+
data = {}
|
71
|
+
proc1 = Proc.new { |env| env[:one] = true }
|
72
|
+
|
73
|
+
# Build the first builder
|
74
|
+
one = described_class.new
|
75
|
+
one.use proc1
|
76
|
+
|
77
|
+
# Add it to this builder
|
78
|
+
two = described_class.new
|
79
|
+
two.use one
|
80
|
+
|
81
|
+
# Call the 2nd and verify results
|
82
|
+
two.call(data)
|
83
|
+
expect(data[:one]).to be_truthy
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should default the env to `nil` if not given" do
|
87
|
+
result = false
|
88
|
+
proc = Proc.new { |env| result = env.nil? }
|
89
|
+
|
90
|
+
instance.use proc
|
91
|
+
instance.call
|
92
|
+
|
93
|
+
expect(result).to be_truthy
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context "inserting" do
|
98
|
+
it "can insert at an index" do
|
99
|
+
instance.use appender_proc(1)
|
100
|
+
instance.insert(0, appender_proc(2))
|
101
|
+
instance.call(data)
|
102
|
+
|
103
|
+
expect(data[:data]).to eq [2, 1]
|
104
|
+
end
|
105
|
+
|
106
|
+
it "can insert next to a previous object" do
|
107
|
+
proc2 = appender_proc(2)
|
108
|
+
instance.use appender_proc(1)
|
109
|
+
instance.use proc2
|
110
|
+
instance.insert(proc2, appender_proc(3))
|
111
|
+
instance.call(data)
|
112
|
+
|
113
|
+
expect(data[:data]).to eq [1, 3, 2]
|
114
|
+
end
|
115
|
+
|
116
|
+
it "can insert before" do
|
117
|
+
instance.use appender_proc(1)
|
118
|
+
instance.insert_before 0, appender_proc(2)
|
119
|
+
instance.call(data)
|
120
|
+
|
121
|
+
expect(data[:data]).to eq [2, 1]
|
122
|
+
end
|
123
|
+
|
124
|
+
it "raises an exception if attempting to insert before an invalid object" do
|
125
|
+
expect { instance.insert "object", appender_proc(1) }.
|
126
|
+
to raise_error(RuntimeError)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "can insert after" do
|
130
|
+
instance.use appender_proc(1)
|
131
|
+
instance.use appender_proc(3)
|
132
|
+
instance.insert_after 0, appender_proc(2)
|
133
|
+
instance.call(data)
|
134
|
+
|
135
|
+
expect(data[:data]).to eq [1, 2, 3]
|
136
|
+
end
|
137
|
+
|
138
|
+
it "raises an exception if attempting to insert after an invalid object" do
|
139
|
+
expect { instance.insert_after "object", appender_proc(1) }.
|
140
|
+
to raise_error(RuntimeError)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context "replace" do
|
145
|
+
it "can replace an object" do
|
146
|
+
proc1 = appender_proc(1)
|
147
|
+
proc2 = appender_proc(2)
|
148
|
+
|
149
|
+
instance.use proc1
|
150
|
+
instance.replace proc1, proc2
|
151
|
+
instance.call(data)
|
152
|
+
|
153
|
+
expect(data[:data]).to eq [2]
|
154
|
+
end
|
155
|
+
|
156
|
+
it "can replace by index" do
|
157
|
+
proc1 = appender_proc(1)
|
158
|
+
proc2 = appender_proc(2)
|
159
|
+
|
160
|
+
instance.use proc1
|
161
|
+
instance.replace 0, proc2
|
162
|
+
instance.call(data)
|
163
|
+
|
164
|
+
expect(data[:data]).to eq [2]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
context "deleting" do
|
169
|
+
it "can delete by object" do
|
170
|
+
proc1 = appender_proc(1)
|
171
|
+
|
172
|
+
instance.use proc1
|
173
|
+
instance.use appender_proc(2)
|
174
|
+
instance.delete proc1
|
175
|
+
instance.call(data)
|
176
|
+
|
177
|
+
expect(data[:data]).to eq [2]
|
178
|
+
end
|
179
|
+
|
180
|
+
it "can delete by index" do
|
181
|
+
proc1 = appender_proc(1)
|
182
|
+
|
183
|
+
instance.use proc1
|
184
|
+
instance.use appender_proc(2)
|
185
|
+
instance.delete 0
|
186
|
+
instance.call(data)
|
187
|
+
|
188
|
+
expect(data[:data]).to eq [2]
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
require "middleware"
|
2
|
+
|
3
|
+
describe Middleware::Runner do
|
4
|
+
it "should work with an empty stack" do
|
5
|
+
instance = described_class.new([])
|
6
|
+
expect { instance.call({}) }.to_not raise_error
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should call classes in the proper order" do
|
10
|
+
a = Class.new do
|
11
|
+
def initialize(app)
|
12
|
+
@app = app
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
env[:result] << "A"
|
17
|
+
@app.call(env)
|
18
|
+
env[:result] << "A"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
b = Class.new do
|
23
|
+
def initialize(app)
|
24
|
+
@app = app
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(env)
|
28
|
+
env[:result] << "B"
|
29
|
+
@app.call(env)
|
30
|
+
env[:result] << "B"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
env = { :result => [] }
|
35
|
+
instance = described_class.new([a, b])
|
36
|
+
instance.call(env)
|
37
|
+
expect(env[:result]).to eq ["A", "B", "B", "A"]
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should call lambdas in the proper order" do
|
41
|
+
data = []
|
42
|
+
a = lambda { |env| data << "A" }
|
43
|
+
b = lambda { |env| data << "B" }
|
44
|
+
|
45
|
+
instance = described_class.new([a, b])
|
46
|
+
instance.call({})
|
47
|
+
|
48
|
+
expect(data).to eq ["A", "B"]
|
49
|
+
end
|
50
|
+
|
51
|
+
it "passes in arguments if given" do
|
52
|
+
a = Class.new do
|
53
|
+
def initialize(app, value)
|
54
|
+
@app = app
|
55
|
+
@value = value
|
56
|
+
end
|
57
|
+
|
58
|
+
def call(env)
|
59
|
+
env[:result] = @value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
env = {}
|
64
|
+
instance = described_class.new([[a, 42]])
|
65
|
+
instance.call(env)
|
66
|
+
|
67
|
+
expect(env[:result]).to eq 42
|
68
|
+
end
|
69
|
+
|
70
|
+
it "passes in a block if given" do
|
71
|
+
a = Class.new do
|
72
|
+
def initialize(app, &block)
|
73
|
+
@block = block
|
74
|
+
end
|
75
|
+
|
76
|
+
def call(env)
|
77
|
+
env[:result] = @block.call
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
block = Proc.new { 42 }
|
82
|
+
env = {}
|
83
|
+
instance = described_class.new([[a, nil, block]])
|
84
|
+
instance.call(env)
|
85
|
+
|
86
|
+
expect(env[:result]).to eq 42
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should raise an error if an invalid middleware is given" do
|
90
|
+
expect { described_class.new([27]) }.to raise_error(/Invalid middleware/)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should not call middlewares which aren't called" do
|
94
|
+
# A does not call B, so B should never execute
|
95
|
+
data = []
|
96
|
+
a = Class.new do
|
97
|
+
def initialize(app); @app = app; end
|
98
|
+
|
99
|
+
define_method :call do |env|
|
100
|
+
data << "a"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
b = lambda { |env| data << "b" }
|
105
|
+
|
106
|
+
env = {}
|
107
|
+
instance = described_class.new([a, b])
|
108
|
+
instance.call(env)
|
109
|
+
|
110
|
+
expect(data).to eq ["a"]
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "exceptions" do
|
114
|
+
it "should propagate the exception up the middleware chain" do
|
115
|
+
# This tests a few important properties:
|
116
|
+
# * Exceptions propagate multiple middlewares
|
117
|
+
# - C raises an exception, which raises through B to A.
|
118
|
+
# * Rescuing exceptions works
|
119
|
+
data = []
|
120
|
+
a = Class.new do
|
121
|
+
def initialize(app)
|
122
|
+
@app = app
|
123
|
+
end
|
124
|
+
|
125
|
+
define_method :call do |env|
|
126
|
+
data << "a"
|
127
|
+
begin
|
128
|
+
@app.call(env)
|
129
|
+
data << "never"
|
130
|
+
rescue Exception => e
|
131
|
+
data << "e"
|
132
|
+
raise
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
b = Class.new do
|
138
|
+
def initialize(app); @app = app; end
|
139
|
+
|
140
|
+
define_method :call do |env|
|
141
|
+
data << "b"
|
142
|
+
@app.call(env)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
c = lambda { |env| raise "ERROR" }
|
147
|
+
|
148
|
+
env = {}
|
149
|
+
instance = described_class.new([a, b, c])
|
150
|
+
expect { instance.call(env) }.to raise_error
|
151
|
+
|
152
|
+
expect(data).to eq ["a", "b", "e"]
|
153
|
+
end
|
154
|
+
|
155
|
+
it "should stop propagation if rescued" do
|
156
|
+
# This test mainly tests that if there is a sequence A, B, C, and
|
157
|
+
# an exception is raised in C, that if B rescues this, then the chain
|
158
|
+
# continues fine backwards.
|
159
|
+
data = []
|
160
|
+
a = Class.new do
|
161
|
+
def initialize(app); @app = app; end
|
162
|
+
|
163
|
+
define_method :call do |env|
|
164
|
+
data << "in_a"
|
165
|
+
@app.call(env)
|
166
|
+
data << "out_a"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
b = Class.new do
|
171
|
+
def initialize(app); @app = app; end
|
172
|
+
|
173
|
+
define_method :call do |env|
|
174
|
+
data << "in_b"
|
175
|
+
@app.call(env) rescue nil
|
176
|
+
data << "out_b"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
c = lambda do |env|
|
181
|
+
data << "in_c"
|
182
|
+
raise "BAD"
|
183
|
+
end
|
184
|
+
|
185
|
+
env = {}
|
186
|
+
instance = described_class.new([a, b, c])
|
187
|
+
instance.call(env)
|
188
|
+
|
189
|
+
expect(data).to eq ["in_a", "in_b", "in_c", "out_b", "out_a"]
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# The generated `.rspec` file contains `--require spec_helper` which will cause this
|
4
|
+
# file to always be loaded, without a need to explicitly require it in any files.
|
5
|
+
#
|
6
|
+
# Given that it is always loaded, you are encouraged to keep this file as
|
7
|
+
# light-weight as possible. Requiring heavyweight dependencies from this file
|
8
|
+
# will add to the boot time of your test suite on EVERY test run, even for an
|
9
|
+
# individual file that may not need all of that loaded. Instead, consider making
|
10
|
+
# a separate helper file that requires the additional dependencies and performs
|
11
|
+
# the additional setup, and require it from the spec files that actually need it.
|
12
|
+
#
|
13
|
+
# The `.rspec` file also contains a few flags that are not defaults but that
|
14
|
+
# users commonly want.
|
15
|
+
#
|
16
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
17
|
+
RSpec.configure do |config|
|
18
|
+
# rspec-expectations config goes here. You can use an alternate
|
19
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
20
|
+
# assertions if you prefer.
|
21
|
+
config.expect_with :rspec do |expectations|
|
22
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
23
|
+
# and `failure_message` of custom matchers include text for helper methods
|
24
|
+
# defined using `chain`, e.g.:
|
25
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
26
|
+
# # => "be bigger than 2 and smaller than 4"
|
27
|
+
# ...rather than:
|
28
|
+
# # => "be bigger than 2"
|
29
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
30
|
+
end
|
31
|
+
|
32
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
33
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
34
|
+
config.mock_with :rspec do |mocks|
|
35
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
36
|
+
# a real object. This is generally recommended, and will default to
|
37
|
+
# `true` in RSpec 4.
|
38
|
+
mocks.verify_partial_doubles = true
|
39
|
+
end
|
40
|
+
|
41
|
+
# The settings below are suggested to provide a good initial experience
|
42
|
+
# with RSpec, but feel free to customize to your heart's content.
|
43
|
+
=begin
|
44
|
+
# These two settings work together to allow you to limit a spec run
|
45
|
+
# to individual examples or groups you care about by tagging them with
|
46
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
47
|
+
# get run.
|
48
|
+
config.filter_run :focus
|
49
|
+
config.run_all_when_everything_filtered = true
|
50
|
+
|
51
|
+
# Limits the available syntax to the non-monkey patched syntax that is recommended.
|
52
|
+
# For more details, see:
|
53
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
54
|
+
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
55
|
+
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
56
|
+
config.disable_monkey_patching!
|
57
|
+
|
58
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
59
|
+
# be too noisy due to issues in dependencies.
|
60
|
+
config.warnings = true
|
61
|
+
|
62
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
63
|
+
# file, and it's useful to allow more verbose output when running an
|
64
|
+
# individual spec file.
|
65
|
+
if config.files_to_run.one?
|
66
|
+
# Use the documentation formatter for detailed output,
|
67
|
+
# unless a formatter has already been configured
|
68
|
+
# (e.g. via a command-line flag).
|
69
|
+
config.default_formatter = 'doc'
|
70
|
+
end
|
71
|
+
|
72
|
+
# Print the 10 slowest examples and example groups at the
|
73
|
+
# end of the spec run, to help surface which specs are running
|
74
|
+
# particularly slow.
|
75
|
+
config.profile_examples = 10
|
76
|
+
|
77
|
+
# Run specs in random order to surface order dependencies. If you find an
|
78
|
+
# order dependency and want to debug it, you can fix the order by providing
|
79
|
+
# the seed, which is printed after each run.
|
80
|
+
# --seed 1234
|
81
|
+
config.order = :random
|
82
|
+
|
83
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
84
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
85
|
+
# test failures related to randomization by passing the same `--seed` value
|
86
|
+
# as the one that triggered the failure.
|
87
|
+
Kernel.srand config.seed
|
88
|
+
=end
|
89
|
+
end
|
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ibsciss-middleware
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mitchell Hashimoto
|
8
|
+
- Arnaud Lemaire
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-04-07 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.6'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.6'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rspec-core
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '3.2'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '3.2'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rspec-expectations
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '3.2'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '3.2'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rspec-mocks
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '3.2'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '3.2'
|
70
|
+
description: Generalized implementation of the rack middleware abstraction for Ruby.
|
71
|
+
email:
|
72
|
+
- mitchell.hashimoto@gmail.com
|
73
|
+
- alemaire@ibsciss.com
|
74
|
+
executables: []
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- ".gitignore"
|
79
|
+
- ".rspec"
|
80
|
+
- ".travis.yml"
|
81
|
+
- CHANGELOG.md
|
82
|
+
- Gemfile
|
83
|
+
- LICENSE
|
84
|
+
- README.md
|
85
|
+
- Rakefile
|
86
|
+
- lib/middleware.rb
|
87
|
+
- lib/middleware/builder.rb
|
88
|
+
- lib/middleware/runner.rb
|
89
|
+
- middleware.gemspec
|
90
|
+
- spec/middleware/builder_spec.rb
|
91
|
+
- spec/middleware/runner_spec.rb
|
92
|
+
- spec/spec_helper.rb
|
93
|
+
homepage: https://github.com/ibsciss/ruby-middleware
|
94
|
+
licenses:
|
95
|
+
- MIT
|
96
|
+
metadata: {}
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubyforge_project:
|
113
|
+
rubygems_version: 2.4.3
|
114
|
+
signing_key:
|
115
|
+
specification_version: 4
|
116
|
+
summary: Generalized implementation of the rack middleware abstraction for Ruby (chain
|
117
|
+
of responsibility design pattern).
|
118
|
+
test_files:
|
119
|
+
- spec/middleware/builder_spec.rb
|
120
|
+
- spec/middleware/runner_spec.rb
|
121
|
+
- spec/spec_helper.rb
|