detour 0.0.11 → 0.0.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/models/detour/feature.rb +0 -2
- data/app/models/detour/flag.rb +11 -0
- data/lib/detour/flaggable.rb +53 -13
- data/lib/detour/version.rb +1 -1
- data/spec/integration/group_rollout_spec.rb +1 -1
- data/spec/integration/percentage_rollout_spec.rb +1 -1
- data/spec/models/detour/feature_spec.rb +0 -80
- data/spec/models/detour/flag_spec.rb +21 -0
- metadata +1 -2
- data/app/models/detour/concerns/matchers.rb +0 -59
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 80d487ed6d4c933fbf39a7743ae7f14583c58335
|
4
|
+
data.tar.gz: b07a8fd7a7e3d6d430c1e7708edf77eae200b2de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf2629eb8e3d045326c69997e05f4a009918ead2a123ef903359d8ac8a39837dfa00d8984ea3205771b5a8edbb49ea4f4411c3b76d7045041df255e1f6b56ddc
|
7
|
+
data.tar.gz: b40978ca2f160e271bbb7495ab26e81378b4e4da3ce7cc0e2a7c84c196d6c0bb3c96a51425d8485ef4a558d66b2629fe3076a4b468977f3ffe4a2eb3aa84ee33
|
@@ -1,8 +1,6 @@
|
|
1
1
|
# Represents an individual feature that may be rolled out to a set of records
|
2
2
|
# via individual flags, percentages, or defined groups.
|
3
3
|
class Detour::Feature < ActiveRecord::Base
|
4
|
-
include Detour::Concerns::Matchers
|
5
|
-
|
6
4
|
self.table_name = :detour_features
|
7
5
|
|
8
6
|
serialize :flag_in_counts, JSON
|
data/app/models/detour/flag.rb
CHANGED
@@ -11,6 +11,17 @@ class Detour::Flag < ActiveRecord::Base
|
|
11
11
|
|
12
12
|
attr_accessible :flaggable_type
|
13
13
|
|
14
|
+
scope :without_opt_outs, ->(record) {
|
15
|
+
where(flaggable_type: record.class.to_s).where <<-SQL
|
16
|
+
feature_id NOT IN (
|
17
|
+
SELECT feature_id FROM detour_flags
|
18
|
+
WHERE detour_flags.type = 'Detour::OptOutFlag'
|
19
|
+
AND detour_flags.flaggable_type = '#{record.class.to_s}'
|
20
|
+
AND detour_flags.flaggable_id = '#{record.id}'
|
21
|
+
)
|
22
|
+
SQL
|
23
|
+
}
|
24
|
+
|
14
25
|
private
|
15
26
|
|
16
27
|
def flag_type
|
data/lib/detour/flaggable.rb
CHANGED
@@ -9,22 +9,18 @@ module Detour::Flaggable
|
|
9
9
|
# @return [Array] An array of {Detour::Feature}s.
|
10
10
|
def features
|
11
11
|
@features ||= begin
|
12
|
-
|
12
|
+
features = unfiltered_features
|
13
|
+
defined_group_flags = Detour::DefinedGroupFlag.without_opt_outs(self).where("feature_id IN (?)", features.map(&:id) << -1) # Prevents NOT IN (NULL)
|
13
14
|
|
14
|
-
|
15
|
-
|
15
|
+
defined_group_flags.each do |defined_group_flag|
|
16
|
+
defined_group = Detour::DefinedGroup.by_type(self.class)[defined_group_flag.group_name]
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
unless defined_group && defined_group.test(self)
|
19
|
+
feeatures.delete defined_group_flag.feature
|
20
|
+
end
|
21
|
+
end
|
19
22
|
|
20
|
-
|
21
|
-
@features.concat defined_group_features.select { |feature| feature.match_defined_groups?(self) }
|
22
|
-
|
23
|
-
percentage_group_features = Detour::Feature.joins(:"#{table_name}_percentage_flag").where("'detour_features'.id NOT IN (?)", opt_out_ids)
|
24
|
-
@features.concat percentage_group_features.select { |feature| feature.match_percentage?(self) }
|
25
|
-
|
26
|
-
flag_in_features = Detour::Feature.joins(:"#{table_name}_flag_ins").where("'detour_features'.id NOT IN (?)", opt_out_ids)
|
27
|
-
@features.concat flag_in_features
|
23
|
+
features
|
28
24
|
end
|
29
25
|
end
|
30
26
|
|
@@ -48,6 +44,50 @@ module Detour::Flaggable
|
|
48
44
|
@detour_features ||= []
|
49
45
|
end
|
50
46
|
|
47
|
+
private
|
48
|
+
|
49
|
+
def unfiltered_features
|
50
|
+
Detour::Feature.where(%Q{
|
51
|
+
detour_features.id IN (
|
52
|
+
-- Get features the record has been individually flagged in to
|
53
|
+
SELECT feature_id FROM detour_flags
|
54
|
+
WHERE detour_flags.type = 'Detour::FlagInFlag'
|
55
|
+
AND detour_flags.flaggable_type = '#{self.class.to_s}'
|
56
|
+
AND detour_flags.flaggable_id = '#{id}'
|
57
|
+
) OR detour_features.id IN (
|
58
|
+
-- Get features the record has been flagged into via group membership
|
59
|
+
SELECT feature_id FROM detour_flags
|
60
|
+
INNER JOIN detour_groups
|
61
|
+
ON detour_groups.id = detour_flags.group_id
|
62
|
+
INNER JOIN detour_memberships
|
63
|
+
ON detour_memberships.group_id = detour_groups.id
|
64
|
+
AND detour_memberships.member_id = '#{id}'
|
65
|
+
WHERE detour_flags.type = 'Detour::DatabaseGroupFlag'
|
66
|
+
AND detour_flags.flaggable_type = '#{self.class}'
|
67
|
+
) OR detour_features.id IN (
|
68
|
+
-- Get features the record has been flagged into via defined membership
|
69
|
+
-- We'll test them later
|
70
|
+
SELECT feature_id FROM detour_flags
|
71
|
+
WHERE detour_flags.type = 'Detour::DefinedGroupFlag'
|
72
|
+
AND detour_flags.flaggable_type = '#{self.class}'
|
73
|
+
) OR detour_features.id IN (
|
74
|
+
-- Get features the record has been flagged into via percentage
|
75
|
+
SELECT feature_id FROM detour_flags
|
76
|
+
WHERE detour_flags.type = 'Detour::PercentageFlag'
|
77
|
+
AND detour_flags.flaggable_type = '#{self.class}'
|
78
|
+
AND '#{id}' % 10 < detour_flags.percentage / 10
|
79
|
+
)
|
80
|
+
}).where(%Q{
|
81
|
+
-- Exclude features the record has been opted out of.
|
82
|
+
detour_features.id NOT IN (
|
83
|
+
SELECT feature_id FROM detour_flags
|
84
|
+
WHERE detour_flags.type = 'Detour::OptOutFlag'
|
85
|
+
AND detour_flags.flaggable_type = '#{self.class}'
|
86
|
+
AND detour_flags.flaggable_id = '#{id}'
|
87
|
+
)
|
88
|
+
})
|
89
|
+
end
|
90
|
+
|
51
91
|
included do
|
52
92
|
# Finds a record by the field set by the :find_by param in
|
53
93
|
# `acts_as_flaggable`. If no :find_by param was provided, :id is used.
|
data/lib/detour/version.rb
CHANGED
@@ -7,7 +7,7 @@ describe "percentage rollouts" do
|
|
7
7
|
|
8
8
|
describe "creating a percentage rollout" do
|
9
9
|
it "makes the feature available to the given percentage of instances" do
|
10
|
-
users.select { |user|
|
10
|
+
users.select { |user| user.features.include?(feature) }.length.should eq users.length / 5
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
@@ -132,84 +132,4 @@ describe Detour::Feature do
|
|
132
132
|
end
|
133
133
|
end
|
134
134
|
end
|
135
|
-
|
136
|
-
describe "#match_percentage?" do
|
137
|
-
let(:user) { create :user }
|
138
|
-
let(:feature) { create :feature }
|
139
|
-
let!(:flag) { create :percentage_flag, feature: feature, flaggable_type: user.class.to_s, percentage: 50 }
|
140
|
-
|
141
|
-
context "when the user's ID matches `id % 10 < percentage / 10" do
|
142
|
-
it "returns true" do
|
143
|
-
user.stub(:id) { 1 }
|
144
|
-
feature.match_percentage?(user).should be_true
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
context "when the user's ID does not match `id % 10 < percentage / 10" do
|
149
|
-
it "returns false" do
|
150
|
-
user.stub(:id) { 5 }
|
151
|
-
feature.match_percentage?(user).should be_false
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
describe "#match_database_group?" do
|
157
|
-
let(:user) { create :user, name: "foo" }
|
158
|
-
let(:user2) { create :user }
|
159
|
-
let(:widget) { create :widget }
|
160
|
-
let(:feature) { create :feature }
|
161
|
-
let(:group) { create :group }
|
162
|
-
let!(:membership) { create :membership, group: group, member: user }
|
163
|
-
let!(:flag) { create :database_group_flag, feature: feature, flaggable_type: user.class.to_s, group: group }
|
164
|
-
|
165
|
-
context "when the instance is in the group" do
|
166
|
-
it "returns true" do
|
167
|
-
feature.match_database_groups?(user).should be_true
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
context "when the instance is not in the group" do
|
172
|
-
it "returns false" do
|
173
|
-
feature.match_database_groups?(user2).should be_false
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
context "when the instance is not of the type of the group" do
|
178
|
-
it "returns false" do
|
179
|
-
feature.match_database_groups?(widget).should be_false
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
describe "#match_defined_groups?" do
|
185
|
-
let(:user) { create :user, name: "foo" }
|
186
|
-
let(:user2) { create :user }
|
187
|
-
let(:widget) { create :widget }
|
188
|
-
let(:feature) { create :feature }
|
189
|
-
let!(:flag) { create :defined_group_flag, feature: feature, flaggable_type: user.class.to_s, group_name: "foo_users" }
|
190
|
-
|
191
|
-
before do
|
192
|
-
Detour.config.define_user_group "foo_users" do |user|
|
193
|
-
user.name == "foo"
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
context "when the instance matches the block" do
|
198
|
-
it "returns true" do
|
199
|
-
feature.match_defined_groups?(user).should be_true
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
context "when the instance does not match the block" do
|
204
|
-
it "returns false" do
|
205
|
-
feature.match_defined_groups?(user2).should be_false
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
context "when the instance is not of the type of the block" do
|
210
|
-
it "returns false" do
|
211
|
-
feature.match_defined_groups?(widget).should be_false
|
212
|
-
end
|
213
|
-
end
|
214
|
-
end
|
215
135
|
end
|
@@ -5,4 +5,25 @@ describe Detour::Flag do
|
|
5
5
|
it { should validate_presence_of :feature }
|
6
6
|
it { should validate_presence_of :flaggable_type }
|
7
7
|
it { should allow_mass_assignment_of :flaggable_type }
|
8
|
+
|
9
|
+
describe ".without_opt_outs" do
|
10
|
+
let(:feature) { create :feature }
|
11
|
+
let(:feature2) { create :feature }
|
12
|
+
let(:user) { create :user }
|
13
|
+
|
14
|
+
before do
|
15
|
+
Detour.config.define_user_group("foo") { true }
|
16
|
+
Detour.config.define_user_group("bar") { true }
|
17
|
+
Detour.config.define_widget_group("baz") { true }
|
18
|
+
|
19
|
+
@flag1 = create :defined_group_flag, feature: feature, flaggable_type: "User", group_name: "foo"
|
20
|
+
@flag2 = create :defined_group_flag, feature: feature2, flaggable_type: "User", group_name: "bar"
|
21
|
+
@flag3 = create :defined_group_flag, feature: feature, flaggable_type: "Widget", group_name: "bar"
|
22
|
+
create :opt_out_flag, feature: feature2, flaggable: user
|
23
|
+
end
|
24
|
+
|
25
|
+
it "returns flags without opt outs" do
|
26
|
+
Detour::DefinedGroupFlag.without_opt_outs(user).should eq [@flag1]
|
27
|
+
end
|
28
|
+
end
|
8
29
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: detour
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Clem
|
@@ -245,7 +245,6 @@ files:
|
|
245
245
|
- app/helpers/detour/flags_helper.rb
|
246
246
|
- app/models/detour/concerns/countable_flag.rb
|
247
247
|
- app/models/detour/concerns/keepable.rb
|
248
|
-
- app/models/detour/concerns/matchers.rb
|
249
248
|
- app/models/detour/database_group_flag.rb
|
250
249
|
- app/models/detour/defined_group.rb
|
251
250
|
- app/models/detour/defined_group_flag.rb
|
@@ -1,59 +0,0 @@
|
|
1
|
-
module Detour::Concerns
|
2
|
-
module Matchers
|
3
|
-
# Determines whether or not the given instance has had the feature rolled out
|
4
|
-
# to it via percentage.
|
5
|
-
#
|
6
|
-
# @example
|
7
|
-
# feature.match_percentage?(current_user)
|
8
|
-
#
|
9
|
-
# @param [ActiveRecord::Base] instance A record to be tested for feature
|
10
|
-
# rollout.
|
11
|
-
#
|
12
|
-
# @return Whether or not the given instance has the feature rolled out to it
|
13
|
-
# via direct percentage.
|
14
|
-
def match_percentage?(instance)
|
15
|
-
flag = percentage_flags.find(:first, conditions: ["flaggable_type = ?", instance.class.to_s])
|
16
|
-
percentage = flag ? flag.percentage : 0
|
17
|
-
|
18
|
-
instance.id % 10 < percentage / 10
|
19
|
-
end
|
20
|
-
|
21
|
-
# Determines whether or not the given instance has had the feature rolled out
|
22
|
-
# to it via database group membership.
|
23
|
-
#
|
24
|
-
# @example
|
25
|
-
# feature.match_database_groups?(current_user)
|
26
|
-
#
|
27
|
-
# @param [ActiveRecord::Base] instance A record to be tested for feature
|
28
|
-
# rollout.
|
29
|
-
#
|
30
|
-
# @return Whether or not the given instance has the feature rolled out to it
|
31
|
-
# via direct database group membership.
|
32
|
-
def match_database_groups?(instance)
|
33
|
-
database_group_flags.where(flaggable_type: instance.class).map(&:members).flatten.uniq.include? instance
|
34
|
-
end
|
35
|
-
|
36
|
-
# Determines whether or not the given instance has had the feature rolled out
|
37
|
-
# to it via defined group membership.
|
38
|
-
#
|
39
|
-
# @example
|
40
|
-
# feature.match_defined_groups?(current_user)
|
41
|
-
#
|
42
|
-
# @param [ActiveRecord::Base] instance A record to be tested for feature
|
43
|
-
# rollout.
|
44
|
-
#
|
45
|
-
# @return Whether or not the given instance has the feature rolled out to it
|
46
|
-
# via direct group membership.
|
47
|
-
def match_defined_groups?(instance)
|
48
|
-
klass = instance.class.to_s
|
49
|
-
|
50
|
-
return unless Detour::DefinedGroup.by_type(klass).any?
|
51
|
-
|
52
|
-
group_names = defined_group_flags.find_all_by_flaggable_type(klass).collect(&:group_name)
|
53
|
-
|
54
|
-
Detour::DefinedGroup.by_type(klass).collect { |name, group|
|
55
|
-
group.test(instance) if group_names.include? group.name
|
56
|
-
}.any?
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|