flipflop 2.2.1 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +5 -13
  2. data/.gitignore +0 -1
  3. data/.travis.yml +23 -27
  4. data/CHANGES.md +7 -0
  5. data/Gemfile +14 -8
  6. data/README.md +27 -1
  7. data/Rakefile +20 -8
  8. data/app/controllers/flipflop/features_controller.rb +20 -10
  9. data/app/controllers/flipflop/strategies_controller.rb +4 -4
  10. data/app/views/flipflop/features/index.html.erb +64 -41
  11. data/app/views/flipflop/stylesheets/_flipflop.css +1 -0
  12. data/app/views/layouts/flipflop.html.erb +8 -1
  13. data/config/features.rb +7 -0
  14. data/config/locales/en.yml +34 -0
  15. data/flipflop.gemspec +0 -5
  16. data/lib/flipflop/configurable.rb +10 -0
  17. data/lib/flipflop/engine.rb +10 -15
  18. data/lib/flipflop/feature_definition.rb +5 -6
  19. data/lib/flipflop/feature_loader.rb +40 -0
  20. data/lib/flipflop/feature_set.rb +7 -9
  21. data/lib/flipflop/group_definition.rb +11 -0
  22. data/lib/flipflop/strategies/abstract_strategy.rb +2 -1
  23. data/lib/flipflop/version.rb +1 -1
  24. data/lib/flipflop.rb +2 -0
  25. data/lib/test_engine/config/features.rb +3 -0
  26. data/lib/test_engine/test_engine.rb +7 -0
  27. data/src/stylesheets/_flipflop.scss +160 -0
  28. data/test/integration/app_test.rb +50 -21
  29. data/test/integration/dashboard_test.rb +395 -52
  30. data/test/templates/nl.yml +30 -0
  31. data/test/templates/test_app_features.rb +7 -0
  32. data/test/templates/test_engine.rb +7 -0
  33. data/test/templates/test_engine_features.rb +3 -0
  34. data/test/test_helper.rb +83 -10
  35. data/test/unit/configurable_test.rb +1 -1
  36. data/test/unit/feature_definition_test.rb +27 -2
  37. data/test/unit/feature_set_test.rb +7 -19
  38. data/test/unit/flipflop_test.rb +16 -11
  39. data/test/unit/group_definition_test.rb +21 -0
  40. data/test/unit/strategies/abstract_strategy_test.rb +14 -25
  41. data/test/unit/strategies/active_record_strategy_test.rb +4 -0
  42. data/test/unit/strategies/cookie_strategy_test.rb +4 -0
  43. data/test/unit/strategies/default_strategy_test.rb +10 -4
  44. data/test/unit/strategies/lambda_strategy_test.rb +11 -27
  45. data/test/unit/strategies/query_string_strategy_test.rb +4 -0
  46. data/test/unit/strategies/redis_strategy_test.rb +4 -0
  47. data/test/unit/strategies/session_strategy_test.rb +4 -0
  48. data/test/unit/strategies/test_strategy_test.rb +4 -0
  49. metadata +22 -9
  50. data/app/assets/stylesheets/flipflop.css +0 -1
@@ -0,0 +1,34 @@
1
+ en:
2
+ flipflop:
3
+ title: "%{application} Features"
4
+ feature: "Feature"
5
+ description: "Description"
6
+
7
+ feature_states:
8
+ enabled: "on"
9
+ disabled: "off"
10
+
11
+ clear: "clear"
12
+
13
+ # You can optionally translate the names of the chosen features, feature
14
+ # groups and strategies:
15
+
16
+ groups:
17
+ default: "Other features"
18
+
19
+ # groups:
20
+ # design_improvements: "Design improvements"
21
+
22
+ # features:
23
+ # world_domination: "World domination"
24
+ # world_domination_description: "Take over the world!"
25
+
26
+ # strategies:
27
+ # cookie: "Cookie"
28
+ # active_record: "Active record"
29
+ # default: "Default"
30
+ # lambda: "Lambda"
31
+ # query_string: "Query string"
32
+ # redis: "Redis"
33
+ # session: "Session"
34
+ # test: "Test"
data/flipflop.gemspec CHANGED
@@ -14,12 +14,7 @@ Gem::Specification.new do |s|
14
14
  s.license = "MIT"
15
15
 
16
16
  s.add_dependency("activesupport", ">= 4.0")
17
- if ENV["CONTINUOUS_INTEGRATION"]
18
- s.files = `git ls-files`.split("\n").map { |f| f.sub(".scss", ".css") }
19
- else
20
- s.add_dependency("bootstrap", "= 4.0.0.alpha3")
21
17
  s.files = `git ls-files`.split("\n")
22
- end
23
18
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
19
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
20
  s.require_paths = ["lib"]
