ruby_flipper 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +34 -0
- data/README.rdoc +126 -0
- data/Rakefile +8 -0
- data/autotest/discover.rb +1 -0
- data/lib/ruby_flipper/condition_context.rb +11 -0
- data/lib/ruby_flipper/dsl.rb +12 -0
- data/lib/ruby_flipper/feature.rb +51 -0
- data/lib/ruby_flipper/object_mixin.rb +16 -0
- data/lib/ruby_flipper.rb +19 -0
- data/ruby_flipper.gemspec +24 -0
- data/spec/fixtures/features.rb +38 -0
- data/spec/ruby_flipper/condition_context_spec.rb +31 -0
- data/spec/ruby_flipper/dsl_spec.rb +34 -0
- data/spec/ruby_flipper/feature_spec.rb +178 -0
- data/spec/ruby_flipper/object_mixin_spec.rb +72 -0
- data/spec/ruby_flipper_spec.rb +222 -0
- data/spec/spec_helper.rb +12 -0
- metadata +140 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
ruby_flipper (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
builder (3.0.0)
|
10
|
+
ci_reporter (1.6.3)
|
11
|
+
builder (>= 2.1.2)
|
12
|
+
diff-lcs (1.1.2)
|
13
|
+
mocha (0.9.10)
|
14
|
+
rake
|
15
|
+
rake (0.8.7)
|
16
|
+
rspec (2.0.1)
|
17
|
+
rspec-core (~> 2.0.1)
|
18
|
+
rspec-expectations (~> 2.0.1)
|
19
|
+
rspec-mocks (~> 2.0.1)
|
20
|
+
rspec-core (2.0.1)
|
21
|
+
rspec-expectations (2.0.1)
|
22
|
+
diff-lcs (>= 1.1.2)
|
23
|
+
rspec-mocks (2.0.1)
|
24
|
+
rspec-core (~> 2.0.1)
|
25
|
+
rspec-expectations (~> 2.0.1)
|
26
|
+
|
27
|
+
PLATFORMS
|
28
|
+
ruby
|
29
|
+
|
30
|
+
DEPENDENCIES
|
31
|
+
ci_reporter (~> 1.6.3)
|
32
|
+
mocha (~> 0.9.10)
|
33
|
+
rspec (~> 2.0.1)
|
34
|
+
ruby_flipper!
|
data/README.rdoc
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
= Ruby Flipper
|
2
|
+
|
3
|
+
Ruby Flipper makes it easy to define features and switch them on and off. It's configuration DSL provides a very flexible but still not too verbose syntax to fulfil your flipping needs.
|
4
|
+
|
5
|
+
|
6
|
+
== Usage
|
7
|
+
|
8
|
+
To use ruby flipper, you have to load a feature definition file somewhere:
|
9
|
+
|
10
|
+
RubyFlipper.load(File.expand_path '../features.rb', __FILE__)
|
11
|
+
|
12
|
+
This configuration can be reset by calling:
|
13
|
+
|
14
|
+
RubyFlipper.reset
|
15
|
+
|
16
|
+
|
17
|
+
== Feature Definitions
|
18
|
+
|
19
|
+
The file loaded by ruby flipper is written in ruby flipper's simple DSL. The most basic feature definition is just a feature with a name. Without any further conditions, the feature is active by default.
|
20
|
+
|
21
|
+
feature :always_on_feature
|
22
|
+
|
23
|
+
You can specify conditions that are used to determine whether a feature is active or not in three different ways:
|
24
|
+
|
25
|
+
feature :conditional_feature_with_parameter, false
|
26
|
+
feature :conditional_feature_with_condition_opt, :condition => false
|
27
|
+
feature :conditional_feature_with_conditions_opt, :conditions => false
|
28
|
+
|
29
|
+
In the examples above, the features are statically disabled. You can use three different types of conditions.
|
30
|
+
|
31
|
+
=== Static Conditions
|
32
|
+
|
33
|
+
A static condition is a boolean that is evaluated when the feature definitions are loaded. This can be used to statically activate/deactivate features or make features dependent on variables that don't change during runtime.
|
34
|
+
|
35
|
+
feature :development_feature, Rails.env.development?
|
36
|
+
|
37
|
+
This defines a feature that is only active in Rails' development mode.
|
38
|
+
|
39
|
+
=== Reference Conditions
|
40
|
+
|
41
|
+
A reference condition is a symbol that references another feature. This can be used to make a feature dependent on another feature or extract complex condition definitions and reuse them for several features.
|
42
|
+
|
43
|
+
feature :dependent_feature, :development_feature
|
44
|
+
|
45
|
+
This defines a feature that is only active when the feature :development_feature is active.
|
46
|
+
|
47
|
+
=== Dynamic Conditions
|
48
|
+
|
49
|
+
A dynamic condition is a proc (or anything that responds to :call) that will be evaluated each time the state of a feature is checked. This can be used to activate/deactivate features dynamically depending on variables that change during runtime.
|
50
|
+
|
51
|
+
feature :admin_feature, condition => lambda { User.current.is_admin? }
|
52
|
+
|
53
|
+
This defines a feature that is only active when the current user is an admin (the user specific stuff must be implemented somewhere else).
|
54
|
+
|
55
|
+
Dynamic conditions can also be given to the feature method as a block:
|
56
|
+
|
57
|
+
feature :admin_feature do
|
58
|
+
User.current.is_admin?
|
59
|
+
end
|
60
|
+
|
61
|
+
Within such a proc, you can also check whether another feature is active or not to phrase complex condition dependencies:
|
62
|
+
|
63
|
+
feature :shy_feature do
|
64
|
+
active?(:development_feature) && (active?(:admin_feature) || Time.now.hour < 8)
|
65
|
+
end
|
66
|
+
|
67
|
+
This defines a feature that is only active when the feature :development_feature is active and either the feature :admin_feature is active or it is between midnight and 8:00 in the morning (however that might be useful).
|
68
|
+
|
69
|
+
=== Combined Conditions
|
70
|
+
|
71
|
+
A combined condition is an array of other conditions. Any type of condition can be combined in an array. All conditions have to be met for a feature to be active.
|
72
|
+
|
73
|
+
feature :combined_feature, [:development_feature, lambda { User.current.name == 'patti' }]
|
74
|
+
|
75
|
+
This defines a feature that is only enabled when the feature :development_feature is enbled and the current user is 'patti'.
|
76
|
+
|
77
|
+
=== More
|
78
|
+
|
79
|
+
For more examples, see spec/fixtures/features.rb and spec/ruby_flipper_spec.rb (section .load which is kind of an end-to-end integration test).
|
80
|
+
|
81
|
+
|
82
|
+
== Condition Definitions
|
83
|
+
|
84
|
+
If you have a lot of dependent features with shared conditions you can clean up your definition file by explicitly defining conditions that aren't used as features but just as conditions real features depend on.
|
85
|
+
|
86
|
+
condition :development, Rails.env.development?
|
87
|
+
condition :admin { User.current.is_admin? }
|
88
|
+
|
89
|
+
feature :user_administration, :development, :admin
|
90
|
+
feature :new_feature, :development
|
91
|
+
|
92
|
+
Of course, you can nest this as deep as you like (features depending on conditions depending on conditions depending on features depending on conditions...), although this might lead to completely incomprehensible rulesets.
|
93
|
+
|
94
|
+
The condition method is just an alias to the feature method, so internally, it makes absolutely no difference which method you use. Using both methods just helps you structuring your feature definitions and makes the feature definition file easier to understand.
|
95
|
+
|
96
|
+
|
97
|
+
== Usage
|
98
|
+
|
99
|
+
Finally, there are two ways to check whether a feature is active or not within your code. You can just give it a block that will only be evaluated when the feature is active:
|
100
|
+
|
101
|
+
feature_active?(:hidden_feature) do
|
102
|
+
# do something for that feature
|
103
|
+
end
|
104
|
+
|
105
|
+
or if you need the state of a feature as a boolean value or want to do something when a feature is not active:
|
106
|
+
|
107
|
+
if feature_active?(:hidden_feature)
|
108
|
+
# do something for that feature
|
109
|
+
else
|
110
|
+
# do something else
|
111
|
+
end
|
112
|
+
|
113
|
+
This method is defined on Object, so it is available anywhere you might ever need it.
|
114
|
+
|
115
|
+
|
116
|
+
== Describing Features
|
117
|
+
|
118
|
+
Last and least, you can describe your features, for example when you use ticket numbers or anything other cryptic for your feature names. The description isn't used anywhere yet, but it might make your code more readable.
|
119
|
+
|
120
|
+
feature :feature_452, :development, :description => 'Feature 452: An end user can change his data on his profile page.'
|
121
|
+
|
122
|
+
You could also just use comments for that, but giving this to ruby flipper in code enables you to evaluate your features and print lists containing the description.
|
123
|
+
|
124
|
+
RubyFlipper::Feature.all.each do |feature|
|
125
|
+
puts "#{feature.name}: #{feature.description}"
|
126
|
+
end
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Autotest.add_discovery { "rspec2" }
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module RubyFlipper
|
2
|
+
|
3
|
+
class Feature
|
4
|
+
|
5
|
+
def self.all
|
6
|
+
@@features ||= {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.reset
|
10
|
+
@@features = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.add(name, *conditions, &block)
|
14
|
+
all[name] = new(name, *conditions, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.find(name)
|
18
|
+
all[name] || raise(NotDefinedError, "'#{name}' is not defined")
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.condition_met?(condition)
|
22
|
+
if condition.is_a?(Symbol)
|
23
|
+
find(condition).active?
|
24
|
+
elsif condition.is_a?(Proc)
|
25
|
+
!!ConditionContext.new.instance_eval(&condition)
|
26
|
+
elsif condition.respond_to?(:call)
|
27
|
+
!!condition.call
|
28
|
+
else
|
29
|
+
!!condition
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :name, :description, :conditions
|
34
|
+
|
35
|
+
def initialize(name, *conditions, &block)
|
36
|
+
if conditions.last.is_a?(Hash)
|
37
|
+
opts = conditions.pop
|
38
|
+
@description = opts[:description]
|
39
|
+
conditions << opts[:condition]
|
40
|
+
conditions << opts[:conditions]
|
41
|
+
end
|
42
|
+
@name, @conditions = name, [conditions, block].flatten.compact
|
43
|
+
end
|
44
|
+
|
45
|
+
def active?
|
46
|
+
conditions.map {|condition| self.class.condition_met?(condition)}.all?
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module RubyFlipper
|
2
|
+
|
3
|
+
module ObjectMixin
|
4
|
+
|
5
|
+
def feature_active?(name)
|
6
|
+
active = Feature.find(name).active?
|
7
|
+
yield if active && block_given?
|
8
|
+
active
|
9
|
+
end
|
10
|
+
alias :condition_met? :feature_active?
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
Object.send :include, RubyFlipper::ObjectMixin
|
data/lib/ruby_flipper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'ruby_flipper/object_mixin'
|
2
|
+
|
3
|
+
module RubyFlipper
|
4
|
+
|
5
|
+
class NotDefinedError < StandardError; end
|
6
|
+
|
7
|
+
autoload :ConditionContext, 'ruby_flipper/condition_context'
|
8
|
+
autoload :Dsl, 'ruby_flipper/dsl'
|
9
|
+
autoload :Feature, 'ruby_flipper/feature'
|
10
|
+
|
11
|
+
def self.load(file)
|
12
|
+
Dsl.new.instance_eval(IO.read file)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.reset
|
16
|
+
Feature.reset
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "ruby_flipper"
|
6
|
+
s.version = "0.0.2"
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Thomas Jachmann"]
|
9
|
+
s.email = ["self@thomasjachmann.com"]
|
10
|
+
s.homepage = "https://github.com/blaulabs/ruby_flipper"
|
11
|
+
s.summary = %q{Make switching features on and off easy.}
|
12
|
+
s.description = %q{Most flexible still least verbose feature flipper for ruby projects.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "ruby_flipper"
|
15
|
+
|
16
|
+
s.add_development_dependency "ci_reporter", "~> 1.6.3"
|
17
|
+
s.add_development_dependency "rspec", "~> 2.0.1"
|
18
|
+
s.add_development_dependency "mocha", "~> 0.9.10"
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# statically configured condition (initially evaluated when configuration is loaded)
|
2
|
+
condition :static_is_cherry, FLIPPER_ENV[:static] == 'cherry'
|
3
|
+
|
4
|
+
# dynamically configured condition (evaluated on each access)
|
5
|
+
condition :dynamic_is_floyd do
|
6
|
+
FLIPPER_ENV[:dynamic] == 'floyd'
|
7
|
+
end
|
8
|
+
|
9
|
+
# combined condition referencing both above
|
10
|
+
condition :combined_is_cherry_and_floyd, :static_is_cherry, :dynamic_is_floyd
|
11
|
+
|
12
|
+
# combined condition with static and dynamic
|
13
|
+
condition :combined_is_lulu_first_then_gavin, FLIPPER_ENV[:changing] == 'lulu' do
|
14
|
+
FLIPPER_ENV[:changing] == 'gavin'
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
# disabled feature
|
19
|
+
feature :disabled, :description => 'disabled feature', :condition => false
|
20
|
+
|
21
|
+
# feature referencing a single predefined condition
|
22
|
+
feature :floyd, :description => 'just for floyd', :condition => :dynamic_is_floyd
|
23
|
+
|
24
|
+
# feature depending on a dynamic condition (lambda)
|
25
|
+
feature :philip, :description => 'just for philip', :condition => lambda { FLIPPER_ENV[:dynamic] == 'philip' }
|
26
|
+
|
27
|
+
# feature depending on array of a static condition, a predefined condition and a dynamic condition (lambda)
|
28
|
+
# given in :conditions (can be both :condition/:conditions)
|
29
|
+
feature :patti, :description => 'just for patti', :conditions => [
|
30
|
+
FLIPPER_ENV[:changing] == 'patti',
|
31
|
+
:dynamic_is_floyd,
|
32
|
+
lambda { FLIPPER_ENV[:changing] == 'gavin'}
|
33
|
+
]
|
34
|
+
|
35
|
+
# feature with a complex combination of dynamic and predifined conditions given as a block
|
36
|
+
feature :sue, :description => 'just for sue' do
|
37
|
+
(active?(:static_is_cherry) || active?(:dynamic_is_floyd)) && FLIPPER_ENV[:changing] == 'sue'
|
38
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RubyFlipper::ConditionContext do
|
4
|
+
|
5
|
+
describe '#active?' do
|
6
|
+
|
7
|
+
it 'should return true on an active feature' do
|
8
|
+
RubyFlipper::Feature.add(:referenced, true)
|
9
|
+
subject.active?(:referenced).should == true
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should return false on an inactive feature' do
|
13
|
+
RubyFlipper::Feature.add(:referenced, false)
|
14
|
+
subject.active?(:referenced).should == false
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should raise an error when the referenced feature is not defined' do
|
18
|
+
lambda { subject.active?(:referenced) }.should raise_error RubyFlipper::NotDefinedError, '\'referenced\' is not defined'
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should handle multiple conditions as well' do
|
22
|
+
RubyFlipper::Feature.add(:referenced1, true)
|
23
|
+
RubyFlipper::Feature.add(:referenced2, false)
|
24
|
+
RubyFlipper::Feature.add(:referenced3, true)
|
25
|
+
subject.active?(:referenced1, :referenced2).should == false
|
26
|
+
subject.active?(:referenced1, :referenced3).should == true
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RubyFlipper::Dsl do
|
4
|
+
|
5
|
+
it 'should ensure mocked methods exist' do
|
6
|
+
RubyFlipper::Feature.should respond_to(:add)
|
7
|
+
end
|
8
|
+
|
9
|
+
[:feature, :condition].each do |method|
|
10
|
+
|
11
|
+
describe "##{method}" do
|
12
|
+
|
13
|
+
it 'should call add with only a name' do
|
14
|
+
RubyFlipper::Feature.expects(:add).with(:feature_name).returns(:feature)
|
15
|
+
subject.send(method, :feature_name).should == :feature
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should call add with a name and conditions' do
|
19
|
+
RubyFlipper::Feature.expects(:add).with(:feature_name, :one, :two).returns(:feature)
|
20
|
+
subject.send(method, :feature_name, :one, :two).should == :feature
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should call add with a name, conditions and a block' do
|
24
|
+
block = lambda {}
|
25
|
+
# cannot mock this since mocha doesn't allow to test for a proc [thomas, 2010-12-13]
|
26
|
+
feature = subject.send(method, :feature_name, :one, :two, &block)
|
27
|
+
feature.conditions.should == [:one, :two, block]
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RubyFlipper::Feature do
|
4
|
+
|
5
|
+
describe '.all' do
|
6
|
+
|
7
|
+
it 'should be a hash' do
|
8
|
+
RubyFlipper::Feature.all.should == {}
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '.reset' do
|
14
|
+
|
15
|
+
it 'should reset the all hash' do
|
16
|
+
RubyFlipper::Feature.add(:inactive, false)
|
17
|
+
RubyFlipper::Feature.all.should_not be_empty
|
18
|
+
RubyFlipper::Feature.reset
|
19
|
+
RubyFlipper::Feature.all.should be_empty
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '.add' do
|
25
|
+
|
26
|
+
it 'should call new with only a name and store the feature' do
|
27
|
+
RubyFlipper::Feature.expects(:new).with(:feature_name).returns(:feature)
|
28
|
+
RubyFlipper::Feature.add(:feature_name).should == :feature
|
29
|
+
RubyFlipper::Feature.all[:feature_name].should == :feature
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should call add with a name and conditions and store the feature' do
|
33
|
+
RubyFlipper::Feature.expects(:new).with(:feature_name, :one, :two).returns(:feature)
|
34
|
+
RubyFlipper::Feature.add(:feature_name, :one, :two).should == :feature
|
35
|
+
RubyFlipper::Feature.all[:feature_name].should == :feature
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should call add with a name, conditions and a block and store the feature' do
|
39
|
+
block = lambda {}
|
40
|
+
# cannot mock this since mocha doesn't allow to test for a proc [thomas, 2010-12-13]
|
41
|
+
feature = RubyFlipper::Feature.add(:feature_name, :one, :two, &block)
|
42
|
+
feature.conditions.should == [:one, :two, block]
|
43
|
+
RubyFlipper::Feature.all[:feature_name].should == feature
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '.find' do
|
49
|
+
|
50
|
+
it 'should return the stored feature' do
|
51
|
+
feature = RubyFlipper::Feature.add(:feature_name)
|
52
|
+
RubyFlipper::Feature.find(:feature_name).should == feature
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should raise an error when the referenced feature is not defined' do
|
56
|
+
lambda { RubyFlipper::Feature.find(:feature_name) }.should raise_error RubyFlipper::NotDefinedError, '\'feature_name\' is not defined'
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '.condition_met?' do
|
62
|
+
|
63
|
+
context 'with a symbol' do
|
64
|
+
|
65
|
+
it 'should return the state of the referenced feature' do
|
66
|
+
RubyFlipper::Feature.add(:referenced, true)
|
67
|
+
RubyFlipper::Feature.condition_met?(:referenced).should == true
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should raise an error when the referenced feature is not defined' do
|
71
|
+
lambda { RubyFlipper::Feature.condition_met?(:missing) }.should raise_error RubyFlipper::NotDefinedError, '\'missing\' is not defined'
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
{
|
77
|
+
true => true,
|
78
|
+
'anything' => true,
|
79
|
+
false => false,
|
80
|
+
nil => false
|
81
|
+
}.each do |condition, expected|
|
82
|
+
|
83
|
+
it "should call a given proc and return #{expected} when it returns #{condition}" do
|
84
|
+
RubyFlipper::Feature.condition_met?(lambda { condition }).should == expected
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should call anything callable and return #{expected} when it returns #{condition}" do
|
88
|
+
RubyFlipper::Feature.condition_met?(stub(:call => condition)).should == expected
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should return #{expected} when the condition is #{condition}" do
|
92
|
+
RubyFlipper::Feature.condition_met?(condition).should == expected
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'with a complex proc' do
|
98
|
+
|
99
|
+
it 'should return the met? of the combined referenced conditions' do
|
100
|
+
RubyFlipper::Feature.add(:true, true)
|
101
|
+
RubyFlipper::Feature.add(:false, false)
|
102
|
+
RubyFlipper::Feature.condition_met?(lambda { active?(:true) || active?(:false) }).should == true
|
103
|
+
RubyFlipper::Feature.condition_met?(lambda { active?(:true) && active?(:false) }).should == false
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
describe 'initializer' do
|
111
|
+
|
112
|
+
it 'should store the name' do
|
113
|
+
RubyFlipper::Feature.new(:feature_name).name.should == :feature_name
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'should store the description' do
|
117
|
+
RubyFlipper::Feature.new(:feature_name, :description => 'desc').description.should == 'desc'
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should work with a single static condition' do
|
121
|
+
RubyFlipper::Feature.new(:feature_name, true).conditions.should == [true]
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'should work with multiple static conditions' do
|
125
|
+
RubyFlipper::Feature.new(:feature_name, true, :development).conditions.should == [true, :development]
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'should work with a dynamic condition as parameter' do
|
129
|
+
condition = lambda { true }
|
130
|
+
RubyFlipper::Feature.new(:feature_name, condition).conditions.should == [condition]
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'should work with a dynamic condition as block' do
|
134
|
+
condition = lambda { true }
|
135
|
+
RubyFlipper::Feature.new(:feature_name, &condition).conditions.should == [condition]
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should work with a combination of static and dynamic conditions' do
|
139
|
+
condition = lambda { true }
|
140
|
+
RubyFlipper::Feature.new(:feature_name, false, :live, condition).conditions.should == [false, :live, condition]
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'should work with conditions given in the opts hash as :condition' do
|
144
|
+
RubyFlipper::Feature.new(:feature_name, :condition => true).conditions.should == [true]
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'should work with conditions given in the opts hash as :conditions' do
|
148
|
+
RubyFlipper::Feature.new(:feature_name, :conditions => false).conditions.should == [false]
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'should work with a combination of arrays, dynamic conditions and conditions given in the opts hash and eliminate nil' do
|
152
|
+
condition = lambda { true }
|
153
|
+
RubyFlipper::Feature.new(:feature_name, [false, nil], :condition => [nil, 'c'], :conditions => ['c1', 'c2'], &condition).conditions.should == [false, 'c', 'c1', 'c2', condition]
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
|
158
|
+
describe '#active?' do
|
159
|
+
|
160
|
+
it 'should return true when no conditions are given (unconditionally active)' do
|
161
|
+
RubyFlipper::Feature.new(:feature_name).active?.should == true
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'should return false when not all conditions are met (with dynamic)' do
|
165
|
+
RubyFlipper::Feature.new(:feature_name, true, lambda { false }).active?.should == false
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'should return false when not all conditions are met (only static)' do
|
169
|
+
RubyFlipper::Feature.new(:feature_name, false, true).active?.should == false
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'should return true when all conditions are met' do
|
173
|
+
RubyFlipper::Feature.new(:feature_name, true, true).active?.should == true
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RubyFlipper::ObjectMixin do
|
4
|
+
|
5
|
+
it 'should be included in Object' do
|
6
|
+
Object.included_modules.should include RubyFlipper::ObjectMixin
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '#feature_active?' do
|
10
|
+
|
11
|
+
subject do
|
12
|
+
# this is not really needed, since one could just call feature_active?
|
13
|
+
# but to be independent of the inclusion in Object (see above), better
|
14
|
+
# create a new subject that is sure to include RubyFlipper::ObjectMixin.
|
15
|
+
Class.new do
|
16
|
+
include RubyFlipper::ObjectMixin
|
17
|
+
end.new
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'with an active feature' do
|
21
|
+
|
22
|
+
before(:each) do
|
23
|
+
RubyFlipper::Feature.add(:active, true)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should return true when called without a block' do
|
27
|
+
subject.feature_active?(:active).should == true
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should execute the block and return true when called with a block' do
|
31
|
+
var = 'before'
|
32
|
+
subject.feature_active?(:active) do
|
33
|
+
var = 'after'
|
34
|
+
end.should == true
|
35
|
+
var.should == 'after'
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'with an inactive feature' do
|
41
|
+
|
42
|
+
before(:each) do
|
43
|
+
RubyFlipper::Feature.add(:inactive, false)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should return false when called without a block' do
|
47
|
+
subject.feature_active?(:inactive).should == false
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should not execute the block and return false when called with a block' do
|
51
|
+
subject.feature_active?(:inactive) do
|
52
|
+
fail 'the given block should not be called'
|
53
|
+
end.should == false
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'with a missing feature' do
|
59
|
+
|
60
|
+
it 'should raise an error when called without a block and the referenced feature is not defined' do
|
61
|
+
lambda { subject.feature_active?(:missing) }.should raise_error RubyFlipper::NotDefinedError, '\'missing\' is not defined'
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should not execute the block and raise an error when called with a block and the referenced feature is not defined' do
|
65
|
+
lambda { subject.feature_active?(:missing) { fail 'the given block should not be called' } }.should raise_error RubyFlipper::NotDefinedError, '\'missing\' is not defined'
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RubyFlipper do
|
4
|
+
|
5
|
+
describe '.load' do
|
6
|
+
|
7
|
+
def load_features
|
8
|
+
RubyFlipper.load(File.expand_path '../fixtures/features.rb', __FILE__)
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'conditions' do
|
12
|
+
|
13
|
+
context ':static_is_cherry' do
|
14
|
+
|
15
|
+
it 'should not be met when static is not cherry when loading' do
|
16
|
+
load_features
|
17
|
+
RubyFlipper::Feature.find(:static_is_cherry).should_not be_active
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should not be met when static is not cherry when loading even when it is when called' do
|
21
|
+
load_features
|
22
|
+
FLIPPER_ENV[:static] = 'cherry'
|
23
|
+
RubyFlipper::Feature.find(:static_is_cherry).should_not be_active
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should be met when static is not cherry when called' do
|
27
|
+
FLIPPER_ENV[:static] = 'cherry'
|
28
|
+
load_features
|
29
|
+
RubyFlipper::Feature.find(:static_is_cherry).should be_active
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
context ':dynamic_is_floyd' do
|
35
|
+
|
36
|
+
it 'should not be met when dynamic is not floyd when called' do
|
37
|
+
load_features
|
38
|
+
RubyFlipper::Feature.find(:dynamic_is_floyd).should_not be_active
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should not be met when dynamic is not floyd when called even when it was when loaded' do
|
42
|
+
FLIPPER_ENV[:dynamic] = 'floyd'
|
43
|
+
load_features
|
44
|
+
FLIPPER_ENV[:dynamic] = 'sue'
|
45
|
+
RubyFlipper::Feature.find(:dynamic_is_floyd).should_not be_active
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should be met when dynamic is floyd when called' do
|
49
|
+
load_features
|
50
|
+
FLIPPER_ENV[:dynamic] = 'floyd'
|
51
|
+
RubyFlipper::Feature.find(:dynamic_is_floyd).should be_active
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
context ':combined_is_cherry_and_floyd' do
|
57
|
+
|
58
|
+
it 'should not be met when static is not cherry when loaded even when it is when called and dynamic is floyd' do
|
59
|
+
load_features
|
60
|
+
FLIPPER_ENV[:dynamic] = 'floyd'
|
61
|
+
RubyFlipper::Feature.find(:combined_is_cherry_and_floyd).should_not be_active
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should not be met when static is cherry but dynamic is not floyd when called even when it was when loaded' do
|
65
|
+
FLIPPER_ENV[:static] = 'cherry'
|
66
|
+
FLIPPER_ENV[:dynamic] = 'floyd'
|
67
|
+
load_features
|
68
|
+
FLIPPER_ENV[:dynamic] = 'sue'
|
69
|
+
RubyFlipper::Feature.find(:combined_is_cherry_and_floyd).should_not be_active
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should be met when static is cherry when loaded and dynamic is floyd when called even when static is not cherry when called and dynamic was not floyd when called' do
|
73
|
+
FLIPPER_ENV[:static] = 'cherry'
|
74
|
+
load_features
|
75
|
+
FLIPPER_ENV[:static] = 'philip'
|
76
|
+
FLIPPER_ENV[:dynamic] = 'floyd'
|
77
|
+
RubyFlipper::Feature.find(:combined_is_cherry_and_floyd).should be_active
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
context ':combined_is_lulu_first_then_gavin' do
|
83
|
+
|
84
|
+
it 'should not be met when changing is not lulu when loaded even when it is gavin when called' do
|
85
|
+
load_features
|
86
|
+
FLIPPER_ENV[:changing] = 'gavin'
|
87
|
+
RubyFlipper::Feature.find(:combined_is_lulu_first_then_gavin).should_not be_active
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'should not be met when changing is lulu when loaded but it is not gavin when called' do
|
91
|
+
FLIPPER_ENV[:changing] = 'lulu'
|
92
|
+
load_features
|
93
|
+
RubyFlipper::Feature.find(:combined_is_lulu_first_then_gavin).should_not be_active
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'should be met when changing is lulu when loaded gavin when called' do
|
97
|
+
FLIPPER_ENV[:changing] = 'lulu'
|
98
|
+
load_features
|
99
|
+
FLIPPER_ENV[:changing] = 'gavin'
|
100
|
+
RubyFlipper::Feature.find(:combined_is_lulu_first_then_gavin).should be_active
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'features' do
|
108
|
+
|
109
|
+
context ':disabled' do
|
110
|
+
|
111
|
+
it 'should not be active' do
|
112
|
+
load_features
|
113
|
+
RubyFlipper::Feature.find(:disabled).should_not be_active
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
context ':floyd' do
|
119
|
+
|
120
|
+
it 'should not be active when dynamic is not floyd' do
|
121
|
+
load_features
|
122
|
+
RubyFlipper::Feature.find(:floyd).should_not be_active
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'should be active when dynamic is floyd' do
|
126
|
+
load_features
|
127
|
+
FLIPPER_ENV[:dynamic] = 'floyd'
|
128
|
+
RubyFlipper::Feature.find(:floyd).should be_active
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
context ':philip' do
|
134
|
+
|
135
|
+
it 'should not be active when dynamic is not philip' do
|
136
|
+
load_features
|
137
|
+
RubyFlipper::Feature.find(:philip).should_not be_active
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'should be active when dynamic is philip' do
|
141
|
+
load_features
|
142
|
+
FLIPPER_ENV[:dynamic] = 'philip'
|
143
|
+
RubyFlipper::Feature.find(:philip).should be_active
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
context ':patti' do
|
149
|
+
|
150
|
+
it 'should not be active when changing is not patti when loaded' do
|
151
|
+
load_features
|
152
|
+
FLIPPER_ENV[:dynamic] = 'floyd'
|
153
|
+
FLIPPER_ENV[:changing] = 'gavin'
|
154
|
+
RubyFlipper::Feature.find(:patti).should_not be_active
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'should not be active when dynamic is not floyd when called' do
|
158
|
+
FLIPPER_ENV[:changing] = 'patti'
|
159
|
+
FLIPPER_ENV[:dynamic] = 'floyd'
|
160
|
+
load_features
|
161
|
+
FLIPPER_ENV[:dynamic] = 'sue'
|
162
|
+
FLIPPER_ENV[:changing] = 'gavin'
|
163
|
+
RubyFlipper::Feature.find(:patti).should_not be_active
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'should not be active when changing is not gavin when called' do
|
167
|
+
FLIPPER_ENV[:changing] = 'patti'
|
168
|
+
FLIPPER_ENV[:changing] = 'gavin'
|
169
|
+
load_features
|
170
|
+
FLIPPER_ENV[:dynamic] = 'floyd'
|
171
|
+
FLIPPER_ENV[:changing] = 'sue'
|
172
|
+
RubyFlipper::Feature.find(:patti).should_not be_active
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'should be active when changing is patti when loaded, dynamic is floyd when called and changing is gavin when called' do
|
176
|
+
FLIPPER_ENV[:changing] = 'patti'
|
177
|
+
load_features
|
178
|
+
FLIPPER_ENV[:dynamic] = 'floyd'
|
179
|
+
FLIPPER_ENV[:changing] = 'gavin'
|
180
|
+
RubyFlipper::Feature.find(:patti).should be_active
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
|
185
|
+
context ':sue' do
|
186
|
+
|
187
|
+
it 'should not be active when static is not cherry when loaded and dynamic is not floyd when called' do
|
188
|
+
FLIPPER_ENV[:dynamic] = 'floyd'
|
189
|
+
load_features
|
190
|
+
FLIPPER_ENV[:dynamic] = 'gavin'
|
191
|
+
FLIPPER_ENV[:changing] = 'sue'
|
192
|
+
RubyFlipper::Feature.find(:sue).should_not be_active
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'should not be active when changing is not sue when called' do
|
196
|
+
FLIPPER_ENV[:static] = 'cherry'
|
197
|
+
load_features
|
198
|
+
FLIPPER_ENV[:dynamic] = 'floyd'
|
199
|
+
RubyFlipper::Feature.find(:sue).should_not be_active
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'should be active when static is cherry when loaded and changing is sue when called' do
|
203
|
+
FLIPPER_ENV[:static] = 'cherry'
|
204
|
+
load_features
|
205
|
+
FLIPPER_ENV[:changing] = 'sue'
|
206
|
+
RubyFlipper::Feature.find(:sue).should be_active
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'should be active when dynamic is floyd when called and changing is sue when called' do
|
210
|
+
load_features
|
211
|
+
FLIPPER_ENV[:dynamic] = 'floyd'
|
212
|
+
FLIPPER_ENV[:changing] = 'sue'
|
213
|
+
RubyFlipper::Feature.find(:sue).should be_active
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby_flipper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Thomas Jachmann
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-12-13 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: ci_reporter
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 9
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 6
|
33
|
+
- 3
|
34
|
+
version: 1.6.3
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rspec
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 13
|
46
|
+
segments:
|
47
|
+
- 2
|
48
|
+
- 0
|
49
|
+
- 1
|
50
|
+
version: 2.0.1
|
51
|
+
type: :development
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: mocha
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 47
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
- 9
|
65
|
+
- 10
|
66
|
+
version: 0.9.10
|
67
|
+
type: :development
|
68
|
+
version_requirements: *id003
|
69
|
+
description: Most flexible still least verbose feature flipper for ruby projects.
|
70
|
+
email:
|
71
|
+
- self@thomasjachmann.com
|
72
|
+
executables: []
|
73
|
+
|
74
|
+
extensions: []
|
75
|
+
|
76
|
+
extra_rdoc_files: []
|
77
|
+
|
78
|
+
files:
|
79
|
+
- .gitignore
|
80
|
+
- .rspec
|
81
|
+
- Gemfile
|
82
|
+
- Gemfile.lock
|
83
|
+
- README.rdoc
|
84
|
+
- Rakefile
|
85
|
+
- autotest/discover.rb
|
86
|
+
- lib/ruby_flipper.rb
|
87
|
+
- lib/ruby_flipper/condition_context.rb
|
88
|
+
- lib/ruby_flipper/dsl.rb
|
89
|
+
- lib/ruby_flipper/feature.rb
|
90
|
+
- lib/ruby_flipper/object_mixin.rb
|
91
|
+
- ruby_flipper.gemspec
|
92
|
+
- spec/fixtures/features.rb
|
93
|
+
- spec/ruby_flipper/condition_context_spec.rb
|
94
|
+
- spec/ruby_flipper/dsl_spec.rb
|
95
|
+
- spec/ruby_flipper/feature_spec.rb
|
96
|
+
- spec/ruby_flipper/object_mixin_spec.rb
|
97
|
+
- spec/ruby_flipper_spec.rb
|
98
|
+
- spec/spec_helper.rb
|
99
|
+
has_rdoc: true
|
100
|
+
homepage: https://github.com/blaulabs/ruby_flipper
|
101
|
+
licenses: []
|
102
|
+
|
103
|
+
post_install_message:
|
104
|
+
rdoc_options: []
|
105
|
+
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
hash: 3
|
114
|
+
segments:
|
115
|
+
- 0
|
116
|
+
version: "0"
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
none: false
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
hash: 3
|
123
|
+
segments:
|
124
|
+
- 0
|
125
|
+
version: "0"
|
126
|
+
requirements: []
|
127
|
+
|
128
|
+
rubyforge_project: ruby_flipper
|
129
|
+
rubygems_version: 1.3.7
|
130
|
+
signing_key:
|
131
|
+
specification_version: 3
|
132
|
+
summary: Make switching features on and off easy.
|
133
|
+
test_files:
|
134
|
+
- spec/fixtures/features.rb
|
135
|
+
- spec/ruby_flipper/condition_context_spec.rb
|
136
|
+
- spec/ruby_flipper/dsl_spec.rb
|
137
|
+
- spec/ruby_flipper/feature_spec.rb
|
138
|
+
- spec/ruby_flipper/object_mixin_spec.rb
|
139
|
+
- spec/ruby_flipper_spec.rb
|
140
|
+
- spec/spec_helper.rb
|