feature_creep 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.
- data/.gitignore +24 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +48 -0
- data/License +21 -0
- data/README.md +25 -0
- data/feature_creep.gemspec +32 -0
- data/lib/feature_creep.rb +131 -0
- data/lib/feature_creep/redis_datastore.rb +133 -0
- data/lib/feature_creep/version.rb +3 -0
- data/spec/feature_creep_spec.rb +222 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +12 -0
- metadata +172 -0
data/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
coverage
|
6
|
+
InstalledFiles
|
7
|
+
lib/bundler/man
|
8
|
+
pkg
|
9
|
+
rdoc
|
10
|
+
spec/reports
|
11
|
+
test/tmp
|
12
|
+
test/version_tmp
|
13
|
+
tmp
|
14
|
+
|
15
|
+
# YARD artifacts
|
16
|
+
.yardoc
|
17
|
+
_yardoc
|
18
|
+
doc/
|
19
|
+
|
20
|
+
*~
|
21
|
+
\#*
|
22
|
+
.\#*
|
23
|
+
|
24
|
+
.DS_Store
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
feature_creep (0.0.1)
|
5
|
+
redis
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
bourne (1.0)
|
11
|
+
mocha (= 0.9.8)
|
12
|
+
coderay (1.0.7)
|
13
|
+
diff-lcs (1.1.3)
|
14
|
+
git (1.2.5)
|
15
|
+
jeweler (1.6.4)
|
16
|
+
bundler (~> 1.0)
|
17
|
+
git (>= 1.2.5)
|
18
|
+
rake
|
19
|
+
method_source (0.8)
|
20
|
+
mocha (0.9.8)
|
21
|
+
rake
|
22
|
+
pry (0.9.10)
|
23
|
+
coderay (~> 1.0.5)
|
24
|
+
method_source (~> 0.8)
|
25
|
+
slop (~> 3.3.1)
|
26
|
+
rake (0.9.2.2)
|
27
|
+
redis (3.0.2)
|
28
|
+
rspec (2.10.0)
|
29
|
+
rspec-core (~> 2.10.0)
|
30
|
+
rspec-expectations (~> 2.10.0)
|
31
|
+
rspec-mocks (~> 2.10.0)
|
32
|
+
rspec-core (2.10.1)
|
33
|
+
rspec-expectations (2.10.0)
|
34
|
+
diff-lcs (~> 1.1.3)
|
35
|
+
rspec-mocks (2.10.1)
|
36
|
+
slop (3.3.3)
|
37
|
+
|
38
|
+
PLATFORMS
|
39
|
+
ruby
|
40
|
+
|
41
|
+
DEPENDENCIES
|
42
|
+
bourne (= 1.0)
|
43
|
+
bundler (>= 1.0.0)
|
44
|
+
feature_creep!
|
45
|
+
jeweler (~> 1.6.4)
|
46
|
+
mocha (= 0.9.8)
|
47
|
+
pry (= 0.9.10)
|
48
|
+
rspec (~> 2.10.0)
|
data/License
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2009 James Golick
|
2
|
+
Copyright (c) 2012 Christian Trosclair
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
a copy of this software and associated documentation files (the
|
6
|
+
"Software"), to deal in the Software without restriction, including
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
feature_creep
|
2
|
+
=============
|
3
|
+
|
4
|
+
I've got your feature flags implementation right here.
|
5
|
+
|
6
|
+
This is a fork/rewrite of James Golick's Rollout with messy and incomplete docs.
|
7
|
+
|
8
|
+
So far, the specs are green and are mostly inherited from Rollout.
|
9
|
+
This is not a drop in replacement. The API has aleadey changed.
|
10
|
+
|
11
|
+
Namely, `groups` are now `scopes` and `users` are `agent_ids`.
|
12
|
+
Agent_ids are expected to be uuids, not the object itself.
|
13
|
+
|
14
|
+
The class constructor now takes a datastore and an options hash.
|
15
|
+
|
16
|
+
options[:info] should be a lambda that returns a hash when called
|
17
|
+
|
18
|
+
options[:warden] is also a lambda that encapsulates the business logic for FeatureCreep#active?
|
19
|
+
|
20
|
+
options[:scopes] is a hash that expects a string key and a lambda that encapsulates the business logic for membership in a scope
|
21
|
+
|
22
|
+
|
23
|
+
At this point it should be easy to extend.
|
24
|
+
I would expect the API to stablize over the next couple of weeks.
|
25
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "feature_creep/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "feature_creep"
|
7
|
+
s.version = FeatureCreep::VERSION
|
8
|
+
s.authors = ["Christian Trosclair"]
|
9
|
+
s.email = ["xn@gotham.us"]
|
10
|
+
s.description = "Feature Flag implementation"
|
11
|
+
s.summary = "Extensible Feature Flags"
|
12
|
+
s.homepage = "https://github.com/xn/feature_creep"
|
13
|
+
|
14
|
+
s.require_paths = ["lib"]
|
15
|
+
|
16
|
+
s.rubyforge_project = "feature_creep"
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
|
23
|
+
# specify any dependencies here; for example:
|
24
|
+
s.add_development_dependency "rspec", "~> 2.10.0"
|
25
|
+
s.add_development_dependency "bundler", ">= 1.0.0"
|
26
|
+
s.add_development_dependency "jeweler", "~> 1.6.4"
|
27
|
+
s.add_development_dependency "bourne", "1.0"
|
28
|
+
s.add_development_dependency "mocha", "0.9.8"
|
29
|
+
s.add_development_dependency "pry", "0.9.10"
|
30
|
+
|
31
|
+
s.add_runtime_dependency "redis"
|
32
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
class FeatureCreep
|
2
|
+
|
3
|
+
attr_accessor :scopes, :info, :warden
|
4
|
+
|
5
|
+
def initialize(datastore, options = {})
|
6
|
+
@datastore = datastore
|
7
|
+
@scopes = {"all" => lambda { |agent_id| true }}
|
8
|
+
if options.has_key?(:scopes)
|
9
|
+
options[:scopes].each do |name,membership_block|
|
10
|
+
@scopes[name.to_s] = membership_block
|
11
|
+
end
|
12
|
+
end
|
13
|
+
@warden = if options.has_key?(:warden)
|
14
|
+
options[:warden]
|
15
|
+
else
|
16
|
+
lambda { |feature,agent_id|
|
17
|
+
if agent_id
|
18
|
+
active_globally?(feature) ||
|
19
|
+
agent_id_in_active_scope?(feature, agent_id) ||
|
20
|
+
agent_id_active?(feature, agent_id) ||
|
21
|
+
agent_id_within_active_percentage?(feature, agent_id)
|
22
|
+
else
|
23
|
+
active_globally?(feature)
|
24
|
+
end
|
25
|
+
}
|
26
|
+
end
|
27
|
+
@info = if options.has_key?(:info)
|
28
|
+
options[:info]
|
29
|
+
else lambda { |feature|
|
30
|
+
if feature
|
31
|
+
{
|
32
|
+
:percentage => (active_percentage(feature) || 0).to_i,
|
33
|
+
:scopes => active_scopes(feature).map { |g| g.to_sym },
|
34
|
+
:agent_ids => active_agent_id_ids(feature),
|
35
|
+
:global => active_global_features
|
36
|
+
}
|
37
|
+
else
|
38
|
+
{
|
39
|
+
:global => active_global_features
|
40
|
+
}
|
41
|
+
end
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def activate_globally(feature)
|
47
|
+
@datastore.activate_globally(feature)
|
48
|
+
end
|
49
|
+
|
50
|
+
def deactivate_globally(feature)
|
51
|
+
@datastore.deactivate_globally(feature)
|
52
|
+
end
|
53
|
+
|
54
|
+
def activate_scope(feature, scope)
|
55
|
+
@datastore.activate_scope(feature, scope)
|
56
|
+
end
|
57
|
+
|
58
|
+
def deactivate_scope(feature, scope)
|
59
|
+
@datastore.deactivate_scope(feature, scope)
|
60
|
+
end
|
61
|
+
|
62
|
+
def deactivate_all(feature)
|
63
|
+
@datastore.deactivate_all(feature)
|
64
|
+
end
|
65
|
+
|
66
|
+
def activate_agent_id(feature, agent_id)
|
67
|
+
@datastore.activate_agent_id(feature, agent_id)
|
68
|
+
end
|
69
|
+
|
70
|
+
def deactivate_agent_id(feature, agent_id)
|
71
|
+
@datastore.deactivate_agent_id(feature, agent_id)
|
72
|
+
end
|
73
|
+
|
74
|
+
def active?(feature, agent_id = nil)
|
75
|
+
@warden.call(feature,agent_id)
|
76
|
+
end
|
77
|
+
|
78
|
+
def activate_percentage(feature, percentage)
|
79
|
+
@datastore.activate_percentage(feature, percentage)
|
80
|
+
end
|
81
|
+
|
82
|
+
def deactivate_percentage(feature)
|
83
|
+
@datastore.deactivate_percentage(feature)
|
84
|
+
end
|
85
|
+
|
86
|
+
def current_features
|
87
|
+
@datastore.current_features
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_feature(feature)
|
91
|
+
@datastore.add_feature(feature)
|
92
|
+
end
|
93
|
+
|
94
|
+
def info(feature = nil)
|
95
|
+
@info.call(feature)
|
96
|
+
end
|
97
|
+
|
98
|
+
def active_scopes(feature)
|
99
|
+
@datastore.active_scopes(feature)
|
100
|
+
end
|
101
|
+
|
102
|
+
def active_agent_id_ids(feature)
|
103
|
+
@datastore.active_agent_id_ids(feature)
|
104
|
+
end
|
105
|
+
|
106
|
+
def active_global_features
|
107
|
+
@datastore.active_global_features
|
108
|
+
end
|
109
|
+
|
110
|
+
def active_percentage(feature)
|
111
|
+
@datastore.active_percentage(feature)
|
112
|
+
end
|
113
|
+
|
114
|
+
def active_globally?(feature)
|
115
|
+
@datastore.active_globally?(feature)
|
116
|
+
end
|
117
|
+
|
118
|
+
def agent_id_in_active_scope?(feature, agent_id)
|
119
|
+
active_scopes(feature).any? do |scope|
|
120
|
+
@scopes.key?(scope) && @scopes[scope].call(agent_id)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def agent_id_active?(feature, agent_id)
|
125
|
+
@datastore.agent_id_active?(feature, agent_id)
|
126
|
+
end
|
127
|
+
|
128
|
+
def agent_id_within_active_percentage?(feature, agent_id)
|
129
|
+
@datastore.agent_id_within_active_percentage?(feature, agent_id)
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
class FeatureCreep
|
2
|
+
class RedisDataStore
|
3
|
+
|
4
|
+
def initialize(datastore = nil, key_prefix = "feature_creep")
|
5
|
+
@key_prefix = key_prefix
|
6
|
+
@redis = datastore || Redis.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def activate_globally(feature)
|
10
|
+
@redis.sadd(global_key, feature)
|
11
|
+
end
|
12
|
+
|
13
|
+
def deactivate_globally(feature)
|
14
|
+
@redis.srem(global_key, feature)
|
15
|
+
end
|
16
|
+
|
17
|
+
def activate_scope(feature, scope)
|
18
|
+
@redis.sadd(scope_key(feature), scope)
|
19
|
+
end
|
20
|
+
|
21
|
+
def deactivate_scope(feature, scope)
|
22
|
+
@redis.srem(scope_key(feature), scope)
|
23
|
+
end
|
24
|
+
|
25
|
+
def deactivate_all(feature)
|
26
|
+
@redis.del(scope_key(feature))
|
27
|
+
@redis.del(agent_id_key(feature))
|
28
|
+
@redis.del(percentage_key(feature))
|
29
|
+
deactivate_globally(feature)
|
30
|
+
end
|
31
|
+
|
32
|
+
def activate_agent_id(feature, agent_id)
|
33
|
+
@redis.sadd(agent_id_key(feature), agent_id)
|
34
|
+
end
|
35
|
+
|
36
|
+
def deactivate_agent_id(feature, agent_id)
|
37
|
+
@redis.srem(agent_id_key(feature), agent_id)
|
38
|
+
end
|
39
|
+
|
40
|
+
def active?(feature, agent_id = nil)
|
41
|
+
if agent_id
|
42
|
+
active_globally?(feature) ||
|
43
|
+
agent_id_in_active_scope?(feature, agent_id) ||
|
44
|
+
agent_id_active?(feature, agent_id) ||
|
45
|
+
agent_id_within_active_percentage?(feature, agent_id)
|
46
|
+
else
|
47
|
+
active_globally?(feature)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def activate_percentage(feature, percentage)
|
52
|
+
@redis.set(percentage_key(feature), percentage)
|
53
|
+
end
|
54
|
+
|
55
|
+
def deactivate_percentage(feature)
|
56
|
+
@redis.del(percentage_key(feature))
|
57
|
+
end
|
58
|
+
|
59
|
+
def info(feature = nil)
|
60
|
+
if feature
|
61
|
+
{
|
62
|
+
:percentage => (active_percentage(feature) || 0).to_i,
|
63
|
+
:scopes => active_scopes(feature).map { |g| g.to_sym },
|
64
|
+
:agent_ids => active_agent_id_ids(feature),
|
65
|
+
:global => active_global_features
|
66
|
+
}
|
67
|
+
else
|
68
|
+
{
|
69
|
+
:features => current_features
|
70
|
+
}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def active_scopes(feature)
|
75
|
+
@redis.smembers(scope_key(feature)) || []
|
76
|
+
end
|
77
|
+
|
78
|
+
def active_agent_id_ids(feature)
|
79
|
+
@redis.smembers(agent_id_key(feature)).map { |id| id.to_i }
|
80
|
+
end
|
81
|
+
|
82
|
+
def active_global_features
|
83
|
+
(@redis.smembers(global_key) || []).map(&:to_sym)
|
84
|
+
end
|
85
|
+
|
86
|
+
def active_percentage(feature)
|
87
|
+
@redis.get(percentage_key(feature))
|
88
|
+
end
|
89
|
+
|
90
|
+
def active_globally?(feature)
|
91
|
+
@redis.sismember(global_key, feature)
|
92
|
+
end
|
93
|
+
|
94
|
+
def agent_id_active?(feature, agent_id)
|
95
|
+
@redis.sismember(agent_id_key(feature), agent_id)
|
96
|
+
end
|
97
|
+
|
98
|
+
def agent_id_within_active_percentage?(feature, agent_id)
|
99
|
+
percentage = active_percentage(feature)
|
100
|
+
return false if percentage.nil?
|
101
|
+
agent_id % 100 < percentage.to_i
|
102
|
+
end
|
103
|
+
|
104
|
+
def current_features
|
105
|
+
@redis.smembers(@key_prefix)
|
106
|
+
end
|
107
|
+
|
108
|
+
def add_feature(feature)
|
109
|
+
redis.sadd(@key_prefix, feature)
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
def key(name)
|
114
|
+
"#{@key_prefix}:#{name}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def scope_key(name)
|
118
|
+
"#{key(name)}:scopes"
|
119
|
+
end
|
120
|
+
|
121
|
+
def agent_id_key(name)
|
122
|
+
"#{key(name)}:agent_ids"
|
123
|
+
end
|
124
|
+
|
125
|
+
def percentage_key(name)
|
126
|
+
"#{key(name)}:percentage"
|
127
|
+
end
|
128
|
+
|
129
|
+
def global_key
|
130
|
+
"#{@key_prefix}:__global__"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "FeatureCreep" do
|
4
|
+
before do
|
5
|
+
@datastore = FeatureCreep::RedisDataStore.new
|
6
|
+
scopes = {
|
7
|
+
:fivesonly => lambda { |agent_id| agent_id == 5 }
|
8
|
+
}
|
9
|
+
@feature_creep = FeatureCreep.new(@datastore, {:scopes => scopes})
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "when a scope is activated" do
|
13
|
+
before do
|
14
|
+
@feature_creep.activate_scope(:chat, :fivesonly)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "the feature is active for agent_ids for which the block evaluates to true" do
|
18
|
+
@feature_creep.should be_active(:chat, 5)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "is not active for agent_ids for which the block evaluates to false" do
|
22
|
+
@feature_creep.should_not be_active(:chat, 1)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "is not active if a scope is found in Redis but not defined in FeatureCreep" do
|
26
|
+
@feature_creep.activate_scope(:chat, :fake)
|
27
|
+
@feature_creep.should_not be_active(:chat, 1)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "the default all scope" do
|
32
|
+
before do
|
33
|
+
@feature_creep.activate_scope(:chat, :all)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "evaluates to true no matter what" do
|
37
|
+
@feature_creep.should be_active(:chat, 0)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "deactivating a scope" do
|
42
|
+
before do
|
43
|
+
@feature_creep.activate_scope(:chat, :all)
|
44
|
+
@feature_creep.activate_scope(:chat, :fivesonly)
|
45
|
+
@feature_creep.deactivate_scope(:chat, :all)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "deactivates the rules for that scope" do
|
49
|
+
@feature_creep.should_not be_active(:chat, 10)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "leaves the other scopes active" do
|
53
|
+
@feature_creep.should be_active(:chat, 5)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "deactivating a feature completely" do
|
58
|
+
before do
|
59
|
+
@feature_creep.activate_scope(:chat, :all)
|
60
|
+
@feature_creep.activate_scope(:chat, :fivesonly)
|
61
|
+
@feature_creep.activate_agent_id(:chat, 51)
|
62
|
+
@feature_creep.activate_percentage(:chat, 100)
|
63
|
+
@feature_creep.activate_globally(:chat)
|
64
|
+
@feature_creep.deactivate_all(:chat)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "removes all of the scopes" do
|
68
|
+
@feature_creep.should_not be_active(:chat, 0)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "removes all of the agent_ids" do
|
72
|
+
@feature_creep.should_not be_active(:chat, 51)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "removes the percentage" do
|
76
|
+
@feature_creep.should_not be_active(:chat, 24)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "removes globally" do
|
80
|
+
@feature_creep.should_not be_active(:chat)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "activating a specific agent_id" do
|
85
|
+
before do
|
86
|
+
@feature_creep.activate_agent_id(:chat, 42)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "is active for that agent_id" do
|
90
|
+
@feature_creep.should be_active(:chat, 42)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "remains inactive for other agent_ids" do
|
94
|
+
@feature_creep.should_not be_active(:chat, 24)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "deactivating a specific agent_id" do
|
99
|
+
before do
|
100
|
+
@feature_creep.activate_agent_id(:chat, 42)
|
101
|
+
@feature_creep.activate_agent_id(:chat, 24)
|
102
|
+
@feature_creep.deactivate_agent_id(:chat, 42)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "that agent_id should no longer be active" do
|
106
|
+
@feature_creep.should_not be_active(:chat, 42)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "remains active for other active agent_ids" do
|
110
|
+
@feature_creep.should be_active(:chat, 24)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe "activating a feature globally" do
|
115
|
+
before do
|
116
|
+
@feature_creep.activate_globally(:chat)
|
117
|
+
end
|
118
|
+
|
119
|
+
it "activates the feature" do
|
120
|
+
@feature_creep.should be_active(:chat)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "activating a feature for a percentage of agent_ids" do
|
125
|
+
before do
|
126
|
+
@feature_creep.activate_percentage(:chat, 20)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "activates the feature for that percentage of the agent_ids" do
|
130
|
+
(1..120).select { |id| @feature_creep.active?(:chat, id) }.length.should == 39
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe "activating a feature for a percentage of agent_ids" do
|
135
|
+
before do
|
136
|
+
@feature_creep.activate_percentage(:chat, 20)
|
137
|
+
end
|
138
|
+
|
139
|
+
it "activates the feature for that percentage of the agent_ids" do
|
140
|
+
(1..200).select { |id| @feature_creep.active?(:chat, id) }.length.should == 40
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "activating a feature for a percentage of agent_ids" do
|
145
|
+
before do
|
146
|
+
@feature_creep.activate_percentage(:chat, 5)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "activates the feature for that percentage of the agent_ids" do
|
150
|
+
(1..100).select { |id| @feature_creep.active?(:chat, id) }.length.should == 5
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
describe "deactivating the percentage of agent_ids" do
|
156
|
+
before do
|
157
|
+
@feature_creep.activate_percentage(:chat, 100)
|
158
|
+
@feature_creep.deactivate_percentage(:chat)
|
159
|
+
end
|
160
|
+
|
161
|
+
it "becomes inactivate for all agent_ids" do
|
162
|
+
@feature_creep.should_not be_active(:chat, 24)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
describe "deactivating the feature globally" do
|
167
|
+
before do
|
168
|
+
@feature_creep.activate_globally(:chat)
|
169
|
+
@feature_creep.deactivate_globally(:chat)
|
170
|
+
end
|
171
|
+
|
172
|
+
it "becomes inactivate" do
|
173
|
+
@feature_creep.should_not be_active(:chat)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe "#info" do
|
178
|
+
context "global features" do
|
179
|
+
let(:features) { [:signup, :chat, :table] }
|
180
|
+
|
181
|
+
before do
|
182
|
+
features.each do |f|
|
183
|
+
@feature_creep.activate_globally(f)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
it "returns all global features" do
|
188
|
+
@feature_creep.info.should eq({ :global => features.reverse })
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe "with a percentage set" do
|
193
|
+
before do
|
194
|
+
@feature_creep.activate_percentage(:chat, 10)
|
195
|
+
@feature_creep.activate_scope(:chat, :caretakers)
|
196
|
+
@feature_creep.activate_scope(:chat, :greeters)
|
197
|
+
@feature_creep.activate_globally(:signup)
|
198
|
+
@feature_creep.activate_agent_id(:chat, 42)
|
199
|
+
end
|
200
|
+
|
201
|
+
it "returns info about all the activations" do
|
202
|
+
@feature_creep.info(:chat).should == {
|
203
|
+
:percentage => 10,
|
204
|
+
:scopes => [:greeters, :caretakers],
|
205
|
+
:agent_ids => [42],
|
206
|
+
:global => [:signup]
|
207
|
+
}
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
describe "without a percentage set" do
|
212
|
+
it "defaults to 0" do
|
213
|
+
@feature_creep.info(:chat).should == {
|
214
|
+
:percentage => 0,
|
215
|
+
:scopes => [],
|
216
|
+
:agent_ids => [],
|
217
|
+
:global => []
|
218
|
+
}
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'feature_creep'
|
4
|
+
require 'feature_creep/redis_datastore'
|
5
|
+
require 'rspec'
|
6
|
+
require 'bourne'
|
7
|
+
require 'redis'
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.mock_with :mocha
|
11
|
+
config.before { Redis.new.flushdb }
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: feature_creep
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Christian Trosclair
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-10-28 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.10.0
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.10.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bundler
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.0.0
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.0.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: jeweler
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.6.4
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.6.4
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: bourne
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - '='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '1.0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - '='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '1.0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: mocha
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - '='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 0.9.8
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - '='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.9.8
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: pry
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - '='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 0.9.10
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - '='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 0.9.10
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: redis
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
description: Feature Flag implementation
|
127
|
+
email:
|
128
|
+
- xn@gotham.us
|
129
|
+
executables: []
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- .gitignore
|
134
|
+
- Gemfile
|
135
|
+
- Gemfile.lock
|
136
|
+
- License
|
137
|
+
- README.md
|
138
|
+
- feature_creep.gemspec
|
139
|
+
- lib/feature_creep.rb
|
140
|
+
- lib/feature_creep/redis_datastore.rb
|
141
|
+
- lib/feature_creep/version.rb
|
142
|
+
- spec/feature_creep_spec.rb
|
143
|
+
- spec/spec.opts
|
144
|
+
- spec/spec_helper.rb
|
145
|
+
homepage: https://github.com/xn/feature_creep
|
146
|
+
licenses: []
|
147
|
+
post_install_message:
|
148
|
+
rdoc_options: []
|
149
|
+
require_paths:
|
150
|
+
- lib
|
151
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
152
|
+
none: false
|
153
|
+
requirements:
|
154
|
+
- - ! '>='
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: '0'
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
|
+
none: false
|
159
|
+
requirements:
|
160
|
+
- - ! '>='
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
requirements: []
|
164
|
+
rubyforge_project: feature_creep
|
165
|
+
rubygems_version: 1.8.24
|
166
|
+
signing_key:
|
167
|
+
specification_version: 3
|
168
|
+
summary: Extensible Feature Flags
|
169
|
+
test_files:
|
170
|
+
- spec/feature_creep_spec.rb
|
171
|
+
- spec/spec.opts
|
172
|
+
- spec/spec_helper.rb
|