flippeur 0.0.1 → 1.0.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/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
|