flipper 0.4.0 → 0.5.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 (59) hide show
  1. data/Guardfile +3 -8
  2. data/README.md +26 -38
  3. data/examples/percentage_of_actors.rb +17 -12
  4. data/examples/percentage_of_random.rb +3 -7
  5. data/lib/flipper.rb +8 -1
  6. data/lib/flipper/adapter.rb +2 -208
  7. data/lib/flipper/adapters/decorator.rb +9 -0
  8. data/lib/flipper/adapters/instrumented.rb +92 -0
  9. data/lib/flipper/adapters/memoizable.rb +88 -0
  10. data/lib/flipper/adapters/memory.rb +89 -7
  11. data/lib/flipper/adapters/operation_logger.rb +31 -45
  12. data/lib/flipper/decorator.rb +6 -0
  13. data/lib/flipper/dsl.rb +29 -2
  14. data/lib/flipper/feature.rb +83 -49
  15. data/lib/flipper/gate.rb +24 -41
  16. data/lib/flipper/gates/actor.rb +24 -24
  17. data/lib/flipper/gates/boolean.rb +28 -15
  18. data/lib/flipper/gates/group.rb +25 -34
  19. data/lib/flipper/gates/percentage_of_actors.rb +21 -13
  20. data/lib/flipper/gates/percentage_of_random.rb +20 -12
  21. data/lib/flipper/instrumentation/log_subscriber.rb +14 -22
  22. data/lib/flipper/middleware/memoizer.rb +23 -0
  23. data/lib/flipper/spec/shared_adapter_specs.rb +141 -92
  24. data/lib/flipper/types/boolean.rb +5 -1
  25. data/lib/flipper/version.rb +1 -1
  26. data/spec/flipper/adapters/instrumented_spec.rb +92 -0
  27. data/spec/flipper/adapters/memoizable_spec.rb +184 -0
  28. data/spec/flipper/adapters/memory_spec.rb +1 -11
  29. data/spec/flipper/adapters/operation_logger_spec.rb +93 -0
  30. data/spec/flipper/dsl_spec.rb +18 -43
  31. data/spec/flipper/feature_spec.rb +25 -9
  32. data/spec/flipper/gate_spec.rb +8 -20
  33. data/spec/flipper/gates/actor_spec.rb +6 -14
  34. data/spec/flipper/gates/boolean_spec.rb +80 -13
  35. data/spec/flipper/gates/group_spec.rb +8 -18
  36. data/spec/flipper/gates/percentage_of_actors_spec.rb +12 -28
  37. data/spec/flipper/gates/percentage_of_random_spec.rb +6 -14
  38. data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -8
  39. data/spec/flipper/instrumentation/metriks_subscriber_spec.rb +3 -6
  40. data/spec/flipper/middleware/{local_cache_spec.rb → memoizer_spec.rb} +25 -55
  41. data/spec/flipper/types/boolean_spec.rb +13 -3
  42. data/spec/flipper_spec.rb +7 -0
  43. data/spec/helper.rb +21 -3
  44. data/spec/integration_spec.rb +115 -116
  45. metadata +17 -27
  46. data/lib/flipper/adapters/memoized.rb +0 -55
  47. data/lib/flipper/key.rb +0 -38
  48. data/lib/flipper/middleware/local_cache.rb +0 -36
  49. data/lib/flipper/toggle.rb +0 -54
  50. data/lib/flipper/toggles/boolean.rb +0 -54
  51. data/lib/flipper/toggles/set.rb +0 -25
  52. data/lib/flipper/toggles/value.rb +0 -25
  53. data/spec/flipper/adapter_spec.rb +0 -463
  54. data/spec/flipper/adapters/memoized_spec.rb +0 -93
  55. data/spec/flipper/key_spec.rb +0 -23
  56. data/spec/flipper/toggle_spec.rb +0 -22
  57. data/spec/flipper/toggles/boolean_spec.rb +0 -40
  58. data/spec/flipper/toggles/set_spec.rb +0 -35
  59. data/spec/flipper/toggles/value_spec.rb +0 -55
@@ -10,7 +10,7 @@ module Flipper
10
10
  # Example Output
11
11
  #
12
12
  # flipper[:search].enabled?(user)
13
- # # Flipper feature(search) enabled? false (1.2ms) [ thing=#<struct flipper_id="1"> ]
13
+ # # Flipper feature(search) enabled? false (1.2ms) [ thing=... ]
14
14
  #
15
15
  # Returns nothing.
16
16
  def feature_operation(event)
@@ -39,36 +39,28 @@ module Flipper
39
39
  # Example Output
40
40
  #