@@ -1,6 +1,16 @@
1
1
  module Flipflop
2
2
  module Configurable
3
+ attr_accessor :current_group
4
+
5
+ def group(group)
6
+ self.current_group = GroupDefinition.new(group)
7
+ yield
8
+ ensure
9
+ self.current_group = nil
10
+ end
11
+
3
12
  def feature(feature, **options)
13
+ options = options.merge(group: current_group)
4
14
  feature = FeatureDefinition.new(feature, **options)
5
15
  FeatureSet.current.add(feature)
6
16
  end
@@ -4,23 +4,24 @@ module Flipflop
4
4
 
5
5
  isolate_namespace Flipflop
6
6
 
7
+ # The following middleware needs to be inserted for this engine, because it
8
+ # may not be available in Rails API only apps.
9
+ middleware.use Rack::MethodOverride
10
+ middleware.use ActionDispatch::Cookies
11
+
7
12
  config.app_middleware.insert_after ActionDispatch::Callbacks,
8
13
  FeatureCache::Middleware
9
14
 
10
15
  config.flipflop = ActiveSupport::OrderedOptions.new
11
16
 
12
- initializer "flipflop.assets" do |app|
13
- config.assets.precompile += ["flipflop.css"]
14
- end
15
-
16
17
  initializer "flipflop.features_path" do |app|
17
- app.paths.add("config/features.rb")
18
+ FeatureLoader.current.append(app)
18
19
  end
19
20
 
20
- initializer "flipflop.features_reloader" do |app|
21
- app.reloaders.push(reloader = feature_reloader(app))
21
+ initializer "flipflop.features_loader" do |app|
22
+ app.reloaders.push(FeatureLoader.current)
22
23
  to_prepare do
23
- reloader.execute
24
+ FeatureLoader.current.execute
24
25
  end
25
26
  end
26
27
 
@@ -37,6 +38,7 @@ module Flipflop
37
38
  initializer "flipflop.request_interceptor" do |app|
38
39
  interceptor = Strategies::AbstractStrategy::RequestInterceptor
39
40
  ActionController::Base.send(:include, interceptor)
41
+ ActionController::API.send(:include, interceptor) if defined?(ActionController::API)
40
42
  end
41
43
 
42
44
  def run_tasks_blocks(app)
@@ -47,13 +49,6 @@ module Flipflop
47
49
 
48
50
  private
49
51
 
50
- def feature_reloader(app)
51
- features = app.paths["config/features.rb"].existent
52
- ActiveSupport::FileUpdateChecker.new(features) do
53
- features.each { |path| load(path) }
54
- end
55
- end
56
-
57
52
  def to_prepare
58
53
  klass = defined?(ActiveSupport::Reloader) ? ActiveSupport::Reloader : ActionDispatch::Reloader
59
54
  klass.to_prepare(&Proc.new)
@@ -1,15 +1,14 @@
1
1
  module Flipflop
2
2
  class FeatureDefinition
3
- attr_reader :key, :default, :description
3
+ attr_reader :key, :name, :title, :description, :default, :group
4
4
 
5
5
  def initialize(key, **options)
6
6
  @key = key
7
+ @name = @key.to_s.freeze
8
+ @title = @name.humanize.freeze
9
+ @description = options.delete(:description).freeze
7
10
  @default = !!options.delete(:default) || false
8
- @description = options.delete(:description) || key.to_s.humanize + "."
9
- end
10
-
11
- def name
12
- key.to_s
11
+ @group = options.delete(:group).freeze
13
12
  end
14
13
  end
15
14
  end
@@ -0,0 +1,40 @@
1
+ module Flipflop
2
+ class FeatureLoader
3
+ @@lock = Monitor.new
4
+
5
+ class << self
6
+ def current
7
+ @current or @@lock.synchronize { @current ||= new }
8
+ end
9
+
10
+ private :new
11
+ end
12
+
13
+ extend Forwardable
14
+ delegate [:execute, :updated?] => :checker
15
+
16
+ def initialize
17
+ @paths = []
18
+ end
19
+
20
+ def append(engine)
21
+ @paths.concat(engine.paths.add("config/features.rb".freeze).existent)
22
+ end
23
+
24
+ private
25
+
26
+ def checker
27
+ @checker or @@lock.synchronize do
28
+ @checker ||= ActiveSupport::FileUpdateChecker.new(@paths) { reload! }
29
+ end
30
+ end
31
+
32
+ def reload!
33
+ @@lock.synchronize do
34
+ Flipflop::FeatureSet.current.replace do
35
+ @paths.each { |path| Kernel.load(path) }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -28,21 +28,19 @@ module Flipflop
28
28
  end
29
29
 
30
30
  def configure
