flippeur 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +0 -2
- data/README.md +103 -8
- data/Rakefile +7 -0
- data/flippeur.gemspec +3 -1
- data/lib/flippeur/feature.rb +14 -0
- data/lib/flippeur/rails_helpers.rb +24 -0
- data/lib/flippeur/railtie.rb +11 -0
- data/lib/flippeur/version.rb +1 -1
- data/lib/flippeur.rb +28 -1
- data/test/controller_helpers_test.rb +63 -0
- data/test/feature_test.rb +34 -0
- data/test/model_helpers_test.rb +60 -0
- data/test/view_helpers_test.rb +63 -0
- metadata +38 -5
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
# Flippeur
|
2
2
|
|
3
|
-
|
3
|
+
Flippeur is a simple feature flipper. Features are setup with a simple Ruby DSL where you specify whether a feature is available to a user.
|
4
|
+
|
5
|
+
If you're using Rails you can pop your feature definition file in an initializer.
|
6
|
+
|
7
|
+
Features are not stored in a database and there's no web UI for toggling them.
|
4
8
|
|
5
9
|
## Installation
|
6
10
|
|
@@ -18,12 +22,103 @@ Or install it yourself as:
|
|
18
22
|
|
19
23
|
## Usage
|
20
24
|
|
21
|
-
|
25
|
+
The basic idea is that a feature is, or isn't, available to a user of your webapp. So the logic determining whether or not a feature is available acts on a user.
|
26
|
+
|
27
|
+
In the view and the controller layers, the user is obtained automatically by calling the controller's `current_person` method (currently and regrettably hardcoded). In the model layer you need to pass an appropriate user instance explicitly.
|
28
|
+
|
29
|
+
(This is suboptimal and I'm exploring alternatives.)
|
30
|
+
|
31
|
+
First, setup your features. With Rails this would go in `config/initializers/flippeur.rb`.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
Flippeur.setup do
|
35
|
+
|
36
|
+
feature :something_fancy do |user|
|
37
|
+
user.is_fancy?
|
38
|
+
end
|
39
|
+
|
40
|
+
feature :something_else do |user|
|
41
|
+
user.name =~ /bob/i
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
Give each feature a name and a block. The block should return a truthy value if the feature is available to the user, and a falsey value otherwise.
|
48
|
+
|
49
|
+
N.B. to return early from the block, use `next retvalue` instead of `return retvalue`. For example:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
feature :add_widget do |user|
|
53
|
+
next false unless user.company.pays_lots_of_money?
|
54
|
+
user.admin? || user.my_friend?
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
Second, use the helpers to determine whether or not features are available to your current user.
|
59
|
+
|
60
|
+
### View
|
61
|
+
|
62
|
+
Use the `feature?(name)` helper with or without a block.
|
63
|
+
|
64
|
+
Without a block it acts as a simple conditional. For example:
|
65
|
+
|
66
|
+
```erb
|
67
|
+
<% if feature? :something_fancy %>
|
68
|
+
<p>Here is something fancy.</p>
|
69
|
+
<% else %>
|
70
|
+
<p>You could have something fancy if you upgraded...</p>
|
71
|
+
<% end %>
|
72
|
+
```
|
73
|
+
|
74
|
+
If you pass a block it will only be called if the feature is available. For example:
|
75
|
+
|
76
|
+
```erb
|
77
|
+
<% feature? :something_fancy do %>
|
78
|
+
<p>Here is something fancy.</p>
|
79
|
+
<% end %>
|
80
|
+
```
|
81
|
+
|
82
|
+
### Controller
|
83
|
+
|
84
|
+
Exactly the same as the view. For example:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
def create
|
88
|
+
if feature? :something_fancy
|
89
|
+
# the usual stuff
|
90
|
+
else
|
91
|
+
redirect_to :index, notice: 'You need to upgrade to create something fancy.'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
### Model
|
97
|
+
|
98
|
+
Here you also need to pass a user instance to the helper. For example:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class Project < ActiveRecord::Base
|
102
|
+
belongs_to :account
|
103
|
+
def do_something_fancy
|
104
|
+
if feature? :something_fancy, account.users.first
|
105
|
+
# something fancy
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
This is suboptimal and should be revisited...
|
112
|
+
|
113
|
+
|
114
|
+
## See Also
|
115
|
+
|
116
|
+
* [Flipper](https://github.com/jnunemaker/flipper)
|
117
|
+
* [Flip](https://github.com/pda/flip)
|
118
|
+
* [Rollout](https://github.com/jamesgolick/rollout)
|
119
|
+
|
120
|
+
|
121
|
+
## Intellectual Property
|
22
122
|
|
23
|
-
|
123
|
+
Copyright Andy Stewart, AirBlade Software. Released under the MIT licence.
|
24
124
|
|
25
|
-
1. Fork it
|
26
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
-
5. Create new Pull Request
|
data/Rakefile
CHANGED
data/flippeur.gemspec
CHANGED
@@ -10,10 +10,12 @@ Gem::Specification.new do |gem|
|
|
10
10
|
gem.email = ["boss@airbladesoftware.com"]
|
11
11
|
gem.description = %q{Simple feature flipping.}
|
12
12
|
gem.summary = %q{Simple feature flipping.}
|
13
|
-
gem.homepage = ""
|
13
|
+
gem.homepage = "https://github.com/airblade/flippeur"
|
14
14
|
|
15
15
|
gem.files = `git ls-files`.split($/)
|
16
16
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
17
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
18
|
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_development_dependency 'rake', '~> 10.0.0'
|
19
21
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Flippeur
|
2
|
+
|
3
|
+
module ViewHelpers
|
4
|
+
def feature?(name)
|
5
|
+
available = Flippeur.find(name).available? @controller.current_person
|
6
|
+
block_given? ? (yield if available) : available
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module ControllerHelpers
|
11
|
+
def feature?(name)
|
12
|
+
available = Flippeur.find(name).available? current_person
|
13
|
+
block_given? ? (yield if available) : available
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ModelHelpers
|
18
|
+
def feature?(name, person)
|
19
|
+
available = Flippeur.find(name).available? person
|
20
|
+
block_given? ? (yield if available) : available
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'flippeur/rails_helpers'
|
2
|
+
|
3
|
+
module Flippeur
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
initializer 'flippeur.rails_helpers' do
|
6
|
+
ActionView::Base.send :include, Flippeur::ViewHelpers
|
7
|
+
ActionController::Base.send :include, Flippeur::ControllerHelpers
|
8
|
+
ActiveRecord::Base.send :include, Flippeur::ModelHelpers
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
data/lib/flippeur/version.rb
CHANGED
data/lib/flippeur.rb
CHANGED
@@ -1,5 +1,32 @@
|
|
1
|
+
require 'flippeur/railtie' if defined?(Rails)
|
2
|
+
require 'flippeur/feature'
|
1
3
|
require "flippeur/version"
|
2
4
|
|
3
5
|
module Flippeur
|
4
|
-
|
6
|
+
|
7
|
+
UnknownFeature = Class.new RuntimeError
|
8
|
+
|
9
|
+
def self.setup(&block)
|
10
|
+
reset
|
11
|
+
module_eval &block
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.feature(name, &block)
|
15
|
+
features[name] = Feature.new(name, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.find(name)
|
19
|
+
features[name]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def self.features
|
25
|
+
@features ||= Hash.new { |_,k| raise UnknownFeature, "Unknown feature: #{k}" }
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.reset
|
29
|
+
@features = nil
|
30
|
+
end
|
31
|
+
|
5
32
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'flippeur'
|
3
|
+
require 'flippeur/rails_helpers'
|
4
|
+
|
5
|
+
|
6
|
+
class Controller
|
7
|
+
include Flippeur::ControllerHelpers
|
8
|
+
def current_person
|
9
|
+
Object.new
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
class ControllerHelpersTest < MiniTest::Unit::TestCase
|
15
|
+
|
16
|
+
def setup
|
17
|
+
@controller = Controller.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_unknown_feature_raises_exception
|
21
|
+
Flippeur.setup { }
|
22
|
+
assert_raises Flippeur::UnknownFeature do
|
23
|
+
@controller.feature? :foo
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_available_feature_as_boolean
|
28
|
+
Flippeur.setup do
|
29
|
+
feature(:foo) { |user| true }
|
30
|
+
end
|
31
|
+
assert @controller.feature?(:foo)
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_available_feature_with_block
|
35
|
+
Flippeur.setup do
|
36
|
+
feature(:foo) { |user| true }
|
37
|
+
end
|
38
|
+
block_called = false
|
39
|
+
@controller.feature?(:foo) do
|
40
|
+
block_called = true
|
41
|
+
end
|
42
|
+
assert block_called
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_unavailable_feature_as_boolean
|
46
|
+
Flippeur.setup do
|
47
|
+
feature(:foo) { |user| false }
|
48
|
+
end
|
49
|
+
refute @controller.feature?(:foo)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_unavailable_feature_with_block
|
53
|
+
Flippeur.setup do
|
54
|
+
feature(:foo) { |user| false }
|
55
|
+
end
|
56
|
+
block_called = false
|
57
|
+
@controller.feature?(:foo) do
|
58
|
+
block_called = true
|
59
|
+
end
|
60
|
+
refute block_called
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'flippeur'
|
3
|
+
|
4
|
+
class FeatureTest < MiniTest::Unit::TestCase
|
5
|
+
|
6
|
+
def test_unknown_features_raises_exception
|
7
|
+
Flippeur.setup { }
|
8
|
+
assert_raises Flippeur::UnknownFeature do
|
9
|
+
Flippeur.find :foo
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_finds_known_feature
|
14
|
+
Flippeur.setup do
|
15
|
+
feature(:foo) { }
|
16
|
+
end
|
17
|
+
assert_instance_of Flippeur::Feature, Flippeur.find(:foo)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_available_feature
|
21
|
+
Flippeur.setup do
|
22
|
+
feature(:foo) { |user| user.id == 42 }
|
23
|
+
end
|
24
|
+
assert Flippeur.find(:foo).available?( OpenStruct.new(id: 42) )
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_unavailable_feature
|
28
|
+
Flippeur.setup do
|
29
|
+
feature(:foo) { |user| user.id == 42 }
|
30
|
+
end
|
31
|
+
refute Flippeur.find(:foo).available?( OpenStruct.new(id: 153) )
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'flippeur'
|
3
|
+
require 'flippeur/rails_helpers'
|
4
|
+
|
5
|
+
|
6
|
+
class Model
|
7
|
+
include Flippeur::ModelHelpers
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
class ModelHelpersTest < MiniTest::Unit::TestCase
|
12
|
+
|
13
|
+
def setup
|
14
|
+
@model = Model.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_unknown_feature_raises_exception
|
18
|
+
Flippeur.setup { }
|
19
|
+
assert_raises Flippeur::UnknownFeature do
|
20
|
+
@model.feature? :foo, Object.new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_available_feature_as_boolean
|
25
|
+
Flippeur.setup do
|
26
|
+
feature(:foo) { |user| true }
|
27
|
+
end
|
28
|
+
assert @model.feature?(:foo, Object.new)
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_available_feature_with_block
|
32
|
+
Flippeur.setup do
|
33
|
+
feature(:foo) { |user| true }
|
34
|
+
end
|
35
|
+
block_called = false
|
36
|
+
@model.feature?(:foo, Object.new) do
|
37
|
+
block_called = true
|
38
|
+
end
|
39
|
+
assert block_called
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_unavailable_feature_as_boolean
|
43
|
+
Flippeur.setup do
|
44
|
+
feature(:foo) { |user| false }
|
45
|
+
end
|
46
|
+
refute @model.feature?(:foo, Object.new)
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_unavailable_feature_with_block
|
50
|
+
Flippeur.setup do
|
51
|
+
feature(:foo) { |user| false }
|
52
|
+
end
|
53
|
+
block_called = false
|
54
|
+
@model.feature?(:foo, Object.new) do
|
55
|
+
block_called = true
|
56
|
+
end
|
57
|
+
refute block_called
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'flippeur'
|
3
|
+
require 'flippeur/rails_helpers'
|
4
|
+
|
5
|
+
|
6
|
+
class View
|
7
|
+
include Flippeur::ViewHelpers
|
8
|
+
def initialize
|
9
|
+
@controller = OpenStruct.new(current_person: Object.new)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
class ViewHelpersTest < MiniTest::Unit::TestCase
|
15
|
+
|
16
|
+
def setup
|
17
|
+
@view = View.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_unknown_feature_raises_exception
|
21
|
+
Flippeur.setup { }
|
22
|
+
assert_raises Flippeur::UnknownFeature do
|
23
|
+
@view.feature? :foo
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_available_feature_as_boolean
|
28
|
+
Flippeur.setup do
|
29
|
+
feature(:foo) { |user| true }
|
30
|
+
end
|
31
|
+
assert @view.feature?(:foo)
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_available_feature_with_block
|
35
|
+
Flippeur.setup do
|
36
|
+
feature(:foo) { |user| true }
|
37
|
+
end
|
38
|
+
block_called = false
|
39
|
+
@view.feature?(:foo) do
|
40
|
+
block_called = true
|
41
|
+
end
|
42
|
+
assert block_called
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_unavailable_feature_as_boolean
|
46
|
+
Flippeur.setup do
|
47
|
+
feature(:foo) { |user| false }
|
48
|
+
end
|
49
|
+
refute @view.feature?(:foo)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_unavailable_feature_with_block
|
53
|
+
Flippeur.setup do
|
54
|
+
feature(:foo) { |user| false }
|
55
|
+
end
|
56
|
+
block_called = false
|
57
|
+
@view.feature?(:foo) do
|
58
|
+
block_called = true
|
59
|
+
end
|
60
|
+
refute block_called
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flippeur
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,24 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-05-
|
13
|
-
dependencies:
|
12
|
+
date: 2013-05-27 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 10.0.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: 10.0.0
|
14
30
|
description: Simple feature flipping.
|
15
31
|
email:
|
16
32
|
- boss@airbladesoftware.com
|
@@ -25,8 +41,15 @@ files:
|
|
25
41
|
- Rakefile
|
26
42
|
- flippeur.gemspec
|
27
43
|
- lib/flippeur.rb
|
44
|
+
- lib/flippeur/feature.rb
|
45
|
+
- lib/flippeur/rails_helpers.rb
|
46
|
+
- lib/flippeur/railtie.rb
|
28
47
|
- lib/flippeur/version.rb
|
29
|
-
|
48
|
+
- test/controller_helpers_test.rb
|
49
|
+
- test/feature_test.rb
|
50
|
+
- test/model_helpers_test.rb
|
51
|
+
- test/view_helpers_test.rb
|
52
|
+
homepage: https://github.com/airblade/flippeur
|
30
53
|
licenses: []
|
31
54
|
post_install_message:
|
32
55
|
rdoc_options: []
|
@@ -38,16 +61,26 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
61
|
- - ! '>='
|
39
62
|
- !ruby/object:Gem::Version
|
40
63
|
version: '0'
|
64
|
+
segments:
|
65
|
+
- 0
|
66
|
+
hash: 1803748012281727158
|
41
67
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
42
68
|
none: false
|
43
69
|
requirements:
|
44
70
|
- - ! '>='
|
45
71
|
- !ruby/object:Gem::Version
|
46
72
|
version: '0'
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
hash: 1803748012281727158
|
47
76
|
requirements: []
|
48
77
|
rubyforge_project:
|
49
78
|
rubygems_version: 1.8.23
|
50
79
|
signing_key:
|
51
80
|
specification_version: 3
|
52
81
|
summary: Simple feature flipping.
|
53
|
-
test_files:
|
82
|
+
test_files:
|
83
|
+
- test/controller_helpers_test.rb
|
84
|
+
- test/feature_test.rb
|
85
|
+
- test/model_helpers_test.rb
|
86
|
+
- test/view_helpers_test.rb
|