41
41
  # # log output for adapter operation with feature
42
- # # Flipper feature(search) adapter(memory) set_add("search/actors") (0.0ms) [ result=#<Set: {"1"}> value="1" ]
42
+ # # Flipper feature(search) adapter(memory) enable (0.0ms) [ result=...]
43
43
  #
44
44
  # # log output for adapter operation with no feature
45
- # # Flipper adapter(memory) set_add("features") (0.0ms) [ result=#<Set: {"search"}> value="search" ]
45
+ # # Flipper adapter(memory) features (0.0ms) [ result=... ]
46
46
  #
47
47
  # Returns nothing.
48
48
  def adapter_operation(event)
49
49
  return unless logger.debug?
50
50
 
51
+ feature_name = event.payload[:feature_name]
51
52
  adapter_name = event.payload[:adapter_name]
53
+ gate_name = event.payload[:gate_name]
52
54
  operation = event.payload[:operation]
53
55
  result = event.payload[:result]
54
- value = event.payload[:value]
55
- key = event.payload[:key]
56
56
 
57
- feature_description = if key.respond_to?(:feature_name)
58
- "Flipper feature(#{key.feature_name})"
59
- else
60
- "Flipper"
61
- end
57
+ description = "Flipper "
58
+ description << "feature(#{feature_name}) " unless feature_name.nil?
59
+ description << "adapter(#{adapter_name}) "
60
+ description << "#{operation} "
62
61
 
63
- adapter_description = "adapter(#{adapter_name})"
64
- operation_description = "#{operation}(#{key.to_s.inspect})"
65
- description = "#{feature_description} #{adapter_description} #{operation_description}"
66
62
  details = "result=#{result.inspect}"
67
63
 
68
- if event.payload.key?(:value)
69
- details += " value=#{value.inspect}"
70
- end
71
-
72
64
  name = '%s (%.1fms)' % [description, event.duration]
73
65
  debug " #{color(name, CYAN, true)} [ #{details} ]"
74
66
  end
@@ -78,11 +70,11 @@ module Flipper
78
70
  # Example Output
79
71
  #
80
72
  # flipper[:search].enabled?(user)
81
- # # Flipper feature(search) gate(boolean) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
82
- # # Flipper feature(search) gate(group) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
83
- # # Flipper feature(search) gate(actor) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
84
- # # Flipper feature(search) gate(percentage_of_actors) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
85
- # # Flipper feature(search) gate(percentage_of_random) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
73
+ # # Flipper feature(search) gate(boolean) open false (0.1ms) [ thing=... ]
74
+ # # Flipper feature(search) gate(group) open false (0.1ms) [ thing=... ]
75
+ # # Flipper feature(search) gate(actor) open false (0.1ms) [ thing=... ]
76
+ # # Flipper feature(search) gate(percentage_of_actors) open false (0.1ms) [ thing=... ]
77
+ # # Flipper feature(search) gate(percentage_of_random) open false (0.1ms) [ thing=... ]
86
78
  #
87
79
  # Returns nothing.
88
80
  def gate_operation(event)
@@ -0,0 +1,23 @@
1
+ require 'rack/body_proxy'
2
+
3
+ module Flipper
4
+ module Middleware
5
+ class Memoizer
6
+ def initialize(app, flipper)
7
+ @app = app
8
+ @flipper = flipper
9
+ end
10
+
11
+ def call(env)
12
+ original = @flipper.adapter.memoizing?
13
+ @flipper.adapter.memoize = true
14
+
15
+ response = @app.call(env)
16
+ response[2] = Rack::BodyProxy.new(response[2]) {
17
+ @flipper.adapter.memoize = original
18
+ }
19
+ response
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,124 +1,173 @@
1
- require 'set'
1
+ # Requires the following methods:
2
+ # * subject - The instance of the adapter
3
+ shared_examples_for 'a flipper adapter' do
4
+ let(:actor_class) { Struct.new(:flipper_id) }
5
+
6
+ let(:flipper) { Flipper.new(subject) }
7
+ let(:feature) { flipper[:stats] }
8
+
9
+ let(:boolean_gate) { feature.gate(:boolean) }
10
+ let(:group_gate) { feature.gate(:group) }
11
+ let(:actor_gate) { feature.gate(:actor) }
12
+ let(:actors_gate) { feature.gate(:percentage_of_actors) }
13
+ let(:random_gate) { feature.gate(:percentage_of_random) }
2
14
 
