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.
- data/.gitignore +18 -0
- data/Gemfile +20 -0
- data/Guardfile +18 -0
- data/LICENSE +22 -0
- data/README.md +182 -0
- data/Rakefile +7 -0
- data/examples/basic.rb +27 -0
- data/examples/dsl.rb +85 -0
- data/examples/example_setup.rb +8 -0
- data/examples/group.rb +36 -0
- data/examples/individual_actor.rb +29 -0
- data/examples/percentage_of_actors.rb +35 -0
- data/examples/percentage_of_random.rb +33 -0
- data/flipper.gemspec +17 -0
- data/lib/flipper.rb +33 -0
- data/lib/flipper/adapters/memory.rb +35 -0
- data/lib/flipper/dsl.rb +61 -0
- data/lib/flipper/errors.rb +11 -0
- data/lib/flipper/feature.rb +49 -0
- data/lib/flipper/gate.rb +55 -0
- data/lib/flipper/gates/actor.rb +29 -0
- data/lib/flipper/gates/boolean.rb +29 -0
- data/lib/flipper/gates/group.rb +32 -0
- data/lib/flipper/gates/percentage_of_actors.rb +25 -0
- data/lib/flipper/gates/percentage_of_random.rb +25 -0
- data/lib/flipper/registry.rb +36 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +78 -0
- data/lib/flipper/toggle.rb +31 -0
- data/lib/flipper/toggles/boolean.rb +22 -0
- data/lib/flipper/toggles/set.rb +17 -0
- data/lib/flipper/toggles/value.rb +17 -0
- data/lib/flipper/type.rb +18 -0
- data/lib/flipper/types/actor.rb +17 -0
- data/lib/flipper/types/boolean.rb +13 -0
- data/lib/flipper/types/group.rb +22 -0
- data/lib/flipper/types/percentage.rb +19 -0
- data/lib/flipper/types/percentage_of_actors.rb +6 -0
- data/lib/flipper/types/percentage_of_random.rb +6 -0
- data/lib/flipper/version.rb +3 -0
- data/spec/flipper/adapters/memory_spec.rb +19 -0
- data/spec/flipper/dsl_spec.rb +185 -0
- data/spec/flipper/feature_spec.rb +401 -0
- data/spec/flipper/registry_spec.rb +71 -0
- data/spec/flipper/types/actor_spec.rb +23 -0
- data/spec/flipper/types/boolean_spec.rb +9 -0
- data/spec/flipper/types/group_spec.rb +32 -0
- data/spec/flipper/types/percentage_of_actors_spec.rb +6 -0
- data/spec/flipper/types/percentage_of_random_spec.rb +6 -0
- data/spec/flipper/types/percentage_spec.rb +6 -0
- data/spec/flipper_spec.rb +59 -0
- data/spec/helper.rb +47 -0
- metadata +114 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
require './example_setup'
|
2
|
+
|
3
|
+
require 'flipper'
|
4
|
+
require 'flipper/adapters/memory'
|
5
|
+
|
6
|
+
adapter = Flipper::Adapters::Memory.new
|
7
|
+
flipper = Flipper.new(adapter)
|
8
|
+
stats = flipper[:stats]
|
9
|
+
|
10
|
+
# Some class that represents what will be trying to do something
|
11
|
+
class User
|
12
|
+
attr_reader :id
|
13
|
+
|
14
|
+
def initialize(id)
|
15
|
+
@id = id
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
pitt = User.new(1)
|
20
|
+
clooney = User.new(10)
|
21
|
+
|
22
|
+
puts "Stats for pitt: #{stats.enabled?(flipper.actor(pitt))}"
|
23
|
+
puts "Stats for clooney: #{stats.enabled?(flipper.actor(clooney))}"
|
24
|
+
|
25
|
+
puts "\nEnabling stats for 5 percent...\n\n"
|
26
|
+
stats.enable(Flipper::Types::PercentageOfActors.new(5))
|
27
|
+
|
28
|
+
puts "Stats for pitt: #{stats.enabled?(flipper.actor(pitt))}"
|
29
|
+
puts "Stats for clooney: #{stats.enabled?(flipper.actor(clooney))}"
|
30
|
+
|
31
|
+
puts "\nEnabling stats for 15 percent...\n\n"
|
32
|
+
stats.enable(Flipper::Types::PercentageOfActors.new(15))
|
33
|
+
|
34
|
+
puts "Stats for pitt: #{stats.enabled?(flipper.actor(pitt))}"
|
35
|
+
puts "Stats for clooney: #{stats.enabled?(flipper.actor(clooney))}"
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require './example_setup'
|
2
|
+
|
3
|
+
require 'flipper'
|
4
|
+
require 'flipper/adapters/memory'
|
5
|
+
|
6
|
+
adapter = Flipper::Adapters::Memory.new
|
7
|
+
flipper = Flipper.new(adapter)
|
8
|
+
logging = flipper[:logging]
|
9
|
+
|
10
|
+
perform_test = lambda do |number|
|
11
|
+
logging.enable flipper.random(number)
|
12
|
+
|
13
|
+
total = 1_000
|
14
|
+
enabled = []
|
15
|
+
disabled = []
|
16
|
+
|
17
|
+
(1..total).each do |number|
|
18
|
+
if logging.enabled?
|
19
|
+
enabled << number
|
20
|
+
else
|
21
|
+
disabled << number
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
actual = (enabled.size / total.to_f * 100).round(2)
|
26
|
+
|
27
|
+
# puts "#{enabled.size} / #{total}"
|
28
|
+
puts "percentage: #{actual} vs #{number}"
|
29
|
+
end
|
30
|
+
|
31
|
+
[1, 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100].each do |number|
|
32
|
+
perform_test.call number
|
33
|
+
end
|
data/flipper.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/flipper/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["John Nunemaker"]
|
6
|
+
gem.email = ["nunemaker@gmail.com"]
|
7
|
+
gem.description = %q{Feature flipper for any adapter}
|
8
|
+
gem.summary = %q{Feature flipper for any adapter}
|
9
|
+
gem.homepage = "http://jnunemaker.github.com/flipper"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "flipper"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Flipper::VERSION
|
17
|
+
end
|
data/lib/flipper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'flipper/dsl'
|
2
|
+
require 'flipper/errors'
|
3
|
+
require 'flipper/feature'
|
4
|
+
require 'flipper/gate'
|
5
|
+
require 'flipper/registry'
|
6
|
+
require 'flipper/toggle'
|
7
|
+
require 'flipper/type'
|
8
|
+
|
9
|
+
module Flipper
|
10
|
+
def self.new(*args)
|
11
|
+
DSL.new(*args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.groups
|
15
|
+
@groups ||= Registry.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.groups=(registry)
|
19
|
+
@groups = registry
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.register(name, &block)
|
23
|
+
group = Types::Group.new(name, &block)
|
24
|
+
groups.add(group.name, group)
|
25
|
+
group
|
26
|
+
rescue Registry::DuplicateKey
|
27
|
+
raise DuplicateGroup, %Q{Group named "#{name}" is already registered}
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.group(name)
|
31
|
+
groups.get(name)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
module Adapters
|
5
|
+
class Memory
|
6
|
+
def initialize(source = nil)
|
7
|
+
@source = source || {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def read(key)
|
11
|
+
@source[key]
|
12
|
+
end
|
13
|
+
|
14
|
+
def write(key, value)
|
15
|
+
@source[key] = value
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete(key)
|
19
|
+
@source.delete(key)
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_add(key, value)
|
23
|
+
set_members(key).add(value)
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_delete(key, value)
|
27
|
+
set_members(key).delete(value)
|
28
|
+
end
|
29
|
+
|
30
|
+
def set_members(key)
|
31
|
+
@source[key] ||= Set.new
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/flipper/dsl.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
module Flipper
|
2
|
+
class DSL
|
3
|
+
def initialize(adapter)
|
4
|
+
@adapter = adapter
|
5
|
+
end
|
6
|
+
|
7
|
+
def enabled?(name, *args)
|
8
|
+
feature(name).enabled?(*args)
|
9
|
+
end
|
10
|
+
|
11
|
+
def disabled?(name, *args)
|
12
|
+
!enabled?(name, *args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def enable(name, *args)
|
16
|
+
feature(name).enable(*args)
|
17
|
+
end
|
18
|
+
|
19
|
+
def disable(name, *args)
|
20
|
+
feature(name).disable(*args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def feature(name)
|
24
|
+
features[name.to_sym] ||= Flipper::Feature.new(name, @adapter)
|
25
|
+
end
|
26
|
+
|
27
|
+
alias :[] :feature
|
28
|
+
|
29
|
+
def group(name)
|
30
|
+
Flipper.group(name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def actor(actor_or_number)
|
34
|
+
raise ArgumentError, "actor cannot be nil" if actor_or_number.nil?
|
35
|
+
|
36
|
+
identifier = if actor_or_number.respond_to?(:identifier)
|
37
|
+
actor_or_number.identifier
|
38
|
+
elsif actor_or_number.respond_to?(:id)
|
39
|
+
actor_or_number.id
|
40
|
+
else
|
41
|
+
actor_or_number
|
42
|
+
end
|
43
|
+
|
44
|
+
Flipper::Types::Actor.new(identifier)
|
45
|
+
end
|
46
|
+
|
47
|
+
def random(number)
|
48
|
+
Flipper::Types::PercentageOfRandom.new(number)
|
49
|
+
end
|
50
|
+
|
51
|
+
def actors(number)
|
52
|
+
Flipper::Types::PercentageOfActors.new(number)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def features
|
58
|
+
@features ||= {}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'flipper/errors'
|
2
|
+
require 'flipper/type'
|
3
|
+
require 'flipper/toggle'
|
4
|
+
require 'flipper/gate'
|
5
|
+
|
6
|
+
module Flipper
|
7
|
+
class Feature
|
8
|
+
attr_reader :name
|
9
|
+
attr_reader :adapter
|
10
|
+
|
11
|
+
def initialize(name, adapter)
|
12
|
+
@name = name
|
13
|
+
@adapter = adapter
|
14
|
+
end
|
15
|
+
|
16
|
+
def enable(thing = Types::Boolean.new)
|
17
|
+
gate_for(thing).enable(thing)
|
18
|
+
end
|
19
|
+
|
20
|
+
def disable(thing = Types::Boolean.new)
|
21
|
+
gate_for(thing).disable(thing)
|
22
|
+
end
|
23
|
+
|
24
|
+
def enabled?(actor = nil)
|
25
|
+
!! catch(:short_circuit) { gates.detect { |gate| gate.open?(actor) } }
|
26
|
+
end
|
27
|
+
|
28
|
+
def disabled?(actor = nil)
|
29
|
+
!enabled?(actor)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def gate_for(thing)
|
35
|
+
gates.detect { |gate| gate.protects?(thing) } ||
|
36
|
+
raise(GateNotFound.new(thing))
|
37
|
+
end
|
38
|
+
|
39
|
+
def gates
|
40
|
+
@gates ||= [
|
41
|
+
Gates::Boolean.new(self),
|
42
|
+
Gates::Group.new(self),
|
43
|
+
Gates::Actor.new(self),
|
44
|
+
Gates::PercentageOfActors.new(self),
|
45
|
+
Gates::PercentageOfRandom.new(self),
|
46
|
+
]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/flipper/gate.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
class Gate
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
Separator = '/'
|
8
|
+
|
9
|
+
attr_reader :feature
|
10
|
+
|
11
|
+
def_delegator :@feature, :adapter
|
12
|
+
|
13
|
+
def initialize(feature)
|
14
|
+
@feature = feature
|
15
|
+
end
|
16
|
+
|
17
|
+
def key_prefix
|
18
|
+
@feature.name
|
19
|
+
end
|
20
|
+
|
21
|
+
def key
|
22
|
+
"#{key_prefix}#{Separator}#{type_key}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def toggle_class
|
26
|
+
Toggles::Value
|
27
|
+
end
|
28
|
+
|
29
|
+
def toggle
|
30
|
+
@toggle ||= toggle_class.new(self)
|
31
|
+
end
|
32
|
+
|
33
|
+
def protects?(thing)
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
def match?(actor)
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
def enable(thing)
|
42
|
+
toggle.enable(thing)
|
43
|
+
end
|
44
|
+
|
45
|
+
def disable(thing)
|
46
|
+
toggle.disable(thing)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
require 'flipper/gates/actor'
|
52
|
+
require 'flipper/gates/boolean'
|
53
|
+
require 'flipper/gates/group'
|
54
|
+
require 'flipper/gates/percentage_of_actors'
|
55
|
+
require 'flipper/gates/percentage_of_random'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Gates
|
3
|
+
class Actor < Gate
|
4
|
+
Key = :actors
|
5
|
+
|
6
|
+
def type_key
|
7
|
+
Key
|
8
|
+
end
|
9
|
+
|
10
|
+
def toggle_class
|
11
|
+
Toggles::Set
|
12
|
+
end
|
13
|
+
|
14
|
+
def open?(actor)
|
15
|
+
if actor && actor.respond_to?(:identifier)
|
16
|
+
identifiers.include?(actor.identifier)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def identifiers
|
21
|
+
toggle.value
|
22
|
+
end
|
23
|
+
|
24
|
+
def protects?(thing)
|
25
|
+
thing.is_a?(Flipper::Types::Actor)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Gates
|
3
|
+
class Boolean < Gate
|
4
|
+
Key = :boolean
|
5
|
+
|
6
|
+
def type_key
|
7
|
+
Key
|
8
|
+
end
|
9
|
+
|
10
|
+
def toggle_class
|
11
|
+
Toggles::Boolean
|
12
|
+
end
|
13
|
+
|
14
|
+
def open?(actor)
|
15
|
+
value = toggle.value
|
16
|
+
|
17
|
+
if value.nil?
|
18
|
+
false
|
19
|
+
else
|
20
|
+
throw :short_circuit, !!value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def protects?(thing)
|
25
|
+
thing.is_a?(Flipper::Types::Boolean)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Gates
|
3
|
+
class Group < Gate
|
4
|
+
Key = :groups
|
5
|
+
|
6
|
+
def type_key
|
7
|
+
Key
|
8
|
+
end
|
9
|
+
|
10
|
+
def toggle_class
|
11
|
+
Toggles::Set
|
12
|
+
end
|
13
|
+
|
14
|
+
def open?(actor)
|
15
|
+
return if actor.nil?
|
16
|
+
groups.any? { |group| group.match?(actor) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def group_names
|
20
|
+
toggle.value
|
21
|
+
end
|
22
|
+
|
23
|
+
def groups
|
24
|
+
group_names.map { |name| Flipper.group(name) }.compact
|
25
|
+
end
|
26
|
+
|
27
|
+
def protects?(thing)
|
28
|
+
thing.is_a?(Flipper::Types::Group)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Gates
|
3
|
+
class PercentageOfActors < Gate
|
4
|
+
Key = :perc_actors
|
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
|
+
actor.identifier % 100 < percentage
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def protects?(thing)
|
21
|
+
thing.is_a?(Flipper::Types::PercentageOfActors)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|