flipper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,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
@@ -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,11 @@
1
+ module Flipper
2
+ class Error < StandardError; end
3
+
4
+ class GateNotFound < Error
5
+ def initialize(thing)
6
+ super "Could not find gate for #{thing.inspect}"
7
+ end
8
+ end
9
+
10
+ class DuplicateGroup < Error; end
11
+ 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
@@ -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