3
- shared_examples_for 'a working percentage' do
4
- it "does not raise when used" do
5
- feature.enable percentage
6
- expect { feature.enabled?(actor) }.to_not raise_error
15
+ before do
16
+ Flipper.register(:admins) { |actor|
17
+ actor.respond_to?(:admin?) && actor.admin?
18
+ }
19
+
20
+ Flipper.register(:early_access) { |actor|
21
+ actor.respond_to?(:early_access?) && actor.early_access?
22
+ }
7
23
  end
8
- end
9
24
 
10
- # Requires the following methods
11
- # subject
12
- # read_key(key)
13
- # write_key(key, value)
14
- shared_examples_for 'a flipper adapter' do
15
- let(:key) { Flipper::Key.new(:foo, :bar) }
25
+ after do
26
+ Flipper.unregister_groups
27
+ end
16
28
 
17
- describe "#write" do
18
- it "sets key to value in store" do
19
- subject.write(key, true)
20
- read_key(key).should be_true
21
- end
29
+ it "has name that is a symbol" do
30
+ subject.name.should_not be_nil
31
+ subject.name.should be_instance_of(Symbol)
22
32
  end
23
33
 
24
- describe "#read" do
25
- it "returns nil if key not in store" do
26
- subject.read(key).should be_nil
27
- end
34
+ it "has included the flipper adapter module" do
35
+ subject.class.ancestors.should include(Flipper::Adapter)
36
+ end
28
37
 
29
- it "returns value if key in store" do
30
- write_key key, 'bar'
31
- subject.read(key).should eq('bar')
32
- end
38
+ it "returns correct default values for the gates if none are enabled" do
39
+ subject.get(feature).should eq({
40
+ :boolean => nil,
41
+ :groups => Set.new,
42
+ :actors => Set.new,
43
+ :percentage_of_actors => nil,
44
+ :percentage_of_random => nil,
45
+ })
33
46
  end
34
47
 
35
- describe "#delete" do
36
- it "deletes key" do
37
- write_key key, 'bar'
38
- subject.delete(key)
39
- read_key(key).should be_nil
40
- end
48
+ it "can enable, disable and get value for boolean gate" do
49
+ subject.enable(feature, boolean_gate, flipper.boolean).should be_true
50
+
51
+ result = subject.get(feature)
52
+ result[:boolean].should eq('true')
53
+
54
+ subject.disable(feature, boolean_gate, flipper.boolean(false)).should be_true
55
+
56
+ result = subject.get(feature)
57
+ result[:boolean].should be_nil
41
58
  end
42
59
 
43
- describe "#set_add" do
44
- it "adds value to store" do
45
- subject.set_add(key, '1')
46
- read_key(key).should eq(Set['1'])
47
- end
60
+ it "fully disables all enabled things when boolean gate disabled" do
61
+ actor_22 = actor_class.new('22')
62
+ subject.enable(feature, boolean_gate, flipper.boolean).should be_true
63
+ subject.enable(feature, group_gate, flipper.group(:admins)).should be_true
64
+ subject.enable(feature, actor_gate, flipper.actor(actor_22)).should be_true
65
+ subject.enable(feature, actors_gate, flipper.actors(25)).should be_true
66
+ subject.enable(feature, random_gate, flipper.random(45)).should be_true
67
+
68
+ subject.disable(feature, boolean_gate, flipper.boolean).should be_true
69
+
70
+ subject.get(feature).should eq({
71
+ :boolean => nil,
72
+ :groups => Set.new,
73
+ :actors => Set.new,
74
+ :percentage_of_actors => nil,
75
+ :percentage_of_random => nil,
76
+ })
77
+ end
48
78
 
49
- it "does not add same value more than once" do
50
- subject.set_add(key, '1')
51
- subject.set_add(key, '1')
52
- subject.set_add(key, '1')
53
- subject.set_add(key, '2')
54
- read_key(key).should eq(Set['1', '2'])
55
- end
79
+ it "can enable, disable and get value for group gate" do
80
+ subject.enable(feature, group_gate, flipper.group(:admins)).should be_true
81
+ subject.enable(feature, group_gate, flipper.group(:early_access)).should be_true
82
+
83
+ result = subject.get(feature)
84
+ result[:groups].should eq(Set['admins', 'early_access'])
85
+
86
+ subject.disable(feature, group_gate, flipper.group(:early_access)).should be_true
87
+ result = subject.get(feature)
88
+ result[:groups].should eq(Set['admins'])
89
+
90
+ subject.disable(feature, group_gate, flipper.group(:admins)).should be_true
91
+ result = subject.get(feature)
92
+ result[:groups].should eq(Set.new)
56
93
  end
57
94
 
