feature_flipper 1.0.0 → 1.1.0
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/README.md +53 -5
- data/examples/dynamic_states.rb +47 -0
- data/examples/features.rb +9 -4
- data/examples/{simple.rb → static_states.rb} +4 -0
- data/lib/feature_flipper/config.rb +27 -23
- data/lib/feature_flipper/version.rb +1 -1
- data/test/feature_flipper_test.rb +43 -23
- data/test/features.rb +14 -10
- data/test/features_dsl.rb +4 -4
- metadata +14 -6
data/README.md
CHANGED
@@ -39,7 +39,7 @@ more detailed description, a ticket number, a date when it was started, etc.
|
|
39
39
|
Features are always defined in a state, you cannot define a feature which
|
40
40
|
doesn't belong to a state.
|
41
41
|
|
42
|
-
|
42
|
+
in_state :dev do
|
43
43
|
feature :rating_game, :description => 'play a game to get recommendations'
|
44
44
|
end
|
45
45
|
|
@@ -68,11 +68,11 @@ Example config file
|
|
68
68
|
-------------------
|
69
69
|
|
70
70
|
FeatureFlipper.features do
|
71
|
-
|
71
|
+
in_state :dev do
|
72
72
|
feature :rating_game, :description => 'play a game to get recommendations'
|
73
73
|
end
|
74
74
|
|
75
|
-
|
75
|
+
in_state :live do
|
76
76
|
feature :city_feed, :description => 'stream of content for each city'
|
77
77
|
end
|
78
78
|
end
|
@@ -84,14 +84,15 @@ Example config file
|
|
84
84
|
|
85
85
|
This is your complete features.rb config file. In the example there are two
|
86
86
|
states: `:dev` is active on development boxes and `:live` is always active
|
87
|
-
(this is the last state a feature
|
87
|
+
(this is the last state a feature goes through).
|
88
88
|
|
89
89
|
The feature `:rating_game` is still in development and not shown on the
|
90
90
|
production site. The feature `:city_feed` is done and already enabled
|
91
91
|
everywhere. You transition features between states by just moving the line to
|
92
92
|
the new state block.
|
93
93
|
|
94
|
-
You can take a look at
|
94
|
+
You can take a look at the `static_states.rb` in the 'examples' folder to
|
95
|
+
see this in detail.
|
95
96
|
|
96
97
|
Cleaning up
|
97
98
|
-----------
|
@@ -100,6 +101,53 @@ The drawback of this approach is that your code can get quite ugly with all
|
|
100
101
|
these if/else branches. So you have to be strict about removing (we call it
|
101
102
|
de-featurizing) features after they have gone live.
|
102
103
|
|
104
|
+
Dynamic feature groups
|
105
|
+
----------------------
|
106
|
+
|
107
|
+
As soon as we have the feature_flipper infrastructure in place, we can start
|
108
|
+
doing more interesting things with it. For example, dynamic features which
|
109
|
+
are enabled on a per user basis. This allows you to release features to
|
110
|
+
employees only, to a private beta group, etc.
|
111
|
+
|
112
|
+
### Defining dynamic states
|
113
|
+
|
114
|
+
A dynamic state is defined a bit different than a normal, static state.
|
115
|
+
|
116
|
+
FeatureFlipper::Config.states = {
|
117
|
+
:dev => ['development', 'test'].include?(Rails.env),
|
118
|
+
:employees => { :required_state => :dev, :feature_group => :employees }
|
119
|
+
}
|
120
|
+
|
121
|
+
It has a required state and a feature group. The feature group defines
|
122
|
+
the name of the group of users which should see this feature. The required
|
123
|
+
state is the state that gets looked at for all other users that aren't in
|
124
|
+
the feature group. The required_state must also be defined as a separate state.
|
125
|
+
|
126
|
+
### Setting the feature group
|
127
|
+
|
128
|
+
The current feature group is set globally and is active for the whole thread.
|
129
|
+
In Rails you would define a before_filter like this:
|
130
|
+
|
131
|
+
class ApplicationController < ActionController::Base
|
132
|
+
before_filter :set_current_feature_group
|
133
|
+
|
134
|
+
def set_current_feature_group
|
135
|
+
# we need to reset the feature group in each request,
|
136
|
+
# otherwise it persists (which is not want we want).
|
137
|
+
FeatureFlipper.reset_current_feature_groups
|
138
|
+
|
139
|
+
if logged_in? && current_user.employee?
|
140
|
+
FeatureFlipper.current_feature_groups << :employees
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
It's really important to reset the feature group, otherwise it's not dynamic.
|
145
|
+
The condition if someone is in a feature group can be anything: You can
|
146
|
+
store it in the database, in Redis, look at request parameters, etc.
|
147
|
+
|
148
|
+
Take a look at `dynamic_states.rb` in the examples folder to see this
|
149
|
+
in detail.
|
150
|
+
|
103
151
|
Meta
|
104
152
|
----
|
105
153
|
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# Setup
|
2
|
+
#
|
3
|
+
|
4
|
+
# just need this to make it work from within the library
|
5
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
6
|
+
|
7
|
+
# fake production Rails environment
|
8
|
+
module Rails
|
9
|
+
def self.env
|
10
|
+
'production'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
# Configuration
|
16
|
+
#
|
17
|
+
|
18
|
+
require 'feature_flipper'
|
19
|
+
|
20
|
+
# set the path to your app specific config file
|
21
|
+
FeatureFlipper::Config.path_to_file = "features.rb"
|
22
|
+
|
23
|
+
|
24
|
+
# Usage
|
25
|
+
#
|
26
|
+
|
27
|
+
puts "=== first example:"
|
28
|
+
|
29
|
+
# no current_feature_group set, so the required_state of badges is looked at
|
30
|
+
if show_feature?(:badges)
|
31
|
+
puts "shiny new badges not live on prod yet"
|
32
|
+
else
|
33
|
+
puts "no new badges"
|
34
|
+
end
|
35
|
+
|
36
|
+
puts "\n=== second example:"
|
37
|
+
|
38
|
+
# now we set the current_feature_group. Usually depending on the logged in user
|
39
|
+
|
40
|
+
FeatureFlipper.reset_current_feature_groups
|
41
|
+
FeatureFlipper.current_feature_groups << :employees
|
42
|
+
|
43
|
+
if show_feature?(:badges)
|
44
|
+
puts "shiny new badges for this user"
|
45
|
+
else
|
46
|
+
puts "no new badges"
|
47
|
+
end
|
data/examples/features.rb
CHANGED
@@ -4,16 +4,21 @@
|
|
4
4
|
#
|
5
5
|
|
6
6
|
FeatureFlipper.features do
|
7
|
-
|
7
|
+
in_state :dev do
|
8
8
|
feature :rating_game, :description => 'play a game to get recommendations'
|
9
9
|
end
|
10
10
|
|
11
|
-
|
11
|
+
in_state :live do
|
12
12
|
feature :city_feed, :description => 'stream of content for each city'
|
13
13
|
end
|
14
|
+
|
15
|
+
in_state :employees do
|
16
|
+
feature :badges, :description => 'new badges'
|
17
|
+
end
|
14
18
|
end
|
15
19
|
|
16
20
|
FeatureFlipper::Config.states = {
|
17
|
-
:dev
|
18
|
-
:
|
21
|
+
:dev => ['development', 'test'].include?(Rails.env),
|
22
|
+
:employees => { :required_state => :dev, :feature_group => :employees },
|
23
|
+
:live => true
|
19
24
|
}
|
@@ -24,6 +24,8 @@ FeatureFlipper::Config.path_to_file = "features.rb"
|
|
24
24
|
# Usage
|
25
25
|
#
|
26
26
|
|
27
|
+
puts "=== first example:"
|
28
|
+
|
27
29
|
# rating_game is still in development, so shouldn't be shown on production
|
28
30
|
if show_feature?(:rating_game)
|
29
31
|
puts "Rating Game"
|
@@ -31,6 +33,8 @@ else
|
|
31
33
|
puts "old stuff"
|
32
34
|
end
|
33
35
|
|
36
|
+
puts "\n=== second example:"
|
37
|
+
|
34
38
|
# city_feed is enabled everywhere
|
35
39
|
if show_feature?(:city_feed)
|
36
40
|
puts "City Feed"
|
@@ -38,16 +38,20 @@ module FeatureFlipper
|
|
38
38
|
@states = states
|
39
39
|
end
|
40
40
|
|
41
|
-
def self.
|
41
|
+
def self.get_state(feature_name)
|
42
42
|
feature = features[feature_name]
|
43
|
-
feature ? feature[:
|
43
|
+
feature ? feature[:state] : nil
|
44
44
|
end
|
45
45
|
|
46
|
-
def self.
|
47
|
-
active = states[
|
46
|
+
def self.active_state?(state)
|
47
|
+
active = states[state]
|
48
48
|
if active.is_a?(Hash)
|
49
|
-
|
50
|
-
|
49
|
+
if active.has_key?(:feature_group)
|
50
|
+
group, required_state = active[:feature_group], active[:required_state]
|
51
|
+
else
|
52
|
+
group, required_state = active.to_a.flatten
|
53
|
+
end
|
54
|
+
(FeatureFlipper.current_feature_groups.include?(group)) || (states[required_state] == true)
|
51
55
|
else
|
52
56
|
active == true
|
53
57
|
end
|
@@ -56,42 +60,42 @@ module FeatureFlipper
|
|
56
60
|
def self.is_active?(feature_name)
|
57
61
|
ensure_config_is_loaded
|
58
62
|
|
59
|
-
|
60
|
-
if
|
61
|
-
|
62
|
-
elsif
|
63
|
-
|
63
|
+
state = get_state(feature_name)
|
64
|
+
if state.is_a?(Symbol)
|
65
|
+
active_state?(state)
|
66
|
+
elsif state.is_a?(Proc)
|
67
|
+
state.call == true
|
64
68
|
else
|
65
|
-
|
69
|
+
state == true
|
66
70
|
end
|
67
71
|
end
|
68
72
|
end
|
69
73
|
|
70
74
|
class Mapper
|
71
|
-
def initialize(
|
72
|
-
@
|
75
|
+
def initialize(state)
|
76
|
+
@state = state
|
73
77
|
end
|
74
78
|
|
75
79
|
def feature(name, options = {})
|
76
|
-
FeatureFlipper::Config.features[name] = options.merge(:
|
80
|
+
FeatureFlipper::Config.features[name] = options.merge(:state => @state)
|
77
81
|
end
|
78
82
|
end
|
79
83
|
|
80
|
-
class
|
81
|
-
def
|
82
|
-
Mapper.new(
|
84
|
+
class StateMapper
|
85
|
+
def in_state(state, &block)
|
86
|
+
Mapper.new(state).instance_eval(&block)
|
83
87
|
end
|
84
88
|
end
|
85
89
|
|
86
90
|
def self.features(&block)
|
87
|
-
|
91
|
+
StateMapper.new.instance_eval(&block)
|
88
92
|
end
|
89
93
|
|
90
|
-
def self.
|
91
|
-
Thread.current[:
|
94
|
+
def self.current_feature_groups
|
95
|
+
Thread.current[:feature_system_current_feature_groups] ||= []
|
92
96
|
end
|
93
97
|
|
94
|
-
def self.
|
95
|
-
|
98
|
+
def self.reset_current_feature_groups
|
99
|
+
current_feature_groups.clear
|
96
100
|
end
|
97
101
|
end
|
@@ -27,34 +27,13 @@ context 'hash based FeatureFlipper' do
|
|
27
27
|
assert show_feature?(:proc_feature)
|
28
28
|
end
|
29
29
|
|
30
|
-
test 'should show a beta feature to the feature group' do
|
31
|
-
Rails.stubs(:env).returns('production')
|
32
|
-
FeatureFlipper.stubs(:current_feature_group).returns(:beta_users)
|
33
|
-
|
34
|
-
assert show_feature?(:beta_feature)
|
35
|
-
end
|
36
|
-
|
37
|
-
test 'should not show a beta feature if not in the group' do
|
38
|
-
Rails.stubs(:env).returns('production')
|
39
|
-
FeatureFlipper.stubs(:current_feature_group).returns(nil)
|
40
|
-
|
41
|
-
assert !show_feature?(:beta_feature)
|
42
|
-
end
|
43
|
-
|
44
|
-
test 'should always show a beta feature on dev' do
|
45
|
-
Rails.stubs(:env).returns('development')
|
46
|
-
FeatureFlipper.stubs(:current_feature_group).returns(nil)
|
47
|
-
|
48
|
-
assert show_feature?(:beta_feature)
|
49
|
-
end
|
50
|
-
|
51
30
|
test 'should be able to get features' do
|
52
31
|
FeatureFlipper::Config.ensure_config_is_loaded
|
53
32
|
all_features = FeatureFlipper::Config.features
|
54
33
|
|
55
34
|
assert_not_nil all_features
|
56
35
|
assert all_features.is_a?(Hash)
|
57
|
-
assert_equal :dev, all_features[:dev_feature][:
|
36
|
+
assert_equal :dev, all_features[:dev_feature][:state]
|
58
37
|
assert_equal 'dev feature', all_features[:dev_feature][:description]
|
59
38
|
end
|
60
39
|
end
|
@@ -88,7 +67,48 @@ context 'DSL based FeatureFlipper' do
|
|
88
67
|
|
89
68
|
assert_not_nil all_features
|
90
69
|
assert all_features.is_a?(Hash)
|
91
|
-
assert_equal :dev, all_features[:dev_feature][:
|
70
|
+
assert_equal :dev, all_features[:dev_feature][:state]
|
92
71
|
assert_equal 'dev feature', all_features[:dev_feature][:description]
|
93
72
|
end
|
94
73
|
end
|
74
|
+
|
75
|
+
context 'dynamic feature groups' do
|
76
|
+
setup do
|
77
|
+
FeatureFlipper::Config.path_to_file = 'features.rb'
|
78
|
+
FeatureFlipper::Config.reload_config
|
79
|
+
FeatureFlipper.reset_current_feature_groups
|
80
|
+
end
|
81
|
+
|
82
|
+
test 'should show a beta feature to the feature group' do
|
83
|
+
Rails.stubs(:env).returns('production')
|
84
|
+
FeatureFlipper.current_feature_groups << :beta_users
|
85
|
+
|
86
|
+
assert show_feature?(:beta_feature_old)
|
87
|
+
assert show_feature?(:beta_feature_new)
|
88
|
+
end
|
89
|
+
|
90
|
+
test 'should not show a beta feature if not in the group' do
|
91
|
+
Rails.stubs(:env).returns('production')
|
92
|
+
FeatureFlipper.current_feature_groups << :different_feature_group
|
93
|
+
|
94
|
+
assert !show_feature?(:beta_feature_old)
|
95
|
+
assert !show_feature?(:beta_feature_new)
|
96
|
+
end
|
97
|
+
|
98
|
+
test 'should always show a beta feature on dev' do
|
99
|
+
Rails.stubs(:env).returns('development')
|
100
|
+
FeatureFlipper.current_feature_groups << nil
|
101
|
+
|
102
|
+
assert show_feature?(:beta_feature_old)
|
103
|
+
assert show_feature?(:beta_feature_new)
|
104
|
+
end
|
105
|
+
|
106
|
+
test 'can be in two feature groups at the same time' do
|
107
|
+
Rails.stubs(:env).returns('production')
|
108
|
+
FeatureFlipper.current_feature_groups << :beta_users
|
109
|
+
FeatureFlipper.current_feature_groups << :employees
|
110
|
+
|
111
|
+
assert show_feature?(:beta_feature_new)
|
112
|
+
assert show_feature?(:employee_feature)
|
113
|
+
end
|
114
|
+
end
|
data/test/features.rb
CHANGED
@@ -1,16 +1,20 @@
|
|
1
1
|
FeatureFlipper::Config.features = {
|
2
|
-
:live_feature => { :
|
3
|
-
:disabled_feature => { :
|
4
|
-
:dev_feature => { :
|
5
|
-
:boolean_feature => { :
|
6
|
-
:proc_feature => { :
|
7
|
-
:
|
2
|
+
:live_feature => { :state => :live },
|
3
|
+
:disabled_feature => { :state => :disabled },
|
4
|
+
:dev_feature => { :state => :dev, :description => 'dev feature' },
|
5
|
+
:boolean_feature => { :state => true },
|
6
|
+
:proc_feature => { :state => Proc.new { Date.today > Date.today - 84000 } },
|
7
|
+
:beta_feature_old => { :state => :beta_old },
|
8
|
+
:beta_feature_new => { :state => :beta_new },
|
9
|
+
:employee_feature => { :state => :employees }
|
8
10
|
}
|
9
11
|
|
10
12
|
|
11
13
|
FeatureFlipper::Config.states = {
|
12
|
-
:disabled
|
13
|
-
:dev
|
14
|
-
:
|
15
|
-
:
|
14
|
+
:disabled => false,
|
15
|
+
:dev => ['development', 'test'].include?(Rails.env),
|
16
|
+
:beta_old => { :beta_users => :dev },
|
17
|
+
:beta_new => { :required_state => :dev, :feature_group => :beta_users },
|
18
|
+
:employees => { :required_state => :dev, :feature_group => :employees },
|
19
|
+
:live => true
|
16
20
|
}
|
data/test/features_dsl.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
FeatureFlipper.features do
|
2
|
-
|
2
|
+
in_state :dev do
|
3
3
|
feature :dev_feature, :description => 'dev feature'
|
4
4
|
end
|
5
5
|
|
6
|
-
|
6
|
+
in_state :live do
|
7
7
|
feature :live_feature
|
8
8
|
end
|
9
9
|
|
10
|
-
|
10
|
+
in_state true do
|
11
11
|
feature :boolean_feature
|
12
12
|
end
|
13
13
|
|
14
|
-
|
14
|
+
in_state Proc.new { Date.today > Date.today - 84000 } do
|
15
15
|
feature :proc_feature
|
16
16
|
end
|
17
17
|
end
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: feature_flipper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 1.1.0
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- Florian Munz
|
@@ -9,7 +14,7 @@ autorequire:
|
|
9
14
|
bindir: bin
|
10
15
|
cert_chain: []
|
11
16
|
|
12
|
-
date: 2010-05-
|
17
|
+
date: 2010-05-21 00:00:00 +02:00
|
13
18
|
default_executable:
|
14
19
|
dependencies: []
|
15
20
|
|
@@ -37,8 +42,9 @@ files:
|
|
37
42
|
- test/features.rb
|
38
43
|
- test/features_dsl.rb
|
39
44
|
- test/test_helper.rb
|
45
|
+
- examples/dynamic_states.rb
|
40
46
|
- examples/features.rb
|
41
|
-
- examples/
|
47
|
+
- examples/static_states.rb
|
42
48
|
has_rdoc: true
|
43
49
|
homepage: http://github.com/qype/feature_flipper
|
44
50
|
licenses: []
|
@@ -52,18 +58,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
58
|
requirements:
|
53
59
|
- - ">="
|
54
60
|
- !ruby/object:Gem::Version
|
61
|
+
segments:
|
62
|
+
- 0
|
55
63
|
version: "0"
|
56
|
-
version:
|
57
64
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
65
|
requirements:
|
59
66
|
- - ">="
|
60
67
|
- !ruby/object:Gem::Version
|
68
|
+
segments:
|
69
|
+
- 0
|
61
70
|
version: "0"
|
62
|
-
version:
|
63
71
|
requirements: []
|
64
72
|
|
65
73
|
rubyforge_project:
|
66
|
-
rubygems_version: 1.3.
|
74
|
+
rubygems_version: 1.3.6
|
67
75
|
signing_key:
|
68
76
|
specification_version: 3
|
69
77
|
summary: FeatureFlipper helps you flipping features
|