flipflop 2.2.1 → 2.3.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.
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