detour 0.0.1
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 +18 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +261 -0
- data/Rakefile +1 -0
- data/detour.gemspec +33 -0
- data/lib/detour/acts_as_flaggable.rb +36 -0
- data/lib/detour/feature.rb +312 -0
- data/lib/detour/flag.rb +13 -0
- data/lib/detour/flaggable.rb +66 -0
- data/lib/detour/flaggable_flag.rb +10 -0
- data/lib/detour/group_flag.rb +8 -0
- data/lib/detour/opt_out_flag.rb +10 -0
- data/lib/detour/percentage_flag.rb +11 -0
- data/lib/detour/version.rb +3 -0
- data/lib/detour.rb +35 -0
- data/lib/generators/active_record_rollout_generator.rb +20 -0
- data/lib/generators/templates/active_record_rollout.rb +5 -0
- data/lib/generators/templates/migration.rb +30 -0
- data/lib/tasks/detour.rake +119 -0
- data/spec/integration/flag_rollout_spec.rb +27 -0
- data/spec/integration/group_rollout_spec.rb +20 -0
- data/spec/integration/percentage_rollout_spec.rb +13 -0
- data/spec/lib/active_record/rollout/acts_as_flaggable_spec.rb +31 -0
- data/spec/lib/active_record/rollout/feature_spec.rb +280 -0
- data/spec/lib/active_record/rollout/flag_spec.rb +8 -0
- data/spec/lib/active_record/rollout/flaggable_flag_spec.rb +9 -0
- data/spec/lib/active_record/rollout/flaggable_spec.rb +149 -0
- data/spec/lib/active_record/rollout/group_flag_spec.rb +8 -0
- data/spec/lib/active_record/rollout/opt_out_flag_spec.rb +9 -0
- data/spec/lib/active_record/rollout/percentage_flag_spec.rb +10 -0
- data/spec/lib/tasks/detour_rake_spec.rb +162 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/support/schema.rb +13 -0
- data/spec/support/shared_contexts/rake.rb +20 -0
- metadata +258 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Jonathan Clem
|
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,261 @@
|
|
1
|
+
# Detour
|
2
|
+
|
3
|
+
Rollouts for `ActiveRecord` models. It is a spiritual fork of [ArRollout](https://github.com/markpundsack/ar_rollout).
|
4
|
+
|
5
|
+
| development status | master status | Code Climate |
|
6
|
+
| ------------------ | ------------- | ------------ |
|
7
|
+
| [![development status][dev_image]][branch_status] | [![master status][master_image]][branch_status] | [![Code Climate][code_climate_image]][code_climate]
|
8
|
+
|
9
|
+
[dev_image]: https://api.travis-ci.org/jclem/detour.png?branch=development
|
10
|
+
[master_image]: https://api.travis-ci.org/jclem/detour.png?branch=master
|
11
|
+
[branch_status]: https://travis-ci.org/jclem/detour/branches
|
12
|
+
[code_climate_image]: https://codeclimate.com/github/jclem/detour.png
|
13
|
+
[code_climate]: https://codeclimate.com/github/jclem/detour
|
14
|
+
|
15
|
+
## Contents
|
16
|
+
|
17
|
+
- [Installation](#installation)
|
18
|
+
- [Usage](#usage)
|
19
|
+
- [Marking a model as flaggable](#marking-a-model-as-flaggable)
|
20
|
+
- [Determining if a record is flagged into a feature](#determining-if-a-record-is-flagged-into-a-feature)
|
21
|
+
- [Feature operations](#feature-operations)
|
22
|
+
- [Creating features](#creating-features)
|
23
|
+
- [Destroying features](#destroying-features)
|
24
|
+
- [Flagging a record into a feature](#flagging-a-record-into-a-feature)
|
25
|
+
- [Removing a flag-in for a record for a feature](#removing-a-flag-in-for-a-record-for-a-feature)
|
26
|
+
- [Opt a record out of a feature](#opt-a-record-out-of-a-feature)
|
27
|
+
- [Un-opt out a record from a feature](#un-opt-out-a-record-from-a-feature)
|
28
|
+
- [Flag a programmatic group into a feature](#flag-a-programmatic-group-into-a-feature)
|
29
|
+
- [Remove a flag-in for a programmatic group for a feature](#remove-a-flag-in-for-a-programmatic-group-for-a-feature)
|
30
|
+
- [Flag a percentage of records into a feature](#flag-a-percentage-of-records-into-a-feature)
|
31
|
+
- [Remove a flag-in for a percentage of records for a feature](#remove-a-flag-in-for-a-percentage-of-records-for-a-feature)
|
32
|
+
- [Defining a default class](#defining-a-default-class)
|
33
|
+
- [Defining programmatic groups](#defining-programmatic-groups)
|
34
|
+
- [Contributing](#contributing)
|
35
|
+
|
36
|
+
|
37
|
+
## Installation
|
38
|
+
|
39
|
+
Add this line to your application's Gemfile:
|
40
|
+
|
41
|
+
gem 'detour'
|
42
|
+
|
43
|
+
And then execute:
|
44
|
+
|
45
|
+
$ bundle
|
46
|
+
|
47
|
+
Or install it yourself as:
|
48
|
+
|
49
|
+
$ gem install detour
|
50
|
+
|
51
|
+
## Usage
|
52
|
+
|
53
|
+
`Detour` works by determining whether or not a specific record
|
54
|
+
should have features accessible to it based on individual flags, flags for a
|
55
|
+
percentage of records, or flags for a programmable group of records.
|
56
|
+
|
57
|
+
### Marking a model as flaggable
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
class User < ActiveRecord::Base
|
61
|
+
acts_as_flaggable
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
Or, in order to user emails rather than IDs to identify records in rake tasks:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
class User < ActiveRecord::Base
|
69
|
+
acts_as_flaggable find_by: :email
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
This module adds `has_many` associations to `User` for `flaggable_flags` (where
|
74
|
+
the user has been individually flagged into a feature), `opt_out_flags` (where
|
75
|
+
the user has been opted out of a feature), and `features` (the features that
|
76
|
+
the user has been individually flagged into, regardless of opt outs).
|
77
|
+
|
78
|
+
However, these methods aren't enough to determine whether or not the user is
|
79
|
+
flagged into a specific feature. The `#has_feature?` method provided by
|
80
|
+
`Detour::Flaggable` should be used for this.
|
81
|
+
|
82
|
+
### Determining if a record is flagged into a feature
|
83
|
+
|
84
|
+
`#has_feature?` has two methods of use. The first one, where it is passed a
|
85
|
+
block, will increment a `failure_count` on the given feature if the block
|
86
|
+
raises an exception (the exception is again raised after incrementing). This
|
87
|
+
currently does not alter the behavior of the feature, but it services a metrics
|
88
|
+
purpose:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
current_user.has_feature? :new_user_interface do
|
92
|
+
render_new_user_interface
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
When not given a block, it simply returns a boolean, and does not watch for
|
97
|
+
exceptions:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
if current_user.has_feature? :new_user_interface
|
101
|
+
render_new_user_interface
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
Want to make use of both? `#has_feature?` returns a boolean even when passed
|
106
|
+
a block:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
if current_user.has_feature? :new_user_interface do
|
110
|
+
render_new_user_interface
|
111
|
+
end; else
|
112
|
+
render_old_user_interface
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
### Feature operations
|
117
|
+
|
118
|
+
Features and flags are intended to be controlled by a rake tasks. To create
|
119
|
+
them programmatically, consult the documentation.
|
120
|
+
|
121
|
+
#### Creating features
|
122
|
+
|
123
|
+
```sh
|
124
|
+
$ bundle exec rake detour:create[new_ui]
|
125
|
+
```
|
126
|
+
|
127
|
+
#### Destroying features
|
128
|
+
|
129
|
+
```sh
|
130
|
+
$ bundle exec rake detour:destroy[new_ui]
|
131
|
+
```
|
132
|
+
|
133
|
+
#### Flagging a record into a feature
|
134
|
+
|
135
|
+
This task requires passing the feature name, the record's class, and the
|
136
|
+
record's ID.
|
137
|
+
|
138
|
+
```sh
|
139
|
+
$ bundle exec rake detour:activate[new_ui,User,2]
|
140
|
+
```
|
141
|
+
|
142
|
+
#### Removing a flag-in for a record for a feature
|
143
|
+
|
144
|
+
This task requires passing the feature name, the record's class, and the
|
145
|
+
record's ID.
|
146
|
+
|
147
|
+
```sh
|
148
|
+
$ bundle exec rake detour:deactivate[new_ui,User,2]
|
149
|
+
```
|
150
|
+
|
151
|
+
#### Opt a record out of a feature
|
152
|
+
|
153
|
+
This will ensure that `record.has_feature?(:feature)` will always be false for
|
154
|
+
the given feature, regardless of other individual flag, percentage, or group
|
155
|
+
rollouts that would otherwise target this record.
|
156
|
+
|
157
|
+
This task requires passing the feature name, the record's class, and the
|
158
|
+
record's ID.
|
159
|
+
|
160
|
+
```sh
|
161
|
+
$ bundle exec rake detour:opt_out[new_ui,User,2]
|
162
|
+
```
|
163
|
+
|
164
|
+
#### Un-opt out a record from a feature
|
165
|
+
|
166
|
+
This task requires passing the feature name, the record's class, and the
|
167
|
+
record's ID.
|
168
|
+
|
169
|
+
```sh
|
170
|
+
$ bundle exec rake detour:un_opt_out[new_ui,User,2]
|
171
|
+
```
|
172
|
+
|
173
|
+
#### Flag a programmatic group into a feature
|
174
|
+
|
175
|
+
This task requires passing the feature name, the record class for the group,
|
176
|
+
and the name of the group.
|
177
|
+
|
178
|
+
```sh
|
179
|
+
$ bundle exec rake detour:activate_group[new_ui,User,admins]
|
180
|
+
```
|
181
|
+
|
182
|
+
#### Remove a flag-in for a programmatic group for a feature
|
183
|
+
|
184
|
+
This task requires passing the feature name, the record class for the group,
|
185
|
+
and the name of the group.
|
186
|
+
|
187
|
+
```sh
|
188
|
+
$ bundle exec rake detour:deactivate_group[new_ui,User,admins]
|
189
|
+
```
|
190
|
+
|
191
|
+
#### Flag a percentage of records into a feature
|
192
|
+
|
193
|
+
This relies on the following formula to determine if a record is flagged in to
|
194
|
+
a feature based on percentage:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
record.id % 10 < percentage / 10
|
198
|
+
```
|
199
|
+
|
200
|
+
This task requires passing the feature name, the record class for the group,
|
201
|
+
and the percentage of records to be flagged in.
|
202
|
+
|
203
|
+
```sh
|
204
|
+
$ bundle exec rake detour:activate_percentage[new_ui,User,20]
|
205
|
+
```
|
206
|
+
|
207
|
+
#### Remove a flag-in for a percentage of records for a feature
|
208
|
+
|
209
|
+
This task requires passing the feature name, and the record class for the group.
|
210
|
+
|
211
|
+
```sh
|
212
|
+
$ bundle exec rake detour:deactivate_percentage[new_ui,User]
|
213
|
+
```
|
214
|
+
|
215
|
+
### Defining a default class
|
216
|
+
|
217
|
+
In order to provide passing a class name into rake tasks, a default class can
|
218
|
+
be set:
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
Detour.configure do |config|
|
222
|
+
config.default_flaggable_class_name = "User"
|
223
|
+
end
|
224
|
+
```
|
225
|
+
|
226
|
+
Then, in your rake tasks:
|
227
|
+
|
228
|
+
```sh
|
229
|
+
# Will activate feature "foo" for all instances of User that match the admins group.
|
230
|
+
$ bundle exec rake detour:activate_group[foo,admins]
|
231
|
+
```
|
232
|
+
|
233
|
+
### Defining programmatic groups
|
234
|
+
|
235
|
+
A specific group of records matching a given block can be flagged into a
|
236
|
+
feature. In order to define these groups, use
|
237
|
+
`Detour.configure`:
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
Detour.configure do |config|
|
241
|
+
# Any User that returns truthy for `user.admin?` will be included in this
|
242
|
+
# group: `admins`.
|
243
|
+
config.define_user_group :admins do |user|
|
244
|
+
user.admin?
|
245
|
+
end
|
246
|
+
|
247
|
+
# Any FizzBuzz that returns truthy for `fizz_buzz.bar?` will be included in
|
248
|
+
# this group: `is_bar`.
|
249
|
+
config.define_fizz_buzz_group :is_bar do |fizz_buzz|
|
250
|
+
fizz_buzz.bar?
|
251
|
+
end
|
252
|
+
end
|
253
|
+
```
|
254
|
+
|
255
|
+
## Contributing
|
256
|
+
|
257
|
+
1. Fork it ( http://github.com/<my-github-username>/detour/fork )
|
258
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
259
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
260
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
261
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/detour.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "detour/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "detour"
|
8
|
+
spec.version = Detour::VERSION
|
9
|
+
spec.authors = ["Jonathan Clem"]
|
10
|
+
spec.email = ["jonathan@heroku.com"]
|
11
|
+
spec.summary = %q{Rollouts (feature flags) for ActiveRecord models.}
|
12
|
+
spec.description = %q{Rollouts (feature flags) for ActiveRecord models.}
|
13
|
+
spec.homepage = "https://github.com/jclem/detour"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.required_ruby_version = ">= 1.9.3"
|
22
|
+
|
23
|
+
spec.add_dependency "activerecord", "~> 3.2"
|
24
|
+
spec.add_dependency "rails", "~> 3.2"
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
26
|
+
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "rspec-rails"
|
28
|
+
spec.add_development_dependency "fakefs"
|
29
|
+
spec.add_development_dependency "shoulda-matchers"
|
30
|
+
spec.add_development_dependency "sqlite3-ruby"
|
31
|
+
spec.add_development_dependency "pry-debugger"
|
32
|
+
spec.add_development_dependency "yard"
|
33
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Detour::ActsAsFlaggable
|
2
|
+
# Sets up ActiveRecord associations for the including class, and includes
|
3
|
+
# {Detour::Flaggable} in the class.
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# class User < ActiveRecord::Base
|
7
|
+
# acts_as_taggable find_by: :email
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# @option options [Symbol] :find_by The field to find the record by when
|
11
|
+
# running rake tasks. Defaults to :id.
|
12
|
+
def acts_as_flaggable(options = {})
|
13
|
+
class_eval do
|
14
|
+
@detour_flaggable_find_by = :id
|
15
|
+
|
16
|
+
has_many :flaggable_flags,
|
17
|
+
as: :flaggable,
|
18
|
+
class_name: "Detour::FlaggableFlag"
|
19
|
+
|
20
|
+
has_many :opt_out_flags,
|
21
|
+
as: :flaggable,
|
22
|
+
class_name: "Detour::OptOutFlag"
|
23
|
+
|
24
|
+
has_many :features,
|
25
|
+
through: :flaggable_flags,
|
26
|
+
class_name: "Detour::Feature"
|
27
|
+
|
28
|
+
if options[:find_by]
|
29
|
+
@detour_flaggable_find_by = options[:find_by]
|
30
|
+
end
|
31
|
+
|
32
|
+
extend Detour::Flaggable::ClassMethods
|
33
|
+
include Detour::Flaggable
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,312 @@
|
|
1
|
+
# Represents an individual feature that may be rolled out to a set of records
|
2
|
+
# via individual flags, percentages, or defined groups.
|
3
|
+
class Detour::Feature < ActiveRecord::Base
|
4
|
+
# A hash representing the groups that have been defined.
|
5
|
+
@defined_groups = {}
|
6
|
+
|
7
|
+
# Directories to grep for feature tests
|
8
|
+
@grep_dirs = []
|
9
|
+
|
10
|
+
self.table_name = :detour_features
|
11
|
+
|
12
|
+
has_many :flaggable_flags
|
13
|
+
has_many :group_flags
|
14
|
+
has_many :percentage_flags
|
15
|
+
has_many :opt_out_flags
|
16
|
+
has_many :flags, dependent: :destroy
|
17
|
+
|
18
|
+
validates :name, presence: true, uniqueness: true
|
19
|
+
|
20
|
+
attr_accessible :name
|
21
|
+
|
22
|
+
# Returns an instance variable intended to hold an array of the lines of code
|
23
|
+
# that this feature appears on.
|
24
|
+
#
|
25
|
+
# @return [Array<String>] The lines that this rollout appears on (if
|
26
|
+
# {Detour::Feature.all_with_lines} has already been called).
|
27
|
+
def lines
|
28
|
+
@lines ||= []
|
29
|
+
end
|
30
|
+
|
31
|
+
# Determines whether or not the given instance has had the feature rolled out
|
32
|
+
# to it either via direct flagging-in, percentage, or by group membership.
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# feature.match?(current_user)
|
36
|
+
#
|
37
|
+
# @param [ActiveRecord::Base] instance A record to be tested for feature
|
38
|
+
# rollout.
|
39
|
+
#
|
40
|
+
# @return Whether or not the given instance has the feature rolled out to it.
|
41
|
+
def match?(instance)
|
42
|
+
match_id?(instance) || match_percentage?(instance) || match_groups?(instance)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Determines whether or not the given instance has had the feature rolled out
|
46
|
+
# to it via direct flagging-in.
|
47
|
+
#
|
48
|
+
# @example
|
49
|
+
# feature.match_id?(current_user)
|
50
|
+
#
|
51
|
+
# @param [ActiveRecord::Base] instance A record to be tested for feature
|
52
|
+
# rollout.
|
53
|
+
#
|
54
|
+
# @return Whether or not the given instance has the feature rolled out to it
|
55
|
+
# via direct flagging-in.
|
56
|
+
def match_id?(instance)
|
57
|
+
flaggable_flags.where(flaggable_type: instance.class.to_s, flaggable_id: instance.id).any?
|
58
|
+
end
|
59
|
+
|
60
|
+
# Determines whether or not the given instance has had the feature rolled out
|
61
|
+
# to it via percentage.
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# feature.match_percentage?(current_user)
|
65
|
+
#
|
66
|
+
# @param [ActiveRecord::Base] instance A record to be tested for feature
|
67
|
+
# rollout.
|
68
|
+
#
|
69
|
+
# @return Whether or not the given instance has the feature rolled out to it
|
70
|
+
# via direct percentage.
|
71
|
+
def match_percentage?(instance)
|
72
|
+
flag = percentage_flags.find(:first, conditions: ["flaggable_type = ?", instance.class.to_s])
|
73
|
+
percentage = flag ? flag.percentage : 0
|
74
|
+
|
75
|
+
instance.id % 10 < percentage / 10
|
76
|
+
end
|
77
|
+
|
78
|
+
# Determines whether or not the given instance has had the feature rolled out
|
79
|
+
# to it via group membership.
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# feature.match_groups?(current_user)
|
83
|
+
#
|
84
|
+
# @param [ActiveRecord::Base] instance A record to be tested for feature
|
85
|
+
# rollout.
|
86
|
+
#
|
87
|
+
# @return Whether or not the given instance has the feature rolled out to it
|
88
|
+
# via direct group membership.
|
89
|
+
def match_groups?(instance)
|
90
|
+
klass = instance.class.to_s
|
91
|
+
|
92
|
+
return unless self.class.defined_groups[klass]
|
93
|
+
|
94
|
+
group_names = group_flags.find_all_by_flaggable_type(klass).collect(&:group_name)
|
95
|
+
|
96
|
+
self.class.defined_groups[klass].collect { |group_name, block|
|
97
|
+
block.call(instance) if group_names.include? group_name
|
98
|
+
}.any?
|
99
|
+
end
|
100
|
+
|
101
|
+
class << self
|
102
|
+
# Returns the defined groups.
|
103
|
+
def defined_groups
|
104
|
+
@defined_groups
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns the default flaggable class.
|
108
|
+
def default_flaggable_class_name
|
109
|
+
@default_flaggable_class_name
|
110
|
+
end
|
111
|
+
|
112
|
+
# Sets the default flaggable class.
|
113
|
+
def default_flaggable_class_name=(klass)
|
114
|
+
@default_flaggable_class_name = klass
|
115
|
+
end
|
116
|
+
|
117
|
+
# A list of directories to search through when finding feature checks.
|
118
|
+
def grep_dirs
|
119
|
+
@grep_dirs
|
120
|
+
end
|
121
|
+
|
122
|
+
# Set the list of directories to search through when finding feature checks.
|
123
|
+
def grep_dirs=(grep_dirs)
|
124
|
+
@grep_dirs = grep_dirs
|
125
|
+
end
|
126
|
+
|
127
|
+
# Return an array of both every feature in the database as well as every
|
128
|
+
# feature that is checked for in `@grep_dirs`. Features that are checked
|
129
|
+
# for but not persisted will be returned as unpersisted instances of this
|
130
|
+
# class. Each instance returned will have its `@lines` set to an array
|
131
|
+
# containing every line in `@grep_dirs` where it is checked for.
|
132
|
+
#
|
133
|
+
# @return [Array<Detour::Feature>] Every persisted and
|
134
|
+
# checked-for feature.
|
135
|
+
def all_with_lines
|
136
|
+
obj = all.each_with_object({}) { |feature, obj| obj[feature.name] = feature }
|
137
|
+
|
138
|
+
Dir[*@grep_dirs].each do |path|
|
139
|
+
next if File.directory? path
|
140
|
+
|
141
|
+
File.open path do |file|
|
142
|
+
file.each_line.with_index(1) do |line, i|
|
143
|
+
line.scan(/\.has_feature\?\s*\(*:(\w+)/).each do |match|
|
144
|
+
match = match[0]
|
145
|
+
obj[match] ||= find_or_initialize_by_name(match)
|
146
|
+
obj[match].lines << "#{path}#L#{i}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
obj.values
|
153
|
+
end
|
154
|
+
|
155
|
+
# Add a record to the given feature. If the feature is not found, an
|
156
|
+
# ActiveRecord::RecordNotFound will be raised.
|
157
|
+
#
|
158
|
+
# @example
|
159
|
+
# Detour::Feature.add_record_to_feature user, :new_ui
|
160
|
+
#
|
161
|
+
# @param [ActiveRecord::Base] record A record to add the feature to.
|
162
|
+
# @param [String,Symbol] feature_name The feature to be added to the record.
|
163
|
+
#
|
164
|
+
# @return [Detour::Flag] The
|
165
|
+
# {Detour::Flag Flag} created.
|
166
|
+
def add_record_to_feature(record, feature_name)
|
167
|
+
feature = find_by_name!(feature_name)
|
168
|
+
feature.flaggable_flags.where(flaggable_type: record.class.to_s, flaggable_id: record.id).first_or_create!
|
169
|
+
end
|
170
|
+
|
171
|
+
# Remove a record from the given feature. If the feature is not found, an
|
172
|
+
# ActiveRecord::RecordNotFound will be raised.
|
173
|
+
#
|
174
|
+
# @example
|
175
|
+
# Detour::Feature.remove_record_from_feature user, :new_ui
|
176
|
+
#
|
177
|
+
# @param [ActiveRecord::Base] record A record to remove the feature from.
|
178
|
+
# @param [String,Symbol] feature_name The feature to be removed from the
|
179
|
+
# record.
|
180
|
+
def remove_record_from_feature(record, feature_name)
|
181
|
+
feature = find_by_name!(feature_name)
|
182
|
+
feature.flaggable_flags.where(flaggable_type: record.class.to_s, flaggable_id: record.id).destroy_all
|
183
|
+
end
|
184
|
+
|
185
|
+
# Opt the given record out of a feature. If the feature is not found, an
|
186
|
+
# ActiveRecord::RecordNotFound will be raised. An opt out ensures that no
|
187
|
+
# matter what, `record.rollout?(:rollout)` will always return false for any
|
188
|
+
# opted-out-of features.
|
189
|
+
#
|
190
|
+
# @param [ActiveRecord::Base] record A record to opt out of the feature.
|
191
|
+
# @param [String,Symbol] feature_name The feature to be opted out of.
|
192
|
+
#
|
193
|
+
# @example
|
194
|
+
# Detour::Feature.opt_record_out_of_feature user, :new_ui
|
195
|
+
#
|
196
|
+
# @return [Detour::OptOut] The
|
197
|
+
# {Detour::OptOut OptOut} created.
|
198
|
+
def opt_record_out_of_feature(record, feature_name)
|
199
|
+
feature = find_by_name!(feature_name)
|
200
|
+
feature.opt_out_flags.where(flaggable_type: record.class.to_s, flaggable_id: record.id).first_or_create!
|
201
|
+
end
|
202
|
+
|
203
|
+
# Remove any opt out for the given record out of a feature. If the feature
|
204
|
+
# is not found, an ActiveRecord::RecordNotFound will be raised.
|
205
|
+
#
|
206
|
+
# @example
|
207
|
+
# Detour::Feature.un_opt_record_out_of_feature user, :new_ui
|
208
|
+
#
|
209
|
+
# @param [ActiveRecord::Base] record A record to un-opt-out of the feature.
|
210
|
+
# @param [String,Symbol] feature_name The feature to be un-opted-out of.
|
211
|
+
def un_opt_record_out_of_feature(record, feature_name)
|
212
|
+
feature = find_by_name!(feature_name)
|
213
|
+
feature.opt_out_flags.where(flaggable_type: record.class.to_s, flaggable_id: record.id).destroy_all
|
214
|
+
end
|
215
|
+
|
216
|
+
# Add a group to the given feature. If the feature is not found, an
|
217
|
+
# ActiveRecord::RecordNotFound will be raised.
|
218
|
+
#
|
219
|
+
# @example
|
220
|
+
# Detour::Feature.add_group_to_feature "User", "admin", :delete_records
|
221
|
+
#
|
222
|
+
# @param [String] flaggable_type The class (as a string) that the group
|
223
|
+
# should be associated with.
|
224
|
+
# @param [String] group_name The name of the group to have the feature
|
225
|
+
# added to it.
|
226
|
+
# @param [String,Symbol] feature_name The feature to be added to the group.
|
227
|
+
#
|
228
|
+
# @return [Detour::Flag] The
|
229
|
+
# {Detour::Flag Flag} created.
|
230
|
+
def add_group_to_feature(flaggable_type, group_name, feature_name)
|
231
|
+
feature = find_by_name!(feature_name)
|
232
|
+
feature.group_flags.where(flaggable_type: flaggable_type, group_name: group_name).first_or_create!
|
233
|
+
end
|
234
|
+
|
235
|
+
# Remove a group from agiven feature. If the feature is not found, an
|
236
|
+
# ActiveRecord::RecordNotFound will be raised.
|
237
|
+
#
|
238
|
+
# @example
|
239
|
+
# Detour::Feature.remove_group_from_feature "User", "admin", :delete_records
|
240
|
+
#
|
241
|
+
# @param [String] flaggable_type The class (as a string) that the group should
|
242
|
+
# be removed from.
|
243
|
+
# @param [String] group_name The name of the group to have the feature
|
244
|
+
# removed from it.
|
245
|
+
# @param [String,Symbol] feature_name The feature to be removed from the
|
246
|
+
# group.
|
247
|
+
def remove_group_from_feature(flaggable_type, group_name, feature_name)
|
248
|
+
feature = find_by_name!(feature_name)
|
249
|
+
feature.group_flags.where(flaggable_type: flaggable_type, group_name: group_name).destroy_all
|
250
|
+
end
|
251
|
+
|
252
|
+
# Add a percentage of records to the given feature. If the feature is not
|
253
|
+
# found, an ActiveRecord::RecordNotFound will be raised.
|
254
|
+
#
|
255
|
+
# @example
|
256
|
+
# Detour::Feature.add_percentage_to_feature "User", 75, :delete_records
|
257
|
+
#
|
258
|
+
# @param [String] flaggable_type The class (as a string) that the percetnage
|
259
|
+
# should be associated with.
|
260
|
+
# @param [Integer] percentage The percentage of `flaggable_type` records
|
261
|
+
# that the feature will be available for.
|
262
|
+
# @param [String,Symbol] feature_name The feature to be added to the
|
263
|
+
# percentage of records.
|
264
|
+
#
|
265
|
+
# @return [Detour::Flag] The
|
266
|
+
# {Detour::Flag Flag} created.
|
267
|
+
def add_percentage_to_feature(flaggable_type, percentage, feature_name)
|
268
|
+
feature = find_by_name!(feature_name)
|
269
|
+
|
270
|
+
flag = feature.percentage_flags.where(flaggable_type: flaggable_type).first_or_initialize
|
271
|
+
flag.update_attributes!(percentage: percentage)
|
272
|
+
end
|
273
|
+
|
274
|
+
# Remove any percentage flags for the given feature. If the feature is not
|
275
|
+
# found, an ActiveRecord::RecordNotFound will be raised.
|
276
|
+
#
|
277
|
+
# @example
|
278
|
+
# Detour::Feature.remove_percentage_from_feature "User", delete_records
|
279
|
+
#
|
280
|
+
# @param [String] flaggable_type The class (as a string) that the percetnage
|
281
|
+
# should be removed from.
|
282
|
+
# @param [String,Symbol] feature_name The feature to have the percentage
|
283
|
+
# flag removed from.
|
284
|
+
def remove_percentage_from_feature(flaggable_type, feature_name)
|
285
|
+
feature = find_by_name!(feature_name)
|
286
|
+
feature.percentage_flags.where(flaggable_type: flaggable_type).destroy_all
|
287
|
+
end
|
288
|
+
|
289
|
+
# Allows for methods of the form `define_user_group` that call the private
|
290
|
+
# method `define_group_for_class`. A new group for any `User` records will
|
291
|
+
# be created that rollouts can be attached to.
|
292
|
+
#
|
293
|
+
# @example
|
294
|
+
# Detour::Feature.define_user_group :admins do |user|
|
295
|
+
# user.admin?
|
296
|
+
# end
|
297
|
+
def method_missing(method, *args, &block)
|
298
|
+
if /^define_(?<klass>[a-z0-9_]+)_group/ =~ method
|
299
|
+
define_group_for_class(klass.classify, args[0], &block)
|
300
|
+
else
|
301
|
+
super
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
private
|
306
|
+
|
307
|
+
def define_group_for_class(klass, group_name, &block)
|
308
|
+
@defined_groups[klass] ||= {}
|
309
|
+
@defined_groups[klass][group_name] = block
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|