detour 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/.gitignore +18 -0
  2. data/.travis.yml +6 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +261 -0
  6. data/Rakefile +1 -0
  7. data/detour.gemspec +33 -0
  8. data/lib/detour/acts_as_flaggable.rb +36 -0
  9. data/lib/detour/feature.rb +312 -0
  10. data/lib/detour/flag.rb +13 -0
  11. data/lib/detour/flaggable.rb +66 -0
  12. data/lib/detour/flaggable_flag.rb +10 -0
  13. data/lib/detour/group_flag.rb +8 -0
  14. data/lib/detour/opt_out_flag.rb +10 -0
  15. data/lib/detour/percentage_flag.rb +11 -0
  16. data/lib/detour/version.rb +3 -0
  17. data/lib/detour.rb +35 -0
  18. data/lib/generators/active_record_rollout_generator.rb +20 -0
  19. data/lib/generators/templates/active_record_rollout.rb +5 -0
  20. data/lib/generators/templates/migration.rb +30 -0
  21. data/lib/tasks/detour.rake +119 -0
  22. data/spec/integration/flag_rollout_spec.rb +27 -0
  23. data/spec/integration/group_rollout_spec.rb +20 -0
  24. data/spec/integration/percentage_rollout_spec.rb +13 -0
  25. data/spec/lib/active_record/rollout/acts_as_flaggable_spec.rb +31 -0
  26. data/spec/lib/active_record/rollout/feature_spec.rb +280 -0
  27. data/spec/lib/active_record/rollout/flag_spec.rb +8 -0
  28. data/spec/lib/active_record/rollout/flaggable_flag_spec.rb +9 -0
  29. data/spec/lib/active_record/rollout/flaggable_spec.rb +149 -0
  30. data/spec/lib/active_record/rollout/group_flag_spec.rb +8 -0
  31. data/spec/lib/active_record/rollout/opt_out_flag_spec.rb +9 -0
  32. data/spec/lib/active_record/rollout/percentage_flag_spec.rb +10 -0
  33. data/spec/lib/tasks/detour_rake_spec.rb +162 -0
  34. data/spec/spec_helper.rb +40 -0
  35. data/spec/support/schema.rb +13 -0
  36. data/spec/support/shared_contexts/rake.rb +20 -0
  37. metadata +258 -0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ spec/spec.sqlite3
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - 2.0.0
6
+ script: bundle exec rspec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in detour.gemspec
4
+ gemspec
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