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.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +5 -0
  3. data/Gemfile +16 -0
  4. data/Gemfile.lock +92 -0
  5. data/LICENSE.txt +9 -0
  6. data/README.md +168 -0
  7. data/example/rails_app/.gitignore +17 -0
  8. data/example/rails_app/Gemfile +36 -0
  9. data/example/rails_app/Gemfile.lock +178 -0
  10. data/example/rails_app/README.rdoc +28 -0
  11. data/example/rails_app/Rakefile +6 -0
  12. data/example/rails_app/app/assets/images/.keep +0 -0
  13. data/example/rails_app/app/assets/images/justin_beaver.jpg +0 -0
  14. data/example/rails_app/app/assets/images/regular_beaver.jpg +0 -0
  15. data/example/rails_app/app/assets/javascripts/application.js +16 -0
  16. data/example/rails_app/app/assets/javascripts/beavers.js.coffee +3 -0
  17. data/example/rails_app/app/assets/stylesheets/application.css +15 -0
  18. data/example/rails_app/app/assets/stylesheets/beavers.css.scss +3 -0
  19. data/example/rails_app/app/assets/stylesheets/scaffolds.css.scss +69 -0
  20. data/example/rails_app/app/controllers/application_controller.rb +5 -0
  21. data/example/rails_app/app/controllers/beavers_controller.rb +74 -0
  22. data/example/rails_app/app/controllers/concerns/.keep +0 -0
  23. data/example/rails_app/app/helpers/application_helper.rb +2 -0
  24. data/example/rails_app/app/helpers/beavers_helper.rb +2 -0
  25. data/example/rails_app/app/mailers/.keep +0 -0
  26. data/example/rails_app/app/models/.keep +0 -0
  27. data/example/rails_app/app/models/beaver.rb +2 -0
  28. data/example/rails_app/app/models/concerns/.keep +0 -0
  29. data/example/rails_app/app/views/beavers/_form.html.erb +21 -0
  30. data/example/rails_app/app/views/beavers/edit.html.erb +6 -0
  31. data/example/rails_app/app/views/beavers/index.html.erb +25 -0
  32. data/example/rails_app/app/views/beavers/index.json.jbuilder +4 -0
  33. data/example/rails_app/app/views/beavers/new.html.erb +5 -0
  34. data/example/rails_app/app/views/beavers/show.html.erb +9 -0
  35. data/example/rails_app/app/views/beavers/show.json.jbuilder +1 -0
  36. data/example/rails_app/app/views/layouts/application.html.erb +18 -0
  37. data/example/rails_app/bin/bundle +3 -0
  38. data/example/rails_app/bin/rails +4 -0
  39. data/example/rails_app/bin/rake +4 -0
  40. data/example/rails_app/config/application.rb +25 -0
  41. data/example/rails_app/config/boot.rb +4 -0
  42. data/example/rails_app/config/database.yml +22 -0
  43. data/example/rails_app/config/environment.rb +5 -0
  44. data/example/rails_app/config/environments/development.rb +83 -0
  45. data/example/rails_app/config/environments/test.rb +41 -0
  46. data/example/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  47. data/example/rails_app/config/initializers/cookies_serializer.rb +3 -0
  48. data/example/rails_app/config/initializers/filter_parameter_logging.rb +4 -0
  49. data/example/rails_app/config/initializers/flip_fab.rb +1 -0
  50. data/example/rails_app/config/initializers/inflections.rb +16 -0
  51. data/example/rails_app/config/initializers/mime_types.rb +4 -0
  52. data/example/rails_app/config/initializers/session_store.rb +3 -0
  53. data/example/rails_app/config/initializers/wrap_parameters.rb +14 -0
  54. data/example/rails_app/config/locales/en.yml +23 -0
  55. data/example/rails_app/config/rabbit_feed.yml +8 -0
  56. data/example/rails_app/config/routes.rb +58 -0
  57. data/example/rails_app/config/secrets.yml +18 -0
  58. data/example/rails_app/config/unicorn.rb +4 -0
  59. data/example/rails_app/config.ru +4 -0
  60. data/example/rails_app/db/migrate/20140424102400_create_beavers.rb +9 -0
  61. data/example/rails_app/db/schema.rb +22 -0
  62. data/example/rails_app/db/seeds.rb +7 -0
  63. data/example/rails_app/lib/assets/.keep +0 -0
  64. data/example/rails_app/lib/tasks/.keep +0 -0
  65. data/example/rails_app/log/.keep +0 -0
  66. data/example/rails_app/public/404.html +67 -0
  67. data/example/rails_app/public/422.html +67 -0
  68. data/example/rails_app/public/500.html +66 -0
  69. data/example/rails_app/public/favicon.ico +0 -0
  70. data/example/rails_app/public/robots.txt +5 -0
  71. data/example/rails_app/spec/rails_helper.rb +50 -0
  72. data/example/rails_app/spec/spec_helper.rb +8 -0
  73. data/example/rails_app/test/controllers/.keep +0 -0
  74. data/example/rails_app/test/controllers/beavers_controller_test.rb +49 -0
  75. data/example/rails_app/test/fixtures/.keep +0 -0
  76. data/example/rails_app/test/fixtures/beavers.yml +7 -0
  77. data/example/rails_app/test/helpers/.keep +0 -0
  78. data/example/rails_app/test/helpers/beavers_helper_test.rb +4 -0
  79. data/example/rails_app/test/integration/.keep +0 -0
  80. data/example/rails_app/test/mailers/.keep +0 -0
  81. data/example/rails_app/test/models/.keep +0 -0
  82. data/example/rails_app/test/models/beaver_test.rb +7 -0
  83. data/example/rails_app/test/test_helper.rb +13 -0
  84. data/flip_fab.gemspec +20 -0
  85. data/lib/flip_fab/contextual_feature.rb +83 -0
  86. data/lib/flip_fab/cookie_persistence.rb +52 -0
  87. data/lib/flip_fab/feature.rb +24 -0
  88. data/lib/flip_fab/features_by_name.rb +22 -0
  89. data/lib/flip_fab/helper.rb +8 -0
  90. data/lib/flip_fab/persistence.rb +19 -0
  91. data/lib/flip_fab/version.rb +3 -0
  92. data/lib/flip_fab.rb +24 -0
  93. data/spec/lib/flip_fab/contextual_feature_spec.rb +352 -0
  94. data/spec/lib/flip_fab/cookie_persistence.feature +50 -0
  95. data/spec/lib/flip_fab/cookie_persistence_spec.rb +90 -0
  96. data/spec/lib/flip_fab/feature_spec.rb +86 -0
  97. data/spec/lib/flip_fab/features_by_name_spec.rb +34 -0
  98. data/spec/lib/flip_fab/helper.feature +31 -0
  99. data/spec/lib/flip_fab/helper_spec.rb +90 -0
  100. data/spec/lib/flip_fab/persistence_spec.rb +32 -0
  101. data/spec/lib/flip_fab.feature +10 -0
  102. data/spec/lib/flip_fab_spec.rb +47 -0
  103. data/spec/spec_helper.rb +93 -0
  104. data/spec/support/test_app.rb +16 -0
  105. data/spec/support/test_context.rb +10 -0
  106. data/spec/support/test_multiple_persistence.rb +14 -0
  107. data/spec/support/test_persistence.rb +14 -0
  108. data/spec/support/test_rack_context.rb +15 -0
  109. 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,5 @@
1
+ # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2
+ #
3
+ # To ban all spiders from the entire site uncomment the next two lines:
4
+ # User-agent: *
5
+ # Disallow: /
@@ -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
@@ -0,0 +1,7 @@
1
+ # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2
+
3
+ one:
4
+ name: MyString
5
+
6
+ two:
7
+ name: MyString
File without changes
@@ -0,0 +1,4 @@
1
+ require 'test_helper'
2
+
3
+ class BeaversHelperTest < ActionView::TestCase
4
+ end
File without changes
File without changes
File without changes
@@ -0,0 +1,7 @@
1
+ require 'test_helper'
2
+
3
+ class BeaverTest < ActiveSupport::TestCase
4
+ # test "the truth" do
5
+ # assert true
6
+ # end
7
+ end
@@ -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,8 @@
1
+ module FlipFab
2
+ module Helper
3
+
4
+ def features
5
+ @features ||= FlipFab.features.with_context self
6
+ end
7
+ end
8
+ 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
@@ -0,0 +1,3 @@
1
+ module FlipFab
2
+ VERSION = '0.0.1'
3
+ 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