58
- describe "#set_delete" do
59
- it "removes value from set if key in store" do
60
- write_key key, Set['1', '2']
61
- subject.set_delete(key, '1')
62
- read_key(key).should eq(Set['2'])
63
- end
95
+ it "can enable, disable and get value for actor gate" do
96
+ actor_22 = actor_class.new('22')
97
+ actor_asdf = actor_class.new('asdf')
98
+
99
+ subject.enable(feature, actor_gate, flipper.actor(actor_22)).should be_true
100
+ subject.enable(feature, actor_gate, flipper.actor(actor_asdf)).should be_true
101
+
102
+ result = subject.get(feature)
103
+ result[:actors].should eq(Set['22', 'asdf'])
104
+
105
+ subject.disable(feature, actor_gate, flipper.actor(actor_22)).should be_true
106
+ result = subject.get(feature)
107
+ result[:actors].should eq(Set['asdf'])
64
108
 
65
- it "works fine if key not in store" do
66
- subject.set_delete(key, 'bar')
67
- end
109
+ subject.disable(feature, actor_gate, flipper.actor(actor_asdf)).should be_true
110
+ result = subject.get(feature)
111
+ result[:actors].should eq(Set.new)
68
112
  end
69
113
 
70
- describe "#set_members" do
71
- it "defaults to empty set" do
72
- subject.set_members(key).should eq(Set.new)
73
- end
114
+ it "can enable, disable and get value for percentage of actors gate" do
115
+ subject.enable(feature, actors_gate, flipper.actors(15)).should be_true
116
+ result = subject.get(feature)
117
+ result[:percentage_of_actors].should eq('15')
74
118
 
75
- it "returns set if in store" do
76
- write_key key, Set['1', '2']
77
- subject.set_members(key).should eq(Set['1', '2'])
78
- end
119
+ subject.disable(feature, actors_gate, flipper.actors(0)).should be_true
120
+ result = subject.get(feature)
121
+ result[:percentage_of_actors].should eq('0')
79
122
  end
80
123
 
81
- it "should work with Flipper.new" do
82
- Flipper.new(subject).should_not be_nil
124
+ it "can enable, disable and get value for percentage of random gate" do
125
+ subject.enable(feature, random_gate, flipper.random(10)).should be_true
126
+ result = subject.get(feature)
127
+ result[:percentage_of_random].should eq('10')
128
+
129
+ subject.disable(feature, random_gate, flipper.random(0)).should be_true
130
+ result = subject.get(feature)
131
+ result[:percentage_of_random].should eq('0')
83
132
  end
84
133
 
85
- context "working with values" do
86
- it "always uses strings" do
87
- subject.read(key).should be_nil
88
- subject.write key, true
89
- subject.read(key).should eq('true')
134
+ it "converts boolean value to a string" do
135
+ subject.enable(feature, boolean_gate, flipper.boolean).should be_true
136
+ result = subject.get(feature)
137
+ result[:boolean].should eq('true')
138
+ end
90
139
 
91
- subject.write key, 22
92
- subject.read(key).should eq('22')
140
+ it "converts the actor value to a string" do
141
+ subject.enable(feature, actor_gate, flipper.actor(actor_class.new(22))).should be_true
142
+ result = subject.get(feature)
143
+ result[:actors].should eq(Set['22'])
144
+ end
145
+
146
+ it "converts group value to a string" do
147
+ subject.enable(feature, group_gate, flipper.group(:admins)).should be_true
148
+ result = subject.get(feature)
149
+ result[:groups].should eq(Set['admins'])
150
+ end
93
151
 
94
- subject.delete(key)
95
- subject.read(key).should be_nil
96
- end
152
+ it "converts percentage of random integer value to a string" do
153
+ subject.enable(feature, random_gate, flipper.random(10)).should be_true
154
+ result = subject.get(feature)
155
+ result[:percentage_of_random].should eq('10')
97
156
  end
98
157
 
99
- context "working with sets" do
100
- it "always uses strings" do
101
- subject.set_add key, 1
102
- subject.set_add key, 2
103
- subject.set_members(key).should eq(Set['1', '2'])
104
- subject.set_delete key, 2
105
- subject.set_members(key).should eq(Set['1'])
106
- end
158
+ it "converts percentage of actors integer value to a string" do
159
+ subject.enable(feature, actors_gate, flipper.actors(10)).should be_true
160
+ result = subject.get(feature)
161
+ result[:percentage_of_actors].should eq('10')
107
162
  end
108
163
 
109
- context "integration spot-checks" do
110
- let(:instance) { Flipper.new(subject) }
111
- let(:feature) { instance[:feature] }
112
- let(:actor) { Struct.new(:id).new(1) }
164
+ it "can add and list known features" do
165
+ subject.features.should eq(Set.new)
113
166
 
