This repository has been archived by the owner on Aug 22, 2019. It is now read-only.
/
feature.rb
312 lines (277 loc) · 11.5 KB
/
feature.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# Represents an individual feature that may be rolled out to a set of records
# via individual flags, percentages, or defined groups.
class ActiveRecord::Rollout::Feature < ActiveRecord::Base
# A hash representing the groups that have been defined.
@defined_groups = {}
# Directories to grep for feature tests
@grep_dirs = []
self.table_name = :active_record_rollout_features
has_many :flaggable_flags
has_many :group_flags
has_many :percentage_flags
has_many :opt_out_flags
has_many :flags, dependent: :destroy
validates :name, presence: true, uniqueness: true
attr_accessible :name
# Returns an instance variable intended to hold an array of the lines of code
# that this feature appears on.
#
# @return [Array<String>] The lines that this rollout appears on (if
# {ActiveRecord::Rollout::Feature.all_with_lines} has already been called).
def lines
@lines ||= []
end
# Determines whether or not the given instance has had the feature rolled out
# to it either via direct flagging-in, percentage, or by group membership.
#
# @example
# feature.match?(current_user)
#
# @param [ActiveRecord::Base] instance A record to be tested for feature
# rollout.
#
# @return Whether or not the given instance has the feature rolled out to it.
def match?(instance)
match_id?(instance) || match_percentage?(instance) || match_groups?(instance)
end
# Determines whether or not the given instance has had the feature rolled out
# to it via direct flagging-in.
#
# @example
# feature.match_id?(current_user)
#
# @param [ActiveRecord::Base] instance A record to be tested for feature
# rollout.
#
# @return Whether or not the given instance has the feature rolled out to it
# via direct flagging-in.
def match_id?(instance)
flaggable_flags.where(flaggable_type: instance.class.to_s, flaggable_id: instance.id).any?
end
# Determines whether or not the given instance has had the feature rolled out
# to it via percentage.
#
# @example
# feature.match_percentage?(current_user)
#
# @param [ActiveRecord::Base] instance A record to be tested for feature
# rollout.
#
# @return Whether or not the given instance has the feature rolled out to it
# via direct percentage.
def match_percentage?(instance)
flag = percentage_flags.find(:first, conditions: ["flaggable_type = ?", instance.class.to_s])
percentage = flag ? flag.percentage : 0
instance.id % 10 < percentage / 10
end
# Determines whether or not the given instance has had the feature rolled out
# to it via group membership.
#
# @example
# feature.match_groups?(current_user)
#
# @param [ActiveRecord::Base] instance A record to be tested for feature
# rollout.
#
# @return Whether or not the given instance has the feature rolled out to it
# via direct group membership.
def match_groups?(instance)
klass = instance.class.to_s
return unless self.class.defined_groups[klass]
group_names = group_flags.find_all_by_flaggable_type(klass).collect(&:group_name)
self.class.defined_groups[klass].collect { |group_name, block|
block.call(instance) if group_names.include? group_name
}.any?
end
class << self
# Returns the defined groups.
def defined_groups
@defined_groups
end
# Returns the default flaggable class.
def default_flaggable_class_name
@default_flaggable_class_name
end
# Sets the default flaggable class.
def default_flaggable_class_name=(klass)
@default_flaggable_class_name = klass
end
# A list of directories to search through when finding feature checks.
def grep_dirs
@grep_dirs
end
# Set the list of directories to search through when finding feature checks.
def grep_dirs=(grep_dirs)
@grep_dirs = grep_dirs
end
# Return an array of both every feature in the database as well as every
# feature that is checked for in `@grep_dirs`. Features that are checked
# for but not persisted will be returned as unpersisted instances of this
# class. Each instance returned will have its `@lines` set to an array
# containing every line in `@grep_dirs` where it is checked for.
#
# @return [Array<ActiveRecord::Rollout::Feature>] Every persisted and
# checked-for feature.
def all_with_lines
obj = all.each_with_object({}) { |feature, obj| obj[feature.name] = feature }
Dir[*@grep_dirs].each do |path|
next if File.directory? path
File.open path do |file|
file.each_line.with_index(1) do |line, i|
line.scan(/\.has_feature\?\s*\(*:(\w+)/).each do |match|
match = match[0]
obj[match] ||= find_or_initialize_by_name(match)
obj[match].lines << "#{path}#L#{i}"
end
end
end
end
obj.values
end
# Add a record to the given feature. If the feature is not found, an
# ActiveRecord::RecordNotFound will be raised.
#
# @example
# ActiveRecord::Rollout::Feature.add_record_to_feature user, :new_ui
#
# @param [ActiveRecord::Base] record A record to add the feature to.
# @param [String,Symbol] feature_name The feature to be added to the record.
#
# @return [ActiveRecord::Rollout::Flag] The
# {ActiveRecord::Rollout::Flag Flag} created.
def add_record_to_feature(record, feature_name)
feature = find_by_name!(feature_name)
feature.flaggable_flags.where(flaggable_type: record.class.to_s, flaggable_id: record.id).first_or_create!
end
# Remove a record from the given feature. If the feature is not found, an
# ActiveRecord::RecordNotFound will be raised.
#
# @example
# ActiveRecord::Rollout::Feature.remove_record_from_feature user, :new_ui
#
# @param [ActiveRecord::Base] record A record to remove the feature from.
# @param [String,Symbol] feature_name The feature to be removed from the
# record.
def remove_record_from_feature(record, feature_name)
feature = find_by_name!(feature_name)
feature.flaggable_flags.where(flaggable_type: record.class.to_s, flaggable_id: record.id).destroy_all
end
# Opt the given record out of a feature. If the feature is not found, an
# ActiveRecord::RecordNotFound will be raised. An opt out ensures that no
# matter what, `record.rollout?(:rollout)` will always return false for any
# opted-out-of features.
#
# @param [ActiveRecord::Base] record A record to opt out of the feature.
# @param [String,Symbol] feature_name The feature to be opted out of.
#
# @example
# ActiveRecord::Rollout::Feature.opt_record_out_of_feature user, :new_ui
#
# @return [ActiveRecord::Rollout::OptOut] The
# {ActiveRecord::Rollout::OptOut OptOut} created.
def opt_record_out_of_feature(record, feature_name)
feature = find_by_name!(feature_name)
feature.opt_out_flags.where(flaggable_type: record.class.to_s, flaggable_id: record.id).first_or_create!
end
# Remove any opt out for the given record out of a feature. If the feature
# is not found, an ActiveRecord::RecordNotFound will be raised.
#
# @example
# ActiveRecord::Rollout::Feature.un_opt_record_out_of_feature user, :new_ui
#
# @param [ActiveRecord::Base] record A record to un-opt-out of the feature.
# @param [String,Symbol] feature_name The feature to be un-opted-out of.
def un_opt_record_out_of_feature(record, feature_name)
feature = find_by_name!(feature_name)
feature.opt_out_flags.where(flaggable_type: record.class.to_s, flaggable_id: record.id).destroy_all
end
# Add a group to the given feature. If the feature is not found, an
# ActiveRecord::RecordNotFound will be raised.
#
# @example
# ActiveRecord::Rollout::Feature.add_group_to_feature "User", "admin", :delete_records
#
# @param [String] flaggable_type The class (as a string) that the group
# should be associated with.
# @param [String] group_name The name of the group to have the feature
# added to it.
# @param [String,Symbol] feature_name The feature to be added to the group.
#
# @return [ActiveRecord::Rollout::Flag] The
# {ActiveRecord::Rollout::Flag Flag} created.
def add_group_to_feature(flaggable_type, group_name, feature_name)
feature = find_by_name!(feature_name)
feature.group_flags.where(flaggable_type: flaggable_type, group_name: group_name).first_or_create!
end
# Remove a group from agiven feature. If the feature is not found, an
# ActiveRecord::RecordNotFound will be raised.
#
# @example
# ActiveRecord::Rollout::Feature.remove_group_from_feature "User", "admin", :delete_records
#
# @param [String] flaggable_type The class (as a string) that the group should
# be removed from.
# @param [String] group_name The name of the group to have the feature
# removed from it.
# @param [String,Symbol] feature_name The feature to be removed from the
# group.
def remove_group_from_feature(flaggable_type, group_name, feature_name)
feature = find_by_name!(feature_name)
feature.group_flags.where(flaggable_type: flaggable_type, group_name: group_name).destroy_all
end
# Add a percentage of records to the given feature. If the feature is not
# found, an ActiveRecord::RecordNotFound will be raised.
#
# @example
# ActiveRecord::Rollout::Feature.add_percentage_to_feature "User", 75, :delete_records
#
# @param [String] flaggable_type The class (as a string) that the percetnage
# should be associated with.
# @param [Integer] percentage The percentage of `flaggable_type` records
# that the feature will be available for.
# @param [String,Symbol] feature_name The feature to be added to the
# percentage of records.
#
# @return [ActiveRecord::Rollout::Flag] The
# {ActiveRecord::Rollout::Flag Flag} created.
def add_percentage_to_feature(flaggable_type, percentage, feature_name)
feature = find_by_name!(feature_name)
flag = feature.percentage_flags.where(flaggable_type: flaggable_type).first_or_initialize
flag.update_attributes!(percentage: percentage)
end
# Remove any percentage flags for the given feature. If the feature is not
# found, an ActiveRecord::RecordNotFound will be raised.
#
# @example
# ActiveRecord::Rollout::Feature.remove_percentage_from_feature "User", delete_records
#
# @param [String] flaggable_type The class (as a string) that the percetnage
# should be removed from.
# @param [String,Symbol] feature_name The feature to have the percentage
# flag removed from.
def remove_percentage_from_feature(flaggable_type, feature_name)
feature = find_by_name!(feature_name)
feature.percentage_flags.where(flaggable_type: flaggable_type).destroy_all
end
# Allows for methods of the form `define_user_group` that call the private
# method `define_group_for_class`. A new group for any `User` records will
# be created that rollouts can be attached to.
#
# @example
# ActiveRecord::Rollout::Feature.define_user_group :admins do |user|
# user.admin?
# end
def method_missing(method, *args, &block)
if /^define_(?<klass>[a-z0-9_]+)_group/ =~ method
define_group_for_class(klass.classify, args[0], &block)
else
super
end
end
private
def define_group_for_class(klass, group_name, &block)
@defined_groups[klass] ||= {}
@defined_groups[klass][group_name] = block
end
end
end