active_record_rollout 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +261 -0
  7. data/Rakefile +1 -0
  8. data/active_record_rollout.gemspec +32 -0
  9. data/lib/active_record/rollout.rb +35 -0
  10. data/lib/active_record/rollout/acts_as_flaggable.rb +36 -0
  11. data/lib/active_record/rollout/feature.rb +260 -0
  12. data/lib/active_record/rollout/flag.rb +13 -0
  13. data/lib/active_record/rollout/flaggable.rb +66 -0
  14. data/lib/active_record/rollout/flaggable_flag.rb +9 -0
  15. data/lib/active_record/rollout/group_flag.rb +7 -0
  16. data/lib/active_record/rollout/opt_out_flag.rb +9 -0
  17. data/lib/active_record/rollout/percentage_flag.rb +11 -0
  18. data/lib/active_record/rollout/version.rb +5 -0
  19. data/lib/generators/active_record_rollout_generator.rb +20 -0
  20. data/lib/generators/templates/active_record_rollout.rb +5 -0
  21. data/lib/generators/templates/migration.rb +24 -0
  22. data/lib/tasks/rollout.rake +119 -0
  23. data/spec/integration/flag_rollout_spec.rb +27 -0
  24. data/spec/integration/group_rollout_spec.rb +20 -0
  25. data/spec/integration/percentage_rollout_spec.rb +13 -0
  26. data/spec/lib/active_record/rollout/acts_as_flaggable_spec.rb +31 -0
  27. data/spec/lib/active_record/rollout/feature_spec.rb +235 -0
  28. data/spec/lib/active_record/rollout/flag_spec.rb +8 -0
  29. data/spec/lib/active_record/rollout/flaggable_flag_spec.rb +8 -0
  30. data/spec/lib/active_record/rollout/flaggable_spec.rb +149 -0
  31. data/spec/lib/active_record/rollout/group_flag_spec.rb +7 -0
  32. data/spec/lib/active_record/rollout/opt_out_flag_spec.rb +8 -0
  33. data/spec/lib/active_record/rollout/percentage_flag_spec.rb +10 -0
  34. data/spec/lib/tasks/rollout_rake_spec.rb +162 -0
  35. data/spec/spec_helper.rb +40 -0
  36. data/spec/support/schema.rb +13 -0
  37. data/spec/support/shared_contexts/rake.rb +20 -0
  38. metadata +222 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 72f43a6987b1c532109d5698080ed00713fbe041