31
- @@lock.synchronize do
32
- initialize
33
- Module.new do
34
- extend Configurable
35
- instance_exec(&Proc.new)
36
- end
37
- @features.freeze
38
- @strategies.freeze
31
+ Module.new do
32
+ extend Configurable
33
+ instance_exec(&Proc.new)
39
34
  end
40
35
  self
41
36
  end
42
37
 
43
- def reset!
38
+ def replace
44
39
  @@lock.synchronize do
45
40
  initialize
41
+ yield if block_given?
42
+ @features.freeze
43
+ @strategies.freeze
46
44
  end
47
45
  self
48
46
  end
@@ -0,0 +1,11 @@
1
+ module Flipflop
2
+ class GroupDefinition
3
+ attr_reader :key, :name, :title
4
+
5
+ def initialize(key)
6
+ @key = key
7
+ @name = @key.to_s.freeze
8
+ @title = @name.humanize.freeze
9
+ end
10
+ end
11
+ end
@@ -35,7 +35,7 @@ module Flipflop
35
35
  end
36
36
  end
37
37
 
38
- attr_reader :key, :name, :description
38
+ attr_reader :key, :name, :title, :description
39
39
 
40
40
  def initialize(**options)
41
41
  # Generate key before setting instance that should be excluded from
@@ -43,6 +43,7 @@ module Flipflop
43
43
  @key = OptionsHasher.new(self).generate
44
44
 
45
45
  @name = (options.delete(:name) || self.class.default_name).freeze
46
+ @title = @name.humanize.freeze
46
47
  @description = (options.delete(:description) || self.class.default_description).freeze
47
48
  @hidden = !!options.delete(:hidden) || false
48
49
 
@@ -1,3 +1,3 @@
1
1
  module Flipflop
2
- VERSION = "2.2.1"
2
+ VERSION = "2.3.0"
3
3
  end
data/lib/flipflop.rb CHANGED
@@ -8,7 +8,9 @@ require "flipflop/configurable"
8
8
  require "flipflop/facade"
9
9
  require "flipflop/feature_cache"
10
10
  require "flipflop/feature_definition"
11
+ require "flipflop/feature_loader"
11
12
  require "flipflop/feature_set"
13
+ require "flipflop/group_definition"
12
14
 
13
15
  require "flipflop/strategies/abstract_strategy"
14
16
  require "flipflop/strategies/options_hasher"
