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,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