flipper 0.1.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 (52) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +20 -0
  3. data/Guardfile +18 -0
  4. data/LICENSE +22 -0
  5. data/README.md +182 -0
  6. data/Rakefile +7 -0
  7. data/examples/basic.rb +27 -0
  8. data/examples/dsl.rb +85 -0
  9. data/examples/example_setup.rb +8 -0
  10. data/examples/group.rb +36 -0
  11. data/examples/individual_actor.rb +29 -0
  12. data/examples/percentage_of_actors.rb +35 -0
  13. data/examples/percentage_of_random.rb +33 -0
  14. data/flipper.gemspec +17 -0
  15. data/lib/flipper.rb +33 -0
  16. data/lib/flipper/adapters/memory.rb +35 -0
  17. data/lib/flipper/dsl.rb +61 -0
  18. data/lib/flipper/errors.rb +11 -0
  19. data/lib/flipper/feature.rb +49 -0
  20. data/lib/flipper/gate.rb +55 -0
  21. data/lib/flipper/gates/actor.rb +29 -0
  22. data/lib/flipper/gates/boolean.rb +29 -0
  23. data/lib/flipper/gates/group.rb +32 -0
  24. data/lib/flipper/gates/percentage_of_actors.rb +25 -0
  25. data/lib/flipper/gates/percentage_of_random.rb +25 -0
  26. data/lib/flipper/registry.rb +36 -0
  27. data/lib/flipper/spec/shared_adapter_specs.rb +78 -0
  28. data/lib/flipper/toggle.rb +31 -0
  29. data/lib/flipper/toggles/boolean.rb +22 -0
  30. data/lib/flipper/toggles/set.rb +17 -0
  31. data/lib/flipper/toggles/value.rb +17 -0
  32. data/lib/flipper/type.rb +18 -0
  33. data/lib/flipper/types/actor.rb +17 -0
  34. data/lib/flipper/types/boolean.rb +13 -0
  35. data/lib/flipper/types/group.rb +22 -0
  36. data/lib/flipper/types/percentage.rb +19 -0
  37. data/lib/flipper/types/percentage_of_actors.rb +6 -0
  38. data/lib/flipper/types/percentage_of_random.rb +6 -0
  39. data/lib/flipper/version.rb +3 -0
  40. data/spec/flipper/adapters/memory_spec.rb +19 -0
  41. data/spec/flipper/dsl_spec.rb +185 -0
  42. data/spec/flipper/feature_spec.rb +401 -0
  43. data/spec/flipper/registry_spec.rb +71 -0
  44. data/spec/flipper/types/actor_spec.rb +23 -0
  45. data/spec/flipper/types/boolean_spec.rb +9 -0
  46. data/spec/flipper/types/group_spec.rb +32 -0
  47. data/spec/flipper/types/percentage_of_actors_spec.rb +6 -0
  48. data/spec/flipper/types/percentage_of_random_spec.rb +6 -0
  49. data/spec/flipper/types/percentage_spec.rb +6 -0
  50. data/spec/flipper_spec.rb +59 -0
  51. data/spec/helper.rb +47 -0
  52. metadata +114 -0