@@ -0,0 +1,3 @@
1
+ Flipflop.configure do
2
+ feature :engine_feature
3
+ end
@@ -0,0 +1,7 @@
1
+ class TestEngine < Rails::Engine
2
+ config.root = "lib/test_engine"
3
+
4
+ initializer "features" do
5
+ Flipflop::FeatureLoader.current.append(self)
6
+ end
7
+ end
@@ -0,0 +1,160 @@
1
+ $enable-transitions: true;
2
+ $grid-gutter-width-base: 0;
3
+
4
+ @import "bootstrap/variables";
5
+ @import "bootstrap/mixins";
6
+ @import "bootstrap/normalize";
7
+ @import "bootstrap/print";
8
+
9
+ @import "bootstrap/reboot";
10
+ @import "bootstrap/type";
11
+ @import "bootstrap/tables";
12
+ @import "bootstrap/buttons";
13
+
14
+ @import "bootstrap/button-group";
15
+ @import "bootstrap/badge";
16
+
17
+ @import "bootstrap/utilities/spacing";
18
+
19
+ section.flipflop {
20
+ @include make-container(); /* @extend .container-fluid; */
21
+ margin: 5rem 0 0;
22
+
23
+ h1 {
24
+ padding: 0 1.4rem;
25
+ }
26
+
27
+ table {
28
+ @extend .table;
29
+
30
+ td:first-child {
31
+ padding-left: 1.5rem;
32
+ }
33
+
34
+ td:last-child {
35
+ padding-right: 1.5rem;
36
+ }
37
+
38
+ thead {
39
+ @extend .thead-inverse;
40
+
41
+ tr {
42
+ th {
43
+ position: relative;
44
+ cursor: default;
45
+
46
+ &[data-tooltip]:before, &[data-tooltip]:after {
47
+ @include transition(all 0.2s ease-out);
48
+ transform: translateY(0.2rem) translateZ(0);
49
+ opacity: 0;
50
+
51
+ display: block;
52
+ position: absolute;
53
+ }
54
+
55
+ &[data-tooltip]:before {
56
+ content: attr(data-tooltip);
57
+ width: 98%;
58
+ left: 0;
59
+ bottom: 3.75rem;
60
+ margin: 0;
61
+ padding: 0.5rem 0.75rem;
62
+ background: $gray;
63
+ border-radius: 0.2rem;
64
+ font-size: 0.875rem;
65
+ font-weight: normal;
66
+ pointer-events: none;
67
+ }
68
+
69
+ &[data-tooltip]:after {
70
+ content: " ";
71
+ width: 0;
72
+ height: 0;
73
+ left: 1rem;
74
+ bottom: 3.25rem;
75
+ border-left: solid transparent 0.5rem;
76
+ border-right: solid transparent 0.5rem;
77
+ border-top: solid $gray 0.5rem;
78
+ }
79
+
80
+ &:hover {
81
+ &[data-tooltip]:before, &[data-tooltip]:after {
82
+ transform: translateY(0) translateZ(0);
83
+ opacity: 1;
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ tbody {
91
+ tr {
92
+ td.status {
93
+ width: 2.2rem;
94
+ font-size: 1.1rem;
95
+
96
+ span.enabled, span.disabled {
97
+ width: 2rem;
98
+ @extend .badge;
99
+ @extend .badge-pill;
100
+ span {
101
+ width: auto;
102
+ text-align: center;
103
+ margin: 0 -2rem;
104
+ }
105
+ }
106
+
107
+ span.enabled { @extend .badge-success; }
108
+ span.disabled { @extend .badge-default; }
109
+ }
110
+
111
+ td.name {
112
+ min-width: 12rem;
113
+ padding-top: 0.9rem;
114
+ font-weight: bold;
115
+ }
116
+
117
+ td.description {
118
+ min-width: 12rem;
119
+ padding-top: 0.9rem;
120
+ }
121
+
122
+ td.toggle {
123
+ min-width: 10rem;
124
+
125
+ div.toolbar {
126
+ @extend .btn-toolbar;
127
+ margin-left: 0;
128
+
129
+ div.group {
130
+ @extend .mr-2;
131
+ @extend .btn-group;
132
+ @extend .btn-group-sm;
133
+
134
+ button {
135
+ @extend .btn;
136
+ @extend .btn-sm;
137
+ cursor: pointer;
138
+ &.active { @extend .btn-primary; }
139
+ &:not(.active) { @extend .btn-secondary; }
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ &.group {
146
+ background-color: $table-bg-accent;
147
+
148
+ td {
149
+ h2 {
150
+ margin: 0;
151
+ line-height: inherit;
152
+ font-size: 1.4rem;
153
+ font-weight: bold;
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
@@ -1,36 +1,65 @@
1
1
  require File.expand_path("../../test_helper", __FILE__)
2
2
 
3
3
  describe Flipflop do
4
- before do
5
- @app = TestApp.new
6
- end
4
+ describe "without engine" do
5
+ before do
6
+ @app = TestApp.new
7
+ end
7
8
 
8
- subject do
9
- @app
10
- end
9
+ after do
10
+ @app.unload!
11
+ end
11
12
 
12
- after do
13
- Flipflop::Strategies::AbstractStrategy::RequestInterceptor.request = nil
14
- end
13
+ subject do
14
+ @app
15
+ end
15
16
 
16
- describe "middleware" do
17
- it "should include cache middleware" do
18
- middlewares = Rails.application.middleware.map(&:klass)
19
- assert_includes middlewares, Flipflop::FeatureCache::Middleware
17
+ describe "middleware" do
18
+ it "should include cache middleware" do
19
+ middlewares = Rails.application.middleware.map(&:klass)
20
+ assert_includes middlewares, Flipflop::FeatureCache::Middleware
21
+ end
22
+ end
23
+
24
+ describe "module" do
25
+ before do
26
+ Flipflop::FeatureSet.current.instance_variable_set(:@features, {})
27
+ Module.new do
28
+ extend Flipflop::Configurable
29
+ feature :world_domination
30
+ end
31
+ end
32
+
33
+ it "should allow querying for app features" do
34
+ assert_equal false, Flipflop.world_domination?
35
+ end
20
36
  end
21
37
  end
22
38
 
23
- describe "module" do
39
+ describe "with engine" do
24
40
  before do
25
- Flipflop::FeatureSet.current.instance_variable_set(:@features, {})
26
- Module.new do
27
- extend Flipflop::Configurable
28
- feature :world_domination
29
- end
41
+ @app = TestApp.new([
42
+ TestFeaturesGenerator,
43
+ TestEngineGenerator,
44
+ ])
45
+ end
46
+
47
+ after do
48
+ @app.unload!
30
49
  end
31
50
 
32
- it "should allow querying for features" do
33
- assert_equal false, Flipflop.world_domination?
51
+ subject do
52
+ @app
53
+ end
54
+
55
+ describe "module" do
56
+ it "should allow querying for app features" do
57
+ assert_equal false, Flipflop.application_feature?
58
+ end
59
+
60
+ it "should allow querying for engine features" do
61
+ assert_equal false, Flipflop.engine_feature?
62
+ end
34
63
  end
35
64
  end
36
65
  end