detour 0.0.11 → 0.0.12
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.
- 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
|