@@ -0,0 +1,25 @@
1
+ module Flipper
2
+ module Gates
3
+ class PercentageOfRandom < Gate
4
+ Key = :perc_time
5
+
6
+ def type_key
7
+ Key
8
+ end
9
+
10
+ def open?(actor)
11
+ percentage = toggle.value
12
+
13
+ if percentage.nil?
14
+ false
15
+ else
16
+ rand < (percentage / 100.0)
17
+ end
18
+ end
19
+
20
+ def protects?(thing)
21
+ thing.is_a?(Flipper::Types::PercentageOfRandom)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ module Flipper
2
+ class Registry
3
+ class Error < StandardError; end
4
+ class DuplicateKey < Error; end
5
+ class MissingKey < Error; end
6
+
7
+ def initialize(source = {})
8
+ @mutex = Mutex.new
9
+ @source = source
10
+ end
11
+
12
+ def add(key, value)
13
+ @mutex.synchronize do
14
+ if @source[key]
15
+ raise DuplicateKey, "#{key} is already registered"
16
+ else
17
+ @source[key] = value
18
+ end
19
+ end
20
+ end
21
+
22
+ def get(key)
23
+ @mutex.synchronize do
24
+ @source[key]
25
+ end
26
+ end
27
+
28
+ def each(&block)
29
+ @mutex.synchronize { @source.dup }.each(&block)
30
+ end
31
+
32
+ def clear
33
+ @mutex.synchronize { @source.clear }
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,78 @@
1
+ require 'set'
2
+
3
+ # Requires the following methods
4
+ # subject
5
+ # read_key(key)
6
+ # write_key(key, value)
7
+ shared_examples_for 'a flipper adapter' do
8
+ describe "#write" do
9
+ let(:separator) { Flipper::Gate::Separator }
10
+
11
+ it "sets key to value in store" do
12
+ subject.write('foo', true)
13
+ read_key('foo').should be_true
14
+ end
15
+
16
+ it "works with separator" do
17
+ subject.write("foo#{separator}bar", true)
18
+ read_key("foo#{separator}bar").should be_true
19
+ end
20
+ end
21
+
22
+ describe "#read" do
23
+ it "returns nil if key not in store" do
24
+ subject.read('foo').should be_nil
25
+ end
26
+
27
+ it "returns value if key in store" do
28
+ write_key 'foo', 'bar'
29
+ subject.read('foo').should eq('bar')
30
+ end
31
+ end
32
+
33
+ describe "#delete" do
34
+ it "deletes key" do
35
+ write_key 'foo', 'bar'
36
+ subject.delete('foo')
37
+ read_key('foo').should be_nil
38
+ end
39
+ end
40
+
41
+ describe "#set_add" do
42
+ it "adds value to store" do
43
+ subject.set_add('foo', 1)
44
+ read_key('foo').should eq(Set[1])
45
+ end
46
+
47
+ it "does not add same value more than once" do
48
+ subject.set_add('foo', 1)
49
+ subject.set_add('foo', 1)
50
+ subject.set_add('foo', 1)
51
+ subject.set_add('foo', 2)
52
+ read_key('foo').should eq(Set[1, 2])
53
+ end
54
+ end
55
+
56
+ describe "#set_delete" do
57
+ it "removes value from set if key in store" do
58
+ write_key 'foo', Set[1, 2]
59
+ subject.set_delete('foo', 1)
60
+ read_key('foo').should eq(Set[2])
61
+ end
62
+
63
+ it "works fine if key not in store" do
64
+ subject.set_delete('foo', 'bar')
65
+ end
66
+ end
67
+
68
+ describe "#set_members" do
69
+ it "defaults to empty set" do
70
+ subject.set_members('foo').should eq(Set.new)
71
+ end
72
+
73
+ it "returns set if in store" do
74
+ write_key 'foo', Set[1, 2]
75
+ subject.set_members('foo').should eq(Set[1, 2])
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,31 @@
1
+ require 'forwardable'
2
+
3
+ module Flipper
4
+ class Toggle
5
+ extend Forwardable
6
+
7
+ attr_reader :gate
8
+
9
+ def_delegators :@gate, :key, :feature, :adapter
10
+
11
+ def initialize(gate)
12
+ @gate = gate
13
+ end
14
+
15
+ def enable(thing)
16
+ raise 'Not implemented'
17
+ end
18
+
19
+ def disable(thing)
20
+ raise 'Not implemented'
21
+ end
22
+
23
+ def value
24
+ raise 'Not implemented'
25
+ end
26
+ end
27
+ end
28
+
29
+ require 'flipper/toggles/boolean'
30
+ require 'flipper/toggles/set'
31
+ require 'flipper/toggles/value'
@@ -0,0 +1,22 @@
1
+ module Flipper
2
+ module Toggles
3
+ class Boolean < Toggle
4
+ def enable(thing)
5
+ adapter.write key, thing.enabled_value
6
+ end
7
+
8
+ def disable(thing)
9
+ adapter.delete key
10
+
11
+ adapter.delete "#{gate.key_prefix}#{Gate::Separator}#{Gates::Actor::Key}"
12
+ adapter.delete "#{gate.key_prefix}#{Gate::Separator}#{Gates::Group::Key}"
13
+ adapter.delete "#{gate.key_prefix}#{Gate::Separator}#{Gates::PercentageOfActors::Key}"
14
+ adapter.delete "#{gate.key_prefix}#{Gate::Separator}#{Gates::PercentageOfRandom::Key}"
15
+ end
16
+
17
+ def value
18
+ adapter.read key
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module Flipper
2
+ module Toggles
3
+ class Set < Toggle
4
+ def enable(thing)
5
+ adapter.set_add key, thing.enabled_value
6
+ end
7
+
8
+ def disable(thing)
9
+ adapter.set_delete key, thing.disabled_value
10
+ end
11
+
12
+ def value
13
+ adapter.set_members key
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Flipper
2
+ module Toggles
3
+ class Value < Toggle
4
+ def enable(thing)
5
+ adapter.write key, thing.enabled_value
6
+ end
7
+
8
+ def disable(thing)
9
+ adapter.write key, thing.disabled_value
10
+ end
11
+
12
+ def value
13
+ adapter.read key
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ module Flipper
2
+ class Type
3
+ def enabled_value
4
+ raise 'Not implemented'
5
+ end
6
+
7
+ def disabled_value
8
+ raise 'Not implemented'
9
+ end
10
+ end
11
+ end
12
+
13
+ require 'flipper/types/actor'
14
+ require 'flipper/types/boolean'
15
+ require 'flipper/types/group'
16
+ require 'flipper/types/percentage'
17
+ require 'flipper/types/percentage_of_actors'
18
+ require 'flipper/types/percentage_of_random'
@@ -0,0 +1,17 @@
1
+ module Flipper
2
+ module Types
3
+ class Actor < Type
4
+ attr_reader :identifier
5
+
6
+ def initialize(identifier)
7
+ @identifier = identifier.to_i
8
+ end
9
+
10
+ def enabled_value
11
+ @identifier
12
+ end
13
+
14
+ alias_method :disabled_value, :enabled_value
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ module Flipper
2
+ module Types
3
+ class Boolean < Type
4
+ def enabled_value
5
+ true
6
+ end
7
+
8
+ def disabled_value
9
+ false
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ module Flipper
2
+ module Types
3
+ class Group < Type
4
+ attr_reader :name
5
+
6
+ def initialize(name, &block)
7
+ @name = name.to_sym
8
+ @block = block
9
+ end
10
+
11
+ def match?(*args)
12
+ @block.call(*args) == true
13
+ end
14
+
15
+ def enabled_value
16
+ @name
17
+ end
18
+
19
+ alias_method :disabled_value, :enabled_value
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ module Flipper
2
+ module Types
3
+ class Percentage < Type
4
+ attr_reader :value
5
+
6
+ def initialize(value)
7
+ @value = value.to_i
8
+ end
9
+
10
+ def enabled_value
11
+ value
12
+ end
13
+
14
+ def disabled_value
15
+ 0
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,6 @@
1
+ module Flipper
2
+ module Types
3
+ class PercentageOfActors < Percentage
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Flipper
2
+ module Types
3
+ class PercentageOfRandom < Percentage
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module Flipper
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,19 @@
1
+ require 'helper'
2
+ require 'flipper/adapters/memory'
3
+ require 'flipper/spec/shared_adapter_specs'
4
+
5
+ describe Flipper::Adapters::Memory do
6
+ let(:source) { {} }
7
+
8
+ subject { Flipper::Adapters::Memory.new(source) }
9
+
10
+ def read_key(key)
11
+ source[key]
12
+ end
13
+
14
+ def write_key(key, value)
15
+ source[key] = value
16
+ end
17
+
18
+ it_should_behave_like 'a flipper adapter'
19
+ end
@@ -0,0 +1,185 @@
1
+ require 'helper'
2
+ require 'flipper/dsl'
3
+
4
+ describe Flipper::DSL do
5
+ subject { Flipper::DSL.new(adapter) }
6
+
7
+ let(:source) { {} }
8
+ let(:adapter) { Flipper::Adapters::Memory.new(source) }
9
+
10
+ let(:admins_feature) { feature(:admins) }
11
+
12
+ def feature(name)
13
+ Flipper::Feature.new(name, adapter)
14
+ end
15
+
16
+ describe "#enabled?" do
17
+ before do
18
+ subject.stub(:feature => admins_feature)
19
+ end
20
+
21
+ it "passes arguments to feature enabled check and returns result" do
22
+ admins_feature.should_receive(:enabled?).with(:foo).and_return(true)
23
+ subject.should_receive(:feature).with(:stats).and_return(admins_feature)
24
+ subject.enabled?(:stats, :foo).should be_true
25
+ end
26
+ end
27
+
28
+ describe "#disabled?" do
29
+ it "passes all args to enabled? and returns the opposite" do
30
+ subject.should_receive(:enabled?).with(:stats, :foo).and_return(true)
31
+ subject.disabled?(:stats, :foo).should be_false
32
+ end
33
+ end
34
+
35
+ describe "#enable" do
36
+ before do
37
+ subject.stub(:feature => admins_feature)
38
+ end
39
+
40
+ it "calls enable for feature with arguments" do
41
+ admins_feature.should_receive(:enable).with(:foo)
42
+ subject.should_receive(:feature).with(:stats).and_return(admins_feature)
43
+ subject.enable :stats, :foo
44
+ end
45
+ end
46
+
47
+ describe "#disable" do
48
+ before do
49
+ subject.stub(:feature => admins_feature)
50
+ end
51
+
52
+ it "calls disable for feature with arguments" do
53
+ admins_feature.should_receive(:disable).with(:foo)
54
+ subject.should_receive(:feature).with(:stats).and_return(admins_feature)
55
+ subject.disable :stats, :foo
56
+ end
57
+ end
58
+
59
+ describe "#feature" do
60
+ before do
61
+ @result = subject.feature(:stats)
62
+ end
63
+
64
+ it "returns instance of feature with correct name and adapter" do
65
+ @result.should be_instance_of(Flipper::Feature)
66
+ @result.name.should eq(:stats)
67
+ @result.adapter.should eq(adapter)
68
+ end
69
+
70
+ it "memoizes the feature" do
71
+ subject.feature(:stats).should equal(@result)
72
+ end
73
+ end
74
+
75
+ describe "#[]" do
76
+ before do
77
+ @result = subject[:stats]
78
+ end
79
+
80
+ it "returns instance of feature with correct name and adapter" do
81
+ @result.should be_instance_of(Flipper::Feature)
82
+ @result.name.should eq(:stats)
83
+ @result.adapter.should eq(adapter)
84
+ end
85
+
86
+ it "memoizes the feature" do
87
+ subject[:stats].should equal(@result)
88
+ end
89
+ end
90
+
91
+ describe "#group" do
92
+ context "for registered group" do
93
+ before do
94
+ @group = Flipper.register(:admins) { }
95
+ end
96
+
97
+ it "returns group" do
98
+ subject.group(:admins).should eq(@group)
99
+ end
100
+
101
+ it "always returns same instance for same name" do
102
+ subject.group(:admins).should equal(subject.group(:admins))
103
+ end
104
+ end
105
+
106
+ context "for unregistered group" do
107
+ it "returns nil" do
108
+ subject.group(:admins).should be_nil
109
+ end
110
+ end
111
+ end
112
+
113
+ describe "#actor" do
114
+ context "for something that responds to id" do
115
+ it "returns actor instance with identifier set to id" do
116
+ user = Struct.new(:id).new(23)
117
+ actor = subject.actor(user)
118
+ actor.should be_instance_of(Flipper::Types::Actor)
119
+ actor.identifier.should eq(23)
120
+ end
121
+ end
122
+
123
+ context "for something that responds to identifier" do
124
+ it "returns actor instance with identifier set to id" do
125
+ user = Struct.new(:identifier).new(45)
126
+ actor = subject.actor(user)
127
+ actor.should be_instance_of(Flipper::Types::Actor)
128
+ actor.identifier.should eq(45)
129
+ end
130
+ end
131
+
132
+ context "for something that responds to identifier and id" do
133
+ it "returns actor instance with identifier set to identifier" do
134
+ user = Struct.new(:id, :identifier).new(1, 50)
135
+ actor = subject.actor(user)
136
+ actor.should be_instance_of(Flipper::Types::Actor)
137
+ actor.identifier.should eq(50)
138
+ end
139
+ end
140
+
141
+ context "for a number" do
142
+ it "returns actor instance with identifer set to number" do
143
+ actor = subject.actor(33)
144
+ actor.should be_instance_of(Flipper::Types::Actor)
145
+ actor.identifier.should eq(33)
146
+ end
147
+ end
148
+
149
+ context "for nil" do
150
+ it "raises error" do
151
+ expect {
152
+ subject.actor(nil)
153
+ }.to raise_error(ArgumentError)
154
+ end
155
+ end
156
+ end
157
+
158
+ describe "#random" do
159
+ before do
160
+ @result = subject.random(5)
161
+ end
162
+
163
+ it "returns percentage of random" do
164
+ @result.should be_instance_of(Flipper::Types::PercentageOfRandom)
165
+ end
166
+
167
+ it "sets value" do
168
+ @result.value.should eq(5)
169
+ end
170
+ end
171
+
172
+ describe "#actors" do
173
+ before do
174
+ @result = subject.actors(17)
175
+ end
176
+
177
+ it "returns percentage of actors" do
178
+ @result.should be_instance_of(Flipper::Types::PercentageOfActors)
179
+ end
180
+
181
+ it "sets value" do
182
+ @result.value.should eq(17)
183
+ end
184
+ end
185
+ end