flip_fab 0.0.1
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.
- 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
|