114
- context "percentage of actors" do
115
- let(:percentage) { instance.actors(10) }
116
- it_should_behave_like 'a working percentage'
117
- end
167
+ subject.add(flipper[:stats]).should be_true
168
+ subject.features.should eq(Set['stats'])
118
169
 
119
- context "random percentage" do
120
- let(:percentage) { instance.random(10) }
121
- it_should_behave_like 'a working percentage'
122
- end
170
+ subject.add(flipper[:search]).should be_true
171
+ subject.features.should eq(Set['stats', 'search'])
123
172
  end
124
173
  end
@@ -1,8 +1,12 @@
1
1
  module Flipper
2
2
  module Types
3
3
  class Boolean < Type
4
+ def initialize(value = nil)
5
+ @value = value.nil? ? true : value
6
+ end
7
+
4
8
  def value
5
- true
9
+ @value
6
10
  end
7
11
  end
8
12
  end
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -0,0 +1,92 @@
1
+ require 'helper'
2
+ require 'flipper/adapters/memory'
3
+ require 'flipper/adapters/instrumented'
4
+ require 'flipper/instrumenters/memory'
5
+ require 'flipper/spec/shared_adapter_specs'
6
+
7
+ describe Flipper::Adapters::Instrumented do
8
+ let(:instrumenter) { Flipper::Instrumenters::Memory.new }
9
+ let(:adapter) { Flipper::Adapters::Memory.new }
10
+ let(:flipper) { Flipper.new(adapter) }
11
+
12
+ let(:feature) { flipper[:stats] }
13
+ let(:gate) { feature.gate(:percentage_of_actors) }
14
+ let(:thing) { flipper.actors(22) }
15
+
16
+ subject {
17
+ described_class.new(adapter, :instrumenter => instrumenter)
18
+ }
19
+
20
+ it_should_behave_like 'a flipper adapter'
21
+
22
+ describe "#get" do
23
+ it "records instrumentation" do
24
+ result = subject.get(feature)
25
+
26
+ event = instrumenter.events.last
27
+ event.should_not be_nil
28
+ event.name.should eq('adapter_operation.flipper')
29
+ event.payload[:operation].should eq(:get)
30
+ event.payload[:adapter_name].should eq(:memory)
31
+ event.payload[:feature_name].should eq(:stats)
32
+ event.payload[:result].should be(result)
33
+ end
34
+ end
35
+
36
+ describe "#enable" do
37
+ it "records instrumentation" do
38
+ result = subject.enable(feature, gate, thing)
39
+
40
+ event = instrumenter.events.last
41
+ event.should_not be_nil
42
+ event.name.should eq('adapter_operation.flipper')
43
+ event.payload[:operation].should eq(:enable)
44
+ event.payload[:adapter_name].should eq(:memory)
45
+ event.payload[:feature_name].should eq(:stats)
46
+ event.payload[:gate_name].should eq(:percentage_of_actors)
47
+ event.payload[:result].should be(result)
48
+ end
49
+ end
50
+
51
+ describe "#disable" do
52
+ it "records instrumentation" do
53
+ result = subject.disable(feature, gate, thing)
54
+
55
+ event = instrumenter.events.last
56
+ event.should_not be_nil
57
+ event.name.should eq('adapter_operation.flipper')
58
+ event.payload[:operation].should eq(:disable)
59
+ event.payload[:adapter_name].should eq(:memory)
60
+ event.payload[:feature_name].should eq(:stats)
61
+ event.payload[:gate_name].should eq(:percentage_of_actors)
62
+ event.payload[:result].should be(result)
63
+ end
64
+ end
65
+
66
+ describe "#add" do
67
+ it "records instrumentation" do
68
+ result = subject.add(feature)
69
+
70
+ event = instrumenter.events.last
71
+ event.should_not be_nil
72
+ event.name.should eq('adapter_operation.flipper')
73
+ event.payload[:operation].should eq(:add)
74
+ event.payload[:adapter_name].should eq(:memory)
75
+ event.payload[:feature_name].should eq(:stats)
76
+ event.payload[:result].should be(result)
77
+ end
78
+ end
79
+
80
+ describe "#features" do
81
+ it "records instrumentation" do
82
+ result = subject.features
83
+
84
+ event = instrumenter.events.last
85
+ event.should_not be_nil
86
+ event.name.should eq('adapter_operation.flipper')
87
+ event.payload[:operation].should eq(:features)
88
+ event.payload[:adapter_name].should eq(:memory)
89
+ event.payload[:result].should be(result)
90
+ end
91
+ end
92
+ end