flip_fab 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +5 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +92 -0
- data/LICENSE.txt +9 -0
- data/README.md +168 -0
- data/example/rails_app/.gitignore +17 -0
- data/example/rails_app/Gemfile +36 -0
- data/example/rails_app/Gemfile.lock +178 -0
- data/example/rails_app/README.rdoc +28 -0
- data/example/rails_app/Rakefile +6 -0
- data/example/rails_app/app/assets/images/.keep +0 -0
- data/example/rails_app/app/assets/images/justin_beaver.jpg +0 -0
- data/example/rails_app/app/assets/images/regular_beaver.jpg +0 -0
- data/example/rails_app/app/assets/javascripts/application.js +16 -0
- data/example/rails_app/app/assets/javascripts/beavers.js.coffee +3 -0
- data/example/rails_app/app/assets/stylesheets/application.css +15 -0
- data/example/rails_app/app/assets/stylesheets/beavers.css.scss +3 -0
- data/example/rails_app/app/assets/stylesheets/scaffolds.css.scss +69 -0
- data/example/rails_app/app/controllers/application_controller.rb +5 -0
- data/example/rails_app/app/controllers/beavers_controller.rb +74 -0
- data/example/rails_app/app/controllers/concerns/.keep +0 -0
- data/example/rails_app/app/helpers/application_helper.rb +2 -0
- data/example/rails_app/app/helpers/beavers_helper.rb +2 -0
- data/example/rails_app/app/mailers/.keep +0 -0
- data/example/rails_app/app/models/.keep +0 -0
- data/example/rails_app/app/models/beaver.rb +2 -0
- data/example/rails_app/app/models/concerns/.keep +0 -0
- data/example/rails_app/app/views/beavers/_form.html.erb +21 -0
- data/example/rails_app/app/views/beavers/edit.html.erb +6 -0
- data/example/rails_app/app/views/beavers/index.html.erb +25 -0
- data/example/rails_app/app/views/beavers/index.json.jbuilder +4 -0
- data/example/rails_app/app/views/beavers/new.html.erb +5 -0
- data/example/rails_app/app/views/beavers/show.html.erb +9 -0
- data/example/rails_app/app/views/beavers/show.json.jbuilder +1 -0
- data/example/rails_app/app/views/layouts/application.html.erb +18 -0
- data/example/rails_app/bin/bundle +3 -0
- data/example/rails_app/bin/rails +4 -0
- data/example/rails_app/bin/rake +4 -0
- data/example/rails_app/config/application.rb +25 -0
- data/example/rails_app/config/boot.rb +4 -0
- data/example/rails_app/config/database.yml +22 -0
- data/example/rails_app/config/environment.rb +5 -0
- data/example/rails_app/config/environments/development.rb +83 -0
- data/example/rails_app/config/environments/test.rb +41 -0
- data/example/rails_app/config/initializers/backtrace_silencers.rb +7 -0
- data/example/rails_app/config/initializers/cookies_serializer.rb +3 -0
- data/example/rails_app/config/initializers/filter_parameter_logging.rb +4 -0
- data/example/rails_app/config/initializers/flip_fab.rb +1 -0
- data/example/rails_app/config/initializers/inflections.rb +16 -0
- data/example/rails_app/config/initializers/mime_types.rb +4 -0
- data/example/rails_app/config/initializers/session_store.rb +3 -0
- data/example/rails_app/config/initializers/wrap_parameters.rb +14 -0
- data/example/rails_app/config/locales/en.yml +23 -0
- data/example/rails_app/config/rabbit_feed.yml +8 -0
- data/example/rails_app/config/routes.rb +58 -0
- data/example/rails_app/config/secrets.yml +18 -0
- data/example/rails_app/config/unicorn.rb +4 -0
- data/example/rails_app/config.ru +4 -0
- data/example/rails_app/db/migrate/20140424102400_create_beavers.rb +9 -0
- data/example/rails_app/db/schema.rb +22 -0
- data/example/rails_app/db/seeds.rb +7 -0
- data/example/rails_app/lib/assets/.keep +0 -0
- data/example/rails_app/lib/tasks/.keep +0 -0
- data/example/rails_app/log/.keep +0 -0
- data/example/rails_app/public/404.html +67 -0
- data/example/rails_app/public/422.html +67 -0
- data/example/rails_app/public/500.html +66 -0
- data/example/rails_app/public/favicon.ico +0 -0
- data/example/rails_app/public/robots.txt +5 -0
- data/example/rails_app/spec/rails_helper.rb +50 -0
- data/example/rails_app/spec/spec_helper.rb +8 -0
- data/example/rails_app/test/controllers/.keep +0 -0
- data/example/rails_app/test/controllers/beavers_controller_test.rb +49 -0
- data/example/rails_app/test/fixtures/.keep +0 -0
- data/example/rails_app/test/fixtures/beavers.yml +7 -0
- data/example/rails_app/test/helpers/.keep +0 -0
- data/example/rails_app/test/helpers/beavers_helper_test.rb +4 -0
- data/example/rails_app/test/integration/.keep +0 -0
- data/example/rails_app/test/mailers/.keep +0 -0
- data/example/rails_app/test/models/.keep +0 -0
- data/example/rails_app/test/models/beaver_test.rb +7 -0
- data/example/rails_app/test/test_helper.rb +13 -0
- data/flip_fab.gemspec +20 -0
- data/lib/flip_fab/contextual_feature.rb +83 -0
- data/lib/flip_fab/cookie_persistence.rb +52 -0
- data/lib/flip_fab/feature.rb +24 -0
- data/lib/flip_fab/features_by_name.rb +22 -0
- data/lib/flip_fab/helper.rb +8 -0
- data/lib/flip_fab/persistence.rb +19 -0
- data/lib/flip_fab/version.rb +3 -0
- data/lib/flip_fab.rb +24 -0
- data/spec/lib/flip_fab/contextual_feature_spec.rb +352 -0
- data/spec/lib/flip_fab/cookie_persistence.feature +50 -0
- data/spec/lib/flip_fab/cookie_persistence_spec.rb +90 -0
- data/spec/lib/flip_fab/feature_spec.rb +86 -0
- data/spec/lib/flip_fab/features_by_name_spec.rb +34 -0
- data/spec/lib/flip_fab/helper.feature +31 -0
- data/spec/lib/flip_fab/helper_spec.rb +90 -0
- data/spec/lib/flip_fab/persistence_spec.rb +32 -0
- data/spec/lib/flip_fab.feature +10 -0
- data/spec/lib/flip_fab_spec.rb +47 -0
- data/spec/spec_helper.rb +93 -0
- data/spec/support/test_app.rb +16 -0
- data/spec/support/test_context.rb +10 -0
- data/spec/support/test_multiple_persistence.rb +14 -0
- data/spec/support/test_persistence.rb +14 -0
- data/spec/support/test_rack_context.rb +15 -0
- metadata +168 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>The page you were looking for doesn't exist (404)</title>
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
6
|
+
<style>
|
7
|
+
body {
|
8
|
+
background-color: #EFEFEF;
|
9
|
+
color: #2E2F30;
|
10
|
+
text-align: center;
|
11
|
+
font-family: arial, sans-serif;
|
12
|
+
margin: 0;
|
13
|
+
}
|
14
|
+
|
15
|
+
div.dialog {
|
16
|
+
width: 95%;
|
17
|
+
max-width: 33em;
|
18
|
+
margin: 4em auto 0;
|
19
|
+
}
|
20
|
+
|
21
|
+
div.dialog > div {
|
22
|
+
border: 1px solid #CCC;
|
23
|
+
border-right-color: #999;
|
24
|
+
border-left-color: #999;
|
25
|
+
border-bottom-color: #BBB;
|
26
|
+
border-top: #B00100 solid 4px;
|
27
|
+
border-top-left-radius: 9px;
|
28
|
+
border-top-right-radius: 9px;
|
29
|
+
background-color: white;
|
30
|
+
padding: 7px 12% 0;
|
31
|
+
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
32
|
+
}
|
33
|
+
|
34
|
+
h1 {
|
35
|
+
font-size: 100%;
|
36
|
+
color: #730E15;
|
37
|
+
line-height: 1.5em;
|
38
|
+
}
|
39
|
+
|
40
|
+
div.dialog > p {
|
41
|
+
margin: 0 0 1em;
|
42
|
+
padding: 1em;
|
43
|
+
background-color: #F7F7F7;
|
44
|
+
border: 1px solid #CCC;
|
45
|
+
border-right-color: #999;
|
46
|
+
border-left-color: #999;
|
47
|
+
border-bottom-color: #999;
|
48
|
+
border-bottom-left-radius: 4px;
|
49
|
+
border-bottom-right-radius: 4px;
|
50
|
+
border-top-color: #DADADA;
|
51
|
+
color: #666;
|
52
|
+
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
53
|
+
}
|
54
|
+
</style>
|
55
|
+
</head>
|
56
|
+
|
57
|
+
<body>
|
58
|
+
<!-- This file lives in public/404.html -->
|
59
|
+
<div class="dialog">
|
60
|
+
<div>
|
61
|
+
<h1>The page you were looking for doesn't exist.</h1>
|
62
|
+
<p>You may have mistyped the address or the page may have moved.</p>
|
63
|
+
</div>
|
64
|
+
<p>If you are the application owner check the logs for more information.</p>
|
65
|
+
</div>
|
66
|
+
</body>
|
67
|
+
</html>
|
@@ -0,0 +1,67 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>The change you wanted was rejected (422)</title>
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
6
|
+
<style>
|
7
|
+
body {
|
8
|
+
background-color: #EFEFEF;
|
9
|
+
color: #2E2F30;
|
10
|
+
text-align: center;
|
11
|
+
font-family: arial, sans-serif;
|
12
|
+
margin: 0;
|
13
|
+
}
|
14
|
+
|
15
|
+
div.dialog {
|
16
|
+
width: 95%;
|
17
|
+
max-width: 33em;
|
18
|
+
margin: 4em auto 0;
|
19
|
+
}
|
20
|
+
|
21
|
+
div.dialog > div {
|
22
|
+
border: 1px solid #CCC;
|
23
|
+
border-right-color: #999;
|
24
|
+
border-left-color: #999;
|
25
|
+
border-bottom-color: #BBB;
|
26
|
+
border-top: #B00100 solid 4px;
|
27
|
+
border-top-left-radius: 9px;
|
28
|
+
border-top-right-radius: 9px;
|
29
|
+
background-color: white;
|
30
|
+
padding: 7px 12% 0;
|
31
|
+
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
32
|
+
}
|
33
|
+
|
34
|
+
h1 {
|
35
|
+
font-size: 100%;
|
36
|
+
color: #730E15;
|
37
|
+
line-height: 1.5em;
|
38
|
+
}
|
39
|
+
|
40
|
+
div.dialog > p {
|
41
|
+
margin: 0 0 1em;
|
42
|
+
padding: 1em;
|
43
|
+
background-color: #F7F7F7;
|
44
|
+
border: 1px solid #CCC;
|
45
|
+
border-right-color: #999;
|
46
|
+
border-left-color: #999;
|
47
|
+
border-bottom-color: #999;
|
48
|
+
border-bottom-left-radius: 4px;
|
49
|
+
border-bottom-right-radius: 4px;
|
50
|
+
border-top-color: #DADADA;
|
51
|
+
color: #666;
|
52
|
+
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
53
|
+
}
|
54
|
+
</style>
|
55
|
+
</head>
|
56
|
+
|
57
|
+
<body>
|
58
|
+
<!-- This file lives in public/422.html -->
|
59
|
+
<div class="dialog">
|
60
|
+
<div>
|
61
|
+
<h1>The change you wanted was rejected.</h1>
|
62
|
+
<p>Maybe you tried to change something you didn't have access to.</p>
|
63
|
+
</div>
|
64
|
+
<p>If you are the application owner check the logs for more information.</p>
|
65
|
+
</div>
|
66
|
+
</body>
|
67
|
+
</html>
|
@@ -0,0 +1,66 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>We're sorry, but something went wrong (500)</title>
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
6
|
+
<style>
|
7
|
+
body {
|
8
|
+
background-color: #EFEFEF;
|
9
|
+
color: #2E2F30;
|
10
|
+
text-align: center;
|
11
|
+
font-family: arial, sans-serif;
|
12
|
+
margin: 0;
|
13
|
+
}
|
14
|
+
|
15
|
+
div.dialog {
|
16
|
+
width: 95%;
|
17
|
+
max-width: 33em;
|
18
|
+
margin: 4em auto 0;
|
19
|
+
}
|
20
|
+
|
21
|
+
div.dialog > div {
|
22
|
+
border: 1px solid #CCC;
|
23
|
+
border-right-color: #999;
|
24
|
+
border-left-color: #999;
|
25
|
+
border-bottom-color: #BBB;
|
26
|
+
border-top: #B00100 solid 4px;
|
27
|
+
border-top-left-radius: 9px;
|
28
|
+
border-top-right-radius: 9px;
|
29
|
+
background-color: white;
|
30
|
+
padding: 7px 12% 0;
|
31
|
+
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
32
|
+
}
|
33
|
+
|
34
|
+
h1 {
|
35
|
+
font-size: 100%;
|
36
|
+
color: #730E15;
|
37
|
+
line-height: 1.5em;
|
38
|
+
}
|
39
|
+
|
40
|
+
div.dialog > p {
|
41
|
+
margin: 0 0 1em;
|
42
|
+
padding: 1em;
|
43
|
+
background-color: #F7F7F7;
|
44
|
+
border: 1px solid #CCC;
|
45
|
+
border-right-color: #999;
|
46
|
+
border-left-color: #999;
|
47
|
+
border-bottom-color: #999;
|
48
|
+
border-bottom-left-radius: 4px;
|
49
|
+
border-bottom-right-radius: 4px;
|
50
|
+
border-top-color: #DADADA;
|
51
|
+
color: #666;
|
52
|
+
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
53
|
+
}
|
54
|
+
</style>
|
55
|
+
</head>
|
56
|
+
|
57
|
+
<body>
|
58
|
+
<!-- This file lives in public/500.html -->
|
59
|
+
<div class="dialog">
|
60
|
+
<div>
|
61
|
+
<h1>We're sorry, but something went wrong.</h1>
|
62
|
+
</div>
|
63
|
+
<p>If you are the application owner check the logs for more information.</p>
|
64
|
+
</div>
|
65
|
+
</body>
|
66
|
+
</html>
|
File without changes
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# This file is copied to spec/ when you run 'rails generate rspec:install'
|
2
|
+
ENV["RAILS_ENV"] ||= 'test'
|
3
|
+
require 'spec_helper'
|
4
|
+
require File.expand_path("../../config/environment", __FILE__)
|
5
|
+
require 'rspec/rails'
|
6
|
+
# Add additional requires below this line. Rails is not loaded until this point!
|
7
|
+
|
8
|
+
# Requires supporting ruby files with custom matchers and macros, etc, in
|
9
|
+
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
|
10
|
+
# run as spec files by default. This means that files in spec/support that end
|
11
|
+
# in _spec.rb will both be required and run as specs, causing the specs to be
|
12
|
+
# run twice. It is recommended that you do not name files matching this glob to
|
13
|
+
# end with _spec.rb. You can configure this pattern with the --pattern
|
14
|
+
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
|
15
|
+
#
|
16
|
+
# The following line is provided for convenience purposes. It has the downside
|
17
|
+
# of increasing the boot-up time by auto-requiring all files in the support
|
18
|
+
# directory. Alternatively, in the individual `*_spec.rb` files, manually
|
19
|
+
# require only the support files necessary.
|
20
|
+
#
|
21
|
+
# Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
|
22
|
+
|
23
|
+
# Checks for pending migrations before tests are run.
|
24
|
+
# If you are not using ActiveRecord, you can remove this line.
|
25
|
+
ActiveRecord::Migration.maintain_test_schema!
|
26
|
+
|
27
|
+
RSpec.configure do |config|
|
28
|
+
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
|
29
|
+
config.fixture_path = "#{::Rails.root}/spec/fixtures"
|
30
|
+
|
31
|
+
# If you're not using ActiveRecord, or you'd prefer not to run each of your
|
32
|
+
# examples within a transaction, remove the following line or assign false
|
33
|
+
# instead of true.
|
34
|
+
config.use_transactional_fixtures = true
|
35
|
+
|
36
|
+
# RSpec Rails can automatically mix in different behaviours to your tests
|
37
|
+
# based on their file location, for example enabling you to call `get` and
|
38
|
+
# `post` in specs under `spec/controllers`.
|
39
|
+
#
|
40
|
+
# You can disable this behaviour by removing the line below, and instead
|
41
|
+
# explicitly tag your specs with their type, e.g.:
|
42
|
+
#
|
43
|
+
# RSpec.describe UsersController, :type => :controller do
|
44
|
+
# # ...
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# The different available types are documented in the features, such as in
|
48
|
+
# https://relishapp.com/rspec/rspec-rails/docs
|
49
|
+
config.infer_spec_type_from_file_location!
|
50
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
RSpec.configure do |config|
|
2
|
+
|
3
|
+
# Run specs in random order to surface order dependencies. If you find an
|
4
|
+
# order dependency and want to debug it, you can fix the order by providing
|
5
|
+
# the seed, which is printed after each run.
|
6
|
+
# --seed 1234
|
7
|
+
config.order = 'random'
|
8
|
+
end
|
File without changes
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class BeaversControllerTest < ActionController::TestCase
|
4
|
+
setup do
|
5
|
+
@beaver = beavers(:one)
|
6
|
+
end
|
7
|
+
|
8
|
+
test "should get index" do
|
9
|
+
get :index
|
10
|
+
assert_response :success
|
11
|
+
assert_not_nil assigns(:beavers)
|
12
|
+
end
|
13
|
+
|
14
|
+
test "should get new" do
|
15
|
+
get :new
|
16
|
+
assert_response :success
|
17
|
+
end
|
18
|
+
|
19
|
+
test "should create beaver" do
|
20
|
+
assert_difference('Beaver.count') do
|
21
|
+
post :create, beaver: { name: @beaver.name }
|
22
|
+
end
|
23
|
+
|
24
|
+
assert_redirected_to beaver_path(assigns(:beaver))
|
25
|
+
end
|
26
|
+
|
27
|
+
test "should show beaver" do
|
28
|
+
get :show, id: @beaver
|
29
|
+
assert_response :success
|
30
|
+
end
|
31
|
+
|
32
|
+
test "should get edit" do
|
33
|
+
get :edit, id: @beaver
|
34
|
+
assert_response :success
|
35
|
+
end
|
36
|
+
|
37
|
+
test "should update beaver" do
|
38
|
+
patch :update, id: @beaver, beaver: { name: @beaver.name }
|
39
|
+
assert_redirected_to beaver_path(assigns(:beaver))
|
40
|
+
end
|
41
|
+
|
42
|
+
test "should destroy beaver" do
|
43
|
+
assert_difference('Beaver.count', -1) do
|
44
|
+
delete :destroy, id: @beaver
|
45
|
+
end
|
46
|
+
|
47
|
+
assert_redirected_to beavers_path
|
48
|
+
end
|
49
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,13 @@
|
|
1
|
+
ENV['RAILS_ENV'] ||= 'test'
|
2
|
+
require File.expand_path('../../config/environment', __FILE__)
|
3
|
+
require 'rails/test_help'
|
4
|
+
|
5
|
+
class ActiveSupport::TestCase
|
6
|
+
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
|
7
|
+
#
|
8
|
+
# Note: You'll currently still have to declare fixtures explicitly in integration tests
|
9
|
+
# -- they do not yet inherit this setting
|
10
|
+
fixtures :all
|
11
|
+
|
12
|
+
# Add more helper methods to be used by all tests here...
|
13
|
+
end
|
data/flip_fab.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'flip_fab/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'flip_fab'
|
8
|
+
spec.version = FlipFab::VERSION
|
9
|
+
spec.authors = ['Simply Business']
|
10
|
+
spec.email = ['tech@simplybusiness.co.uk']
|
11
|
+
spec.description = %q{A gem providing persistent, per-user feature flipping to Rack applications.}
|
12
|
+
spec.summary = %q{A gem providing persistent, per-user feature flipping to Rack applications.}
|
13
|
+
spec.homepage = 'https://github.com/simplybusiness/flip_fab'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module FlipFab
|
2
|
+
class ContextualFeature
|
3
|
+
attr_reader :feature, :context
|
4
|
+
|
5
|
+
def initialize feature, context
|
6
|
+
@feature = feature
|
7
|
+
@context = context
|
8
|
+
if overridden?
|
9
|
+
@state = override
|
10
|
+
persist
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def enabled?
|
15
|
+
state == :enabled
|
16
|
+
end
|
17
|
+
|
18
|
+
def disabled?
|
19
|
+
!enabled?
|
20
|
+
end
|
21
|
+
|
22
|
+
def enable
|
23
|
+
self.state = :enabled
|
24
|
+
end
|
25
|
+
|
26
|
+
def disable
|
27
|
+
self.state = :disabled
|
28
|
+
end
|
29
|
+
|
30
|
+
def state= value
|
31
|
+
raise "Invalid state provided: `#{value}`, possible states are :enabled, :disabled" unless %i(enabled disabled).include? value
|
32
|
+
unless overridden?
|
33
|
+
@state = value
|
34
|
+
persist
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def persist
|
39
|
+
persistence_adapters.each{ |adapter| adapter.write state }
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def persistence_adapters
|
45
|
+
@persistence_adapters ||= feature.persistence_adapters.map { |adapter_class| adapter_class.new feature.name, context }
|
46
|
+
end
|
47
|
+
|
48
|
+
def state
|
49
|
+
@state ||= if state_in_context?
|
50
|
+
state_from_context
|
51
|
+
else
|
52
|
+
default_state
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def state_in_context?
|
57
|
+
!first_adapter_with_state.nil?
|
58
|
+
end
|
59
|
+
|
60
|
+
def state_from_context
|
61
|
+
first_adapter_with_state.read
|
62
|
+
end
|
63
|
+
|
64
|
+
def first_adapter_with_state
|
65
|
+
persistence_adapters.detect{|adapter| !adapter.read.nil?}
|
66
|
+
end
|
67
|
+
|
68
|
+
def default_state
|
69
|
+
feature.default
|
70
|
+
end
|
71
|
+
|
72
|
+
def override
|
73
|
+
return unless context.respond_to? :params
|
74
|
+
override = context.params[feature.name.to_s]
|
75
|
+
return unless %w(enabled disabled).include? override
|
76
|
+
override.to_sym
|
77
|
+
end
|
78
|
+
|
79
|
+
def overridden?
|
80
|
+
!override.nil?
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module FlipFab
|
2
|
+
class CookiePersistence < FlipFab::Persistence
|
3
|
+
|
4
|
+
COOKIE_PATH = '/'
|
5
|
+
COOKIE_DURATION_MONTHS = 12
|
6
|
+
# See: https://github.com/rails/rails/blob/b1124a2ac88778c0feb0157ac09367cbd204bf01/actionpack/lib/action_dispatch/middleware/cookies.rb#L214
|
7
|
+
DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
|
8
|
+
|
9
|
+
def initialize feature_name, context
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def read
|
14
|
+
value.to_sym unless value.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
def write state
|
18
|
+
cookie_domain = ".#{top_level_domain}" unless top_level_domain.nil?
|
19
|
+
context.response.set_cookie key, {
|
20
|
+
value: state,
|
21
|
+
expires: cookie_expiration,
|
22
|
+
domain: cookie_domain,
|
23
|
+
path: COOKIE_PATH,
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def key
|
30
|
+
"flip_fab.#{feature_name}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def value
|
34
|
+
@value ||= context.request.cookies[key]
|
35
|
+
end
|
36
|
+
|
37
|
+
# See: https://github.com/rails/rails/blob/b1124a2ac88778c0feb0157ac09367cbd204bf01/actionpack/lib/action_dispatch/middleware/cookies.rb#L286-L294
|
38
|
+
def top_level_domain
|
39
|
+
if (host !~ /^[\d.]+$/) && (host =~ DOMAIN_REGEXP)
|
40
|
+
$&
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def cookie_expiration
|
45
|
+
(Time.now.utc.to_datetime >> COOKIE_DURATION_MONTHS).to_time
|
46
|
+
end
|
47
|
+
|
48
|
+
def host
|
49
|
+
context.request.host
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module FlipFab
|
2
|
+
class Feature
|
3
|
+
|
4
|
+
attr_reader :name, :default, :persistence_adapters
|
5
|
+
|
6
|
+
def initialize name, options={}
|
7
|
+
@name = name
|
8
|
+
@default = options[:default] || :disabled
|
9
|
+
@persistence_adapters = options[:persistence_adapters] || [CookiePersistence]
|
10
|
+
end
|
11
|
+
|
12
|
+
def enabled?
|
13
|
+
default == :enabled
|
14
|
+
end
|
15
|
+
|
16
|
+
def disabled?
|
17
|
+
!enabled?
|
18
|
+
end
|
19
|
+
|
20
|
+
def with_context context
|
21
|
+
ContextualFeature.new self, context
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module FlipFab
|
4
|
+
class FeaturesByName
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def initialize features_by_name={}
|
8
|
+
@features_by_name = features_by_name
|
9
|
+
end
|
10
|
+
|
11
|
+
def [] name
|
12
|
+
raise "no feature has been defined with the name: #{name}" if @features_by_name[name].nil?
|
13
|
+
@features_by_name[name]
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_context context
|
17
|
+
FeaturesByName.new Hash[@features_by_name.map{|name, feature| [name, (feature.with_context context)]}]
|
18
|
+
end
|
19
|
+
|
20
|
+
def_delegators :@features_by_name, :[]=, :clear, :count
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module FlipFab
|
2
|
+
class Persistence
|
3
|
+
|
4
|
+
attr_reader :feature_name, :context
|
5
|
+
|
6
|
+
def initialize feature_name, context
|
7
|
+
@feature_name = feature_name
|
8
|
+
@context = context
|
9
|
+
end
|
10
|
+
|
11
|
+
def read
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def write state
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/flip_fab.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'flip_fab/contextual_feature'
|
2
|
+
require 'flip_fab/features_by_name'
|
3
|
+
require 'flip_fab/feature'
|
4
|
+
require 'flip_fab/helper'
|
5
|
+
require 'flip_fab/persistence'
|
6
|
+
require 'flip_fab/cookie_persistence'
|
7
|
+
|
8
|
+
module FlipFab
|
9
|
+
extend self
|
10
|
+
|
11
|
+
attr_reader :features
|
12
|
+
|
13
|
+
def define_feature name, options={}
|
14
|
+
@features ||= {}
|
15
|
+
@features[name] = Feature.new name, options
|
16
|
+
end
|
17
|
+
|
18
|
+
@features ||= FeaturesByName.new
|
19
|
+
end
|
20
|
+
|
21
|
+
if defined?(ActionController)
|
22
|
+
ActionController::Base.send :include, FlipFab::Helper
|
23
|
+
ActionController::Base.helper FlipFab::Helper
|
24
|
+
end
|