active_record_rollout 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.
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