4
+ data.tar.gz: 9b1a9a4044074c9d67c3f44b9dd1bcdb2db5b821
5
+ SHA512:
6
+ metadata.gz: ad41939d03da5248a4623751f51a15d16807383b70db30c7d42f205fee1e54f930a4e7a24bec710f9bbf996fbd11147488a4cf70bf6746762e47cdddba58fa6b
7
+ data.tar.gz: d792f7d1a073ba7a9f67c17fd1716f49e04ec84247ee44bd32285ee8d6b8f65801b9f8b4bab1190b809b32c4ad689589c29312455583d153acdf9a1c984e5a63
@@ -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
@@ -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 active_record_rollout.gemspec
4
+ gemspec
@@ -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.
@@ -0,0 +1,261 @@
1
+ # ActiveRecord::Rollout
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/active_record_rollout.png?branch=development
10
+ [master_image]: https://api.travis-ci.org/jclem/active_record_rollout.png?branch=master
11
+ [branch_status]: https://travis-ci.org/jclem/active_record_rollout/branches
12
+ [code_climate_image]: https://codeclimate.com/github/jclem/active_record_rollout.png
13
+ [code_climate]: https://codeclimate.com/github/jclem/active_record_rollout
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 'active_record_rollout', require: 'active_record/rollout'
42
+
43
+ And then execute:
44
+
45
+ $ bundle
46
+
47
+ Or install it yourself as:
48
+
49
+ $ gem install active_record_rollout
50
+
51
+ ## Usage
52
+
53
+ `ActiveRecord::Rollout` 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
+ `ActiveRecord::Rollout::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 rollout:create[new_ui]
125
+ ```
126
+
127
+ #### Destroying features
128
+
129
+ ```sh
130
+ $ bundle exec rake rollout: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 rollout: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 rollout: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 rollout: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 rollout: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 rollout: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 rollout: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 rollout: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 rollout: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
+ ActiveRecord::Rollout.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 rollout: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
+ `ActiveRecord::Rollout.configure`:
238
+
239
+ ```ruby
240
+ ActiveRecord::Rollout.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>/active_record_rollout/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
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "active_record/rollout/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "active_record_rollout"
8
+ spec.version = ActiveRecord::Rollout::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/active_record_rollout"
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 "shoulda-matchers"
29
+ spec.add_development_dependency "sqlite3-ruby"
30
+ spec.add_development_dependency "pry-debugger"
31
+ spec.add_development_dependency "yard"
32
+ end
@@ -0,0 +1,35 @@
1
+ require "active_record"
2
+ require "active_record/rollout/version"
3
+ require "active_record/rollout/feature"
4
+ require "active_record/rollout/flag"
5
+ require "active_record/rollout/flaggable_flag"
6
+ require "active_record/rollout/group_flag"
7
+ require "active_record/rollout/percentage_flag"
8
+ require "active_record/rollout/opt_out_flag"
9
+ require "active_record/rollout/flaggable"
10
+ require "active_record/rollout/acts_as_flaggable"
11
+
12
+ class ActiveRecord::Rollout
13
+ # Allows for configuration of ActiveRecord::Rollout::Feature, mostly intended
14
+ # for defining groups:
15
+ #
16
+ # @example
17
+ # ActiveRecord::Rollout.configure do |config|
18
+ # config.define_user_group :admins do |user|
19
+ # user.admin?
20
+ # end
21
+ # end
22
+ def self.configure(&block)
23
+ yield ActiveRecord::Rollout::Feature
24
+ end
25
+ end
26
+
27
+ class ActiveRecord::Rollout::Task < Rails::Railtie
28
+ rake_tasks do
29
+ Dir[File.join(File.dirname(__FILE__), '../tasks/*.rake')].each { |f| load f }
30
+ end
31
+ end
32
+
33
+ if defined?(ActiveRecord::Base)
34
+ ActiveRecord::Base.extend ActiveRecord::Rollout::ActsAsFlaggable
35
+ end
@@ -0,0 +1,36 @@
1
+ module ActiveRecord::Rollout::ActsAsFlaggable
2
+ # Sets up ActiveRecord associations for the including class, and includes
3
+ # {ActiveRecord::Rollout::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
+ @active_record_rollout_flaggable_find_by = :id
15
+
16
+ has_many :flaggable_flags,
17
+ as: :flaggable,
18
+ class_name: "ActiveRecord::Rollout::FlaggableFlag"
19
+
20
+ has_many :opt_out_flags,
21
+ as: :flaggable,
22
+ class_name: "ActiveRecord::Rollout::OptOutFlag"
23
+
24
+ has_many :features,
25
+ through: :flaggable_flags,
26
+ class_name: "ActiveRecord::Rollout::Feature"
27
+
28
+ if options[:find_by]
29
+ @active_record_rollout_flaggable_find_by = options[:find_by]
30
+ end
31
+
32
+ extend ActiveRecord::Rollout::Flaggable::ClassMethods
33
+ include ActiveRecord::Rollout::Flaggable
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,260 @@
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 ActiveRecord::Rollout::Feature < ActiveRecord::Base
4
+ # A hash representing the groups that have been defined.
5
+ @defined_groups = {}
6
+
7
+ self.table_name = :active_record_rollout_features
8
+
9
+ has_many :flaggable_flags
10
+ has_many :group_flags
11
+ has_many :percentage_flags
12
+ has_many :opt_out_flags
13
+ has_many :flags, dependent: :destroy
14
+
15
+ validates :name, presence: true, uniqueness: true
16
+
17
+ attr_accessible :name
18
+
19
+ # Determines whether or not the given instance has had the feature rolled out
20
+ # to it either via direct flagging-in, percentage, or by group membership.
21
+ #
22
+ # @example
23
+ # feature.match?(current_user)
24
+ #
25
+ # @param [ActiveRecord::Base] instance A record to be tested for feature
26
+ # rollout.
27
+ #
28
+ # @return Whether or not the given instance has the feature rolled out to it.
29
+ def match?(instance)
30
+ match_id?(instance) || match_percentage?(instance) || match_groups?(instance)
31
+ end
32
+
33
+ # Determines whether or not the given instance has had the feature rolled out
34
+ # to it via direct flagging-in.
35
+ #
36
+ # @example
37
+ # feature.match_id?(current_user)
38
+ #
39
+ # @param [ActiveRecord::Base] instance A record to be tested for feature
40
+ # rollout.
41
+ #
42
+ # @return Whether or not the given instance has the feature rolled out to it
43
+ # via direct flagging-in.
44
+ def match_id?(instance)
45
+ flaggable_flags.where(flaggable_type: instance.class, flaggable_id: instance.id).any?
46
+ end
47
+
48
+ # Determines whether or not the given instance has had the feature rolled out
49
+ # to it via percentage.
50
+ #
51
+ # @example
52
+ # feature.match_percentage?(current_user)
53
+ #
54
+ # @param [ActiveRecord::Base] instance A record to be tested for feature
55
+ # rollout.
56
+ #
57
+ # @return Whether or not the given instance has the feature rolled out to it
58
+ # via direct percentage.
59
+ def match_percentage?(instance)
60
+ percentage = percentage_flags.where("flaggable_type = ?", instance.class.to_s).first.try(:percentage)
61
+ instance.id % 10 < (percentage || 0) / 10
62
+ end
63
+
64
+ # Determines whether or not the given instance has had the feature rolled out
65
+ # to it via group membership.
66
+ #
67
+ # @example
68
+ # feature.match_groups?(current_user)
69
+ #
70
+ # @param [ActiveRecord::Base] instance A record to be tested for feature
71
+ # rollout.
72
+ #
73
+ # @return Whether or not the given instance has the feature rolled out to it
74
+ # via direct group membership.
75
+ def match_groups?(instance)
76
+ klass = instance.class.to_s
77
+
78
+ return unless self.class.defined_groups[klass]
79
+
80
+ group_names = group_flags.find_all_by_flaggable_type(klass).collect(&:group_name)
81
+
82
+ self.class.defined_groups[klass].collect { |group_name, block|
83
+ block.call(instance) if group_names.include? group_name
84
+ }.any?
85
+ end
86
+
87
+ class << self
88
+ # Returns the defined groups.
89
+ def defined_groups
90
+ @defined_groups
91
+ end
92
+
93
+ # Returns the default flaggable class.
94
+ def default_flaggable_class_name
95
+ @default_flaggable_class_name
96
+ end
97
+
98
+ # Sets the default flaggable class.
99
+ def default_flaggable_class_name=(klass)
100
+ @default_flaggable_class_name = klass
101
+ end
102
+
103
+ # Add a record to the given feature. If the feature is not found, an
104
+ # ActiveRecord::RecordNotFound will be raised.
105
+ #
106
+ # @example
107
+ # ActiveRecord::Rollout::Feature.add_record_to_feature user, :new_ui
108
+ #
109
+ # @param [ActiveRecord::Base] record A record to add the feature to.
110
+ # @param [String,Symbol] feature_name The feature to be added to the record.
111
+ #
112
+ # @return [ActiveRecord::Rollout::Flag] The
113
+ # {ActiveRecord::Rollout::Flag Flag} created.
114
+ def add_record_to_feature(record, feature_name)
115
+ feature = find_by_name!(feature_name)
116
+ feature.flaggable_flags.create!(flaggable: record)
117
+ end
118
+
119
+ # Remove a record from the given feature. If the feature is not found, an
120
+ # ActiveRecord::RecordNotFound will be raised.
121
+ #
122
+ # @example
123
+ # ActiveRecord::Rollout::Feature.remove_record_from_feature user, :new_ui
124
+ #
125
+ # @param [ActiveRecord::Base] record A record to remove the feature from.
126
+ # @param [String,Symbol] feature_name The feature to be removed from the
127
+ # record.
128
+ def remove_record_from_feature(record, feature_name)
129
+ feature = find_by_name!(feature_name)
130
+ feature.flaggable_flags.where(flaggable_type: record.class, flaggable_id: record.id).destroy_all
131
+ end
132
+
133
+ # Opt the given record out of a feature. If the feature is not found, an
134
+ # ActiveRecord::RecordNotFound will be raised. An opt out ensures that no
135
+ # matter what, `record.rollout?(:rollout)` will always return false for any
136
+ # opted-out-of features.
137
+ #
138
+ # @param [ActiveRecord::Base] record A record to opt out of the feature.
139
+ # @param [String,Symbol] feature_name The feature to be opted out of.
140
+ #
141
+ # @example
142
+ # ActiveRecord::Rollout::Feature.opt_record_out_of_feature user, :new_ui
143
+ #
144
+ # @return [ActiveRecord::Rollout::OptOut] The
145
+ # {ActiveRecord::Rollout::OptOut OptOut} created.
146
+ def opt_record_out_of_feature(record, feature_name)
147
+ feature = find_by_name!(feature_name)
148
+ feature.opt_out_flags.create!(flaggable: record)
149
+ end
150
+
151
+ # Remove any opt out for the given record out of a feature. If the feature
152
+ # is not found, an ActiveRecord::RecordNotFound will be raised.
153
+ #
154
+ # @example
155
+ # ActiveRecord::Rollout::Feature.un_opt_record_out_of_feature user, :new_ui
156
+ #
157
+ # @param [ActiveRecord::Base] record A record to un-opt-out of the feature.
158
+ # @param [String,Symbol] feature_name The feature to be un-opted-out of.
159
+ def un_opt_record_out_of_feature(record, feature_name)
160
+ feature = find_by_name!(feature_name)
161
+ feature.opt_out_flags.where(flaggable_type: record.class.to_s, flaggable_id: record.id).destroy_all
162
+ end
163
+
164
+ # Add a group to the given feature. If the feature is not found, an
165
+ # ActiveRecord::RecordNotFound will be raised.
166
+ #
167
+ # @example
168
+ # ActiveRecord::Rollout::Feature.add_group_to_feature "User", "admin", :delete_records
169
+ #
170
+ # @param [String] flaggable_type The class (as a string) that the group
171
+ # should be associated with.
172
+ # @param [String] group_name The name of the group to have the feature
173
+ # added to it.
174
+ # @param [String,Symbol] feature_name The feature to be added to the group.
175
+ #
176
+ # @return [ActiveRecord::Rollout::Flag] The
177
+ # {ActiveRecord::Rollout::Flag Flag} created.
178
+ def add_group_to_feature(flaggable_type, group_name, feature_name)
179
+ feature = find_by_name!(feature_name)
180
+ feature.group_flags.create!(flaggable_type: flaggable_type, group_name: group_name)
181
+ end
182
+
183
+ # Remove a group from agiven feature. If the feature is not found, an
184
+ # ActiveRecord::RecordNotFound will be raised.
185
+ #
186
+ # @example
187
+ # ActiveRecord::Rollout::Feature.remove_group_from_feature "User", "admin", :delete_records
188
+ #
189
+ # @param [String] flaggable_type The class (as a string) that the group should
190
+ # be removed from.
191
+ # @param [String] group_name The name of the group to have the feature
192
+ # removed from it.
193
+ # @param [String,Symbol] feature_name The feature to be removed from the
194
+ # group.
195
+ def remove_group_from_feature(flaggable_type, group_name, feature_name)
196
+ feature = find_by_name!(feature_name)
197
+ feature.group_flags.where(flaggable_type: flaggable_type, group_name: group_name).destroy_all
198
+ end
199
+
200
+ # Add a percentage of records to the given feature. If the feature is not
201
+ # found, an ActiveRecord::RecordNotFound will be raised.
202
+ #
203
+ # @example
204
+ # ActiveRecord::Rollout::Feature.add_percentage_to_feature "User", 75, :delete_records
205
+ #
206
+ # @param [String] flaggable_type The class (as a string) that the percetnage
207
+ # should be associated with.
208
+ # @param [Integer] percentage The percentage of `flaggable_type` records
209
+ # that the feature will be available for.
210
+ # @param [String,Symbol] feature_name The feature to be added to the
211
+ # percentage of records.
212
+ #
213
+ # @return [ActiveRecord::Rollout::Flag] The
214
+ # {ActiveRecord::Rollout::Flag Flag} created.
215
+ def add_percentage_to_feature(flaggable_type, percentage, feature_name)
216
+ feature = find_by_name!(feature_name)
217
+
218
+ flag = feature.percentage_flags.where(flaggable_type: flaggable_type).first_or_initialize
219
+ flag.update_attributes!(percentage: percentage)
220
+ end
221
+
222
+ # Remove any percentage flags for the given feature. If the feature is not
223
+ # found, an ActiveRecord::RecordNotFound will be raised.
224
+ #
225
+ # @example
226
+ # ActiveRecord::Rollout::Feature.remove_percentage_from_feature "User", delete_records
227
+ #
228
+ # @param [String] flaggable_type The class (as a string) that the percetnage
229
+ # should be removed from.
230
+ # @param [String,Symbol] feature_name The feature to have the percentage
231
+ # flag removed from.
232
+ def remove_percentage_from_feature(flaggable_type, feature_name)
233
+ feature = find_by_name!(feature_name)
234
+ feature.percentage_flags.where(flaggable_type: flaggable_type).destroy_all
235
+ end
236
+
237
+ # Allows for methods of the form `define_user_group` that call the private
238
+ # method `define_group_for_class`. A new group for any `User` records will
239
+ # be created that rollouts can be attached to.
240
+ #
241
+ # @example
242
+ # ActiveRecord::Rollout::Feature.define_user_group :admins do |user|
243
+ # user.admin?
244
+ # end
245
+ def method_missing(method, *args, &block)
246
+ if /^define_(?<klass>[a-z0-9_]+)_group/ =~ method
247
+ define_group_for_class(klass.classify, args[0], &block)
248
+ else
249
+ super
250
+ end
251
+ end
252
+
253
+ private
254
+
255
+ def define_group_for_class(klass, group_name, &block)
256
+ @defined_groups[klass] ||= {}
257
+ @defined_groups[klass][group_name] = block
258
+ end
259
+ end
260
+ end