eaco 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.travis.yml +18 -0
- data/.yardopts +1 -0
- data/Appraisals +22 -0
- data/Gemfile +4 -0
- data/Guardfile +34 -0
- data/LICENSE.txt +23 -0
- data/README.md +225 -0
- data/Rakefile +26 -0
- data/eaco.gemspec +27 -0
- data/features/active_record.example.yml +8 -0
- data/features/active_record.travis.yml +7 -0
- data/features/rails_integration.feature +10 -0
- data/features/step_definitions/database.rb +7 -0
- data/features/step_definitions/resource_authorization.rb +15 -0
- data/features/support/env.rb +9 -0
- data/gemfiles/rails_3.2.gemfile +9 -0
- data/gemfiles/rails_4.0.gemfile +8 -0
- data/gemfiles/rails_4.1.gemfile +8 -0
- data/gemfiles/rails_4.2.gemfile +8 -0
- data/lib/eaco.rb +93 -0
- data/lib/eaco/acl.rb +206 -0
- data/lib/eaco/actor.rb +86 -0
- data/lib/eaco/adapters.rb +14 -0
- data/lib/eaco/adapters/active_record.rb +70 -0
- data/lib/eaco/adapters/active_record/compatibility.rb +83 -0
- data/lib/eaco/adapters/active_record/compatibility/v32.rb +27 -0
- data/lib/eaco/adapters/active_record/compatibility/v40.rb +59 -0
- data/lib/eaco/adapters/active_record/compatibility/v41.rb +16 -0
- data/lib/eaco/adapters/active_record/compatibility/v42.rb +17 -0
- data/lib/eaco/adapters/active_record/postgres_jsonb.rb +36 -0
- data/lib/eaco/adapters/couchrest_model.rb +37 -0
- data/lib/eaco/adapters/couchrest_model/couchdb_lucene.rb +71 -0
- data/lib/eaco/controller.rb +158 -0
- data/lib/eaco/cucumber.rb +11 -0
- data/lib/eaco/cucumber/active_record.rb +163 -0
- data/lib/eaco/cucumber/active_record/department.rb +19 -0
- data/lib/eaco/cucumber/active_record/document.rb +18 -0
- data/lib/eaco/cucumber/active_record/position.rb +21 -0
- data/lib/eaco/cucumber/active_record/schema.rb +36 -0
- data/lib/eaco/cucumber/active_record/user.rb +24 -0
- data/lib/eaco/cucumber/world.rb +136 -0
- data/lib/eaco/designator.rb +264 -0
- data/lib/eaco/dsl.rb +40 -0
- data/lib/eaco/dsl/acl.rb +163 -0
- data/lib/eaco/dsl/actor.rb +139 -0
- data/lib/eaco/dsl/actor/designators.rb +110 -0
- data/lib/eaco/dsl/base.rb +52 -0
- data/lib/eaco/dsl/resource.rb +129 -0
- data/lib/eaco/dsl/resource/permissions.rb +131 -0
- data/lib/eaco/error.rb +36 -0
- data/lib/eaco/railtie.rb +46 -0
- data/lib/eaco/rake.rb +10 -0
- data/lib/eaco/rake/default_task.rb +164 -0
- data/lib/eaco/resource.rb +234 -0
- data/lib/eaco/version.rb +7 -0
- data/spec/eaco/acl_spec.rb +147 -0
- data/spec/eaco/actor_spec.rb +13 -0
- data/spec/eaco/adapters/active_record/postgres_jsonb_spec.rb +9 -0
- data/spec/eaco/adapters/active_record_spec.rb +13 -0
- data/spec/eaco/adapters/couchrest_model/couchdb_lucene_spec.rb +9 -0
- data/spec/eaco/adapters/couchrest_model_spec.rb +9 -0
- data/spec/eaco/controller_spec.rb +12 -0
- data/spec/eaco/designator_spec.rb +25 -0
- data/spec/eaco/dsl/acl_spec.rb +9 -0
- data/spec/eaco/dsl/actor/designators_spec.rb +7 -0
- data/spec/eaco/dsl/actor_spec.rb +15 -0
- data/spec/eaco/dsl/resource/permissions_spec.rb +7 -0
- data/spec/eaco/dsl/resource_spec.rb +17 -0
- data/spec/eaco/error_spec.rb +9 -0
- data/spec/eaco/resource_spec.rb +31 -0
- data/spec/eaco_spec.rb +49 -0
- data/spec/spec_helper.rb +71 -0
- metadata +296 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e72095b7ec920c9b83c284fa3c7331253733bbc8
|
4
|
+
data.tar.gz: 40f6b7c39dc3796bf3495f57d7f4708d233037f4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a8834f93d09048dad12cb0afd5dcae646b8dccb73c193320e6a9eb1bb4a7292b642ef0a02ec9db437616d8ecda44295237853b849138a9157d6e59af8fe7945b
|
7
|
+
data.tar.gz: f107e5cef842ee26018c0fd148ae9ea4d4d91a64f49763b892c56ffe108ee6cc30276661254dadca5b3297c3bf83ce1b7a3075b0ff92ae8411952a1423281994
|
data/.gitignore
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
gemfiles/*.gemfile.lock
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
features/active_record.yml
|
12
|
+
features/active_record.log
|
13
|
+
lib/bundler/man
|
14
|
+
pkg
|
15
|
+
rdoc
|
16
|
+
tmp
|
17
|
+
*.bundle
|
18
|
+
*.so
|
19
|
+
*.o
|
20
|
+
*.a
|
21
|
+
mkmf.log
|
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
rvm:
|
2
|
+
- 2.0.0
|
3
|
+
- 2.1.5
|
4
|
+
- 2.2.0
|
5
|
+
|
6
|
+
gemfile:
|
7
|
+
- gemfiles/rails_3.2.gemfile
|
8
|
+
- gemfiles/rails_4.0.gemfile
|
9
|
+
- gemfiles/rails_4.1.gemfile
|
10
|
+
- gemfiles/rails_4.2.gemfile
|
11
|
+
|
12
|
+
addons:
|
13
|
+
postgresql: "9.4"
|
14
|
+
|
15
|
+
before_script:
|
16
|
+
- psql -c "CREATE DATABASE eaco;" -U postgres
|
17
|
+
|
18
|
+
script: bundle exec rake EACO_AR_CONFIG=./features/active_record.travis.yml
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--protected --private --no-private --hide-void-return '{lib,features/support}/**/*.rb' - README.md LICENSE.txt
|
data/Appraisals
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Test against 3.2 -> 4.2
|
2
|
+
#
|
3
|
+
appraise 'rails-3.2' do
|
4
|
+
gem 'rails', '~> 3.2.0'
|
5
|
+
gem 'pg'
|
6
|
+
gem 'activerecord-postgres-json'
|
7
|
+
end
|
8
|
+
|
9
|
+
appraise 'rails-4.0' do
|
10
|
+
gem 'rails', '~> 4.0.0'
|
11
|
+
gem 'pg'
|
12
|
+
end
|
13
|
+
|
14
|
+
appraise 'rails-4.1' do
|
15
|
+
gem 'rails', '~> 4.1.0'
|
16
|
+
gem 'pg'
|
17
|
+
end
|
18
|
+
|
19
|
+
appraise 'rails-4.2' do
|
20
|
+
gem 'rails', '~> 4.2.0'
|
21
|
+
gem 'pg'
|
22
|
+
end
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# Eaco's Guardfile
|
2
|
+
|
3
|
+
# Watch lib/ and spec/
|
4
|
+
directories %w(lib spec)
|
5
|
+
|
6
|
+
# Clear the screen before every task
|
7
|
+
clearing :on
|
8
|
+
|
9
|
+
guard :rspec, version: 3, cmd: 'rspec' do
|
10
|
+
# When single specs change, run them.
|
11
|
+
watch(%r{^spec/.+_spec\.rb$})
|
12
|
+
|
13
|
+
# When spec_helper changes rerun all the specs.
|
14
|
+
watch('spec/spec_helper.rb') { "spec" }
|
15
|
+
|
16
|
+
# When a source changes run its unit spec.
|
17
|
+
watch(%r{^lib/(.+)\.rb$}) {|m| "spec/#{m[1]}_spec.rb"
|
18
|
+
end
|
19
|
+
|
20
|
+
guard :cucumber do
|
21
|
+
# When single features change, run them.
|
22
|
+
watch(%r{^features/.+\.feature$})
|
23
|
+
|
24
|
+
# When support code changes, rerun all features.
|
25
|
+
watch(%r{^features/support/.+$}) { 'features' }
|
26
|
+
|
27
|
+
# When a step definition for a feature changes, rerun the corresponding feature.
|
28
|
+
watch(%r{^features/step_definitions/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'features' }
|
29
|
+
end
|
30
|
+
|
31
|
+
guard :shell do
|
32
|
+
# Rerun scenarios when source code changes
|
33
|
+
watch(%r{^lib/.+\.rb$}) { 'cucumber' }
|
34
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Copyright (c) 2013-2015 IFAD
|
2
|
+
Copyright (c) 2013-2015 Marcello Barnaba <m.barnaba@ifad.org>
|
3
|
+
|
4
|
+
MIT License
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
a copy of this software and associated documentation files (the
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
21
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
22
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
23
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
# Eaco
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/ifad/eaco.svg)](https://travis-ci.org/ifad/eaco) *currently writing specs*
|
4
|
+
[![Code Climate](https://codeclimate.com/github/ifad/eaco/badges/gpa.svg)](https://codeclimate.com/github/ifad/eaco)
|
5
|
+
[![Inline docs](http://inch-ci.org/github/ifad/eaco.svg?branch=master)](http://inch-ci.org/github/ifad/eaco/master)
|
6
|
+
|
7
|
+
Eacus, the holder of the keys of Hades, is an ACL-based authorization
|
8
|
+
framework for Ruby.
|
9
|
+
|
10
|
+
![Eaco e Telamone][eaco-e-telamone]
|
11
|
+
|
12
|
+
## Design
|
13
|
+
|
14
|
+
Eaco provides your context's Resources discretionary access by an Actor.
|
15
|
+
Access to the Resource is determined using an ACL.
|
16
|
+
|
17
|
+
Different Actors can have different levels of access to the same Resource,
|
18
|
+
depending on their role as determined by the ACL.
|
19
|
+
|
20
|
+
To each role are granted a set of possible abilities, and access is verified
|
21
|
+
by checking whether a given actor can perform a specific ability.
|
22
|
+
|
23
|
+
Actors are described by their Designators, a pluggable mechanism to be
|
24
|
+
implemented in your application.
|
25
|
+
|
26
|
+
Each Actor has many designators that describe either its identity or its
|
27
|
+
belonging to a group or occupying a position in a department.
|
28
|
+
|
29
|
+
Designators are Ruby classes that can embed any sort of custom behaviour that
|
30
|
+
your application requires.
|
31
|
+
|
32
|
+
ACLs are hashes with designators as keys and roles as values. Extracting
|
33
|
+
authorized collections requires only an hash key lookup mechanism in your
|
34
|
+
database. Adapters are provided for PG's jsonb and for CouchDB-Lucene.
|
35
|
+
|
36
|
+
## Installation
|
37
|
+
|
38
|
+
Add this line to your application's Gemfile:
|
39
|
+
|
40
|
+
gem 'eaco', github: 'ifad/eaco'
|
41
|
+
|
42
|
+
And then execute:
|
43
|
+
|
44
|
+
$ bundle
|
45
|
+
|
46
|
+
## Usage
|
47
|
+
|
48
|
+
Create `config/authorization.rb` [(rdoc)](http://www.rubydoc.info/github/ifad/eaco/master/Eaco/DSL)
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
# Defines `Document` to be an authorized resource.
|
52
|
+
#
|
53
|
+
# Adds Document.accessible_by and Document#allows
|
54
|
+
#
|
55
|
+
authorize Document, using: :lucene do
|
56
|
+
roles :owner, :editor, :reader
|
57
|
+
|
58
|
+
permissions do
|
59
|
+
reader :read
|
60
|
+
editor reader, :edit
|
61
|
+
owner editor, :destroy
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Defines an actor and the sources from which the
|
66
|
+
# designators are harvested.
|
67
|
+
#
|
68
|
+
# Adds User#designators
|
69
|
+
#
|
70
|
+
actor User do
|
71
|
+
admin do |user|
|
72
|
+
user.admin?
|
73
|
+
end
|
74
|
+
|
75
|
+
designators do
|
76
|
+
user from: :id
|
77
|
+
group from: :groups
|
78
|
+
tag from: :tags
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
Given a Resource [(rdoc)](http://www.rubydoc.info/github/ifad/eaco/master/Eaco/Resource)
|
84
|
+
with an ACL [(rdoc)](http://www.rubydoc.info/github/ifad/eaco/master/Eaco/ACL):
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# An example ACL
|
88
|
+
>> document = Document.first
|
89
|
+
=> #<Document id:42 name:"President's report for loans.docx" [...]>
|
90
|
+
>> document.acl
|
91
|
+
=> #<Document::ACL {"user:10" => :owner, "group:reviewers" => :reader}>
|
92
|
+
```
|
93
|
+
|
94
|
+
and an Actor [(rdoc)](http://www.rubydoc.info/github/ifad/eaco/master/Eaco/Actor):
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
# An example Actor
|
98
|
+
>> user = User.find(10)
|
99
|
+
=> #<User id:10 name:"Bob Fropp" group_ids:['employees'], tags:['english']>
|
100
|
+
>> user.designators
|
101
|
+
=> #<Set{ #<Designator(User) value:10>, #<Designator(Group) value:"employees">, #<Designator(Tag) value:"english"> }
|
102
|
+
```
|
103
|
+
|
104
|
+
you can check if the Actor can perform a specific action on the Resource:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
>> user.can? :read, document
|
108
|
+
=> true
|
109
|
+
|
110
|
+
>> document.allows? :read, user
|
111
|
+
=> true
|
112
|
+
```
|
113
|
+
|
114
|
+
and which access level (`role`) the Actor has for this Resource:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
>> document.role_of user
|
118
|
+
=> :owner
|
119
|
+
|
120
|
+
>> boss = User.find_by_group('reviewer').first
|
121
|
+
=> #<User id:42 name:"Jake Leister" group_ids:['reviewers', 'bosses']>
|
122
|
+
|
123
|
+
>> document.role_of boss
|
124
|
+
=> :reader
|
125
|
+
|
126
|
+
>> boss.can? :read, document
|
127
|
+
=> true
|
128
|
+
|
129
|
+
>> boss.can? :destroy, document
|
130
|
+
=> false
|
131
|
+
|
132
|
+
>> user.can? :destroy, document
|
133
|
+
=> true
|
134
|
+
```
|
135
|
+
|
136
|
+
Grant reader access to a specific user:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
>> user
|
140
|
+
=> #<User id:42 name:"Bob Frop">
|
141
|
+
|
142
|
+
>> document.grant :reader, :user, user.id
|
143
|
+
=> #<Document::ACL "user:42" => :reader>
|
144
|
+
|
145
|
+
>> user.can? :read, document
|
146
|
+
=> true
|
147
|
+
```
|
148
|
+
|
149
|
+
Grant reader access to a group:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
>> user
|
153
|
+
=> #<User id:42 groups:['reviewers']>
|
154
|
+
|
155
|
+
>> document.grant :reader, :group, 3
|
156
|
+
=> #<Document::ACL "group:reviewers" => :reader>
|
157
|
+
|
158
|
+
>> user.can? :read, document
|
159
|
+
=> true
|
160
|
+
|
161
|
+
>> document.allows? :read, user
|
162
|
+
=> true
|
163
|
+
```
|
164
|
+
|
165
|
+
Obtain a collection of Resources accessible by a given Actor [(rdoc)](http://www.rubydoc.info/github/ifad/eaco/master/Eaco/Adapters):
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
>> Document.accessible_by(user)
|
169
|
+
```
|
170
|
+
|
171
|
+
Check whether a controller action can be accessed by an user. Your
|
172
|
+
`ApplicationController` must respond to `current_user` for this to work.
|
173
|
+
[(rdoc)](http://www.rubydoc.info/github/ifad/eaco/master/Eaco/Controller)
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
class DocumentsController < ApplicationController
|
177
|
+
before_filter :find_document
|
178
|
+
|
179
|
+
authorize :edit, :update, [:document, :read]
|
180
|
+
|
181
|
+
private
|
182
|
+
def find_document
|
183
|
+
@document = Document.find(:id)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
## Running specs
|
189
|
+
|
190
|
+
You need a running postgresql 9.4 instance.
|
191
|
+
|
192
|
+
Create an user and a database:
|
193
|
+
|
194
|
+
$ sudo -u postgres psql
|
195
|
+
|
196
|
+
postgres=# CREATE ROLE eaco LOGIN;
|
197
|
+
CREATE ROLE
|
198
|
+
|
199
|
+
postgres=# CREATE DATABASE eaco OWNER eaco ENCODING 'utf8';
|
200
|
+
CREATE DATABASE
|
201
|
+
|
202
|
+
postgres=# ^D
|
203
|
+
|
204
|
+
Create `features/active_record.yml` with your database configuration,
|
205
|
+
see `features/active_record.example.yml` for an example.
|
206
|
+
|
207
|
+
Run `bundle` once. This will install the base bundle.
|
208
|
+
|
209
|
+
Run `appraisal` once. This will install the supported Rails versions and pg.
|
210
|
+
|
211
|
+
Run `rake`. This will run the specs and cucumber features.
|
212
|
+
|
213
|
+
Specs are run against the supported rails versions in turn. If you want to
|
214
|
+
focus on a single release, use `appraisal rails-X.Y rake`, where `X.Y` can be
|
215
|
+
`3.2`, `4.0`, `4.1` or `4.2`.
|
216
|
+
|
217
|
+
## Contributing
|
218
|
+
|
219
|
+
1. Fork it ( https://github.com/ifad/eaco/fork )
|
220
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
221
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
222
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
223
|
+
5. Create a new Pull Request
|
224
|
+
|
225
|
+
[eaco-e-telamone]: http://upload.wikimedia.org/wikipedia/commons/7/70/Aeacus_telemon.jpg "Aeacus telemon by user Ravenous at en.wikipedia.org - Public domain through Wikimedia Commons - http://commons.wikimedia.org/wiki/File:Aeacus_telemon.jpg#mediaviewer/File:Aeacus_telemon.jpg"
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Bundler
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
|
5
|
+
# YARD
|
6
|
+
require 'yard'
|
7
|
+
YARD::Rake::YardocTask.new
|
8
|
+
|
9
|
+
# RSpec
|
10
|
+
require 'rspec/core/rake_task'
|
11
|
+
RSpec::Core::RakeTask.new
|
12
|
+
|
13
|
+
# Appraisal
|
14
|
+
require 'appraisal/task'
|
15
|
+
Appraisal::Task.new
|
16
|
+
|
17
|
+
# Cucumber
|
18
|
+
require 'cucumber'
|
19
|
+
require 'cucumber/rake/task'
|
20
|
+
Cucumber::Rake::Task.new
|
21
|
+
|
22
|
+
# Our default rake task
|
23
|
+
require 'eaco/rake'
|
24
|
+
Eaco::Rake::DefaultTask.new
|
25
|
+
|
26
|
+
# Thanks for reading.
|
data/eaco.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'eaco/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "eaco"
|
8
|
+
spec.version = Eaco::VERSION
|
9
|
+
spec.authors = ["Marcello Barnaba"]
|
10
|
+
spec.email = ["vjt@openssl.it"]
|
11
|
+
spec.summary = %q{Authorization framework}
|
12
|
+
spec.homepage = "https://github.com/ifad/eaco"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
[
|
21
|
+
["bundler", "~> 1.6"],
|
22
|
+
"rake", "byebug", "guard", "yard", "appraisal",
|
23
|
+
"rspec", "guard-rspec", "yard-rspec",
|
24
|
+
"cucumber", "guard-cucumber"
|
25
|
+
|
26
|
+
].each {|gem| spec.add_development_dependency *gem }
|
27
|
+
end
|