flipper 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - ree
5
+ - 1.9.3
6
+ notifications:
7
+ email: false
8
+ bundler_args: --without guard
data/Gemfile CHANGED
@@ -7,7 +7,8 @@ group(:guard) do
7
7
  gem 'guard'
8
8
  gem 'guard-rspec'
9
9
  gem 'guard-bundler'
10
- gem 'growl'
10
+ gem 'terminal-notifier-guard'
11
+ gem 'rb-fsevent'
11
12
  end
12
13
 
13
14
  group(:test) do
@@ -1,11 +1,3 @@
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
1
  module Flipper
10
2
  def self.new(*args)
11
3
  DSL.new(*args)
@@ -31,3 +23,11 @@ module Flipper
31
23
  groups.get(name)
32
24
  end
33
25
  end
26
+
27
+ require 'flipper/dsl'
28
+ require 'flipper/errors'
29
+ require 'flipper/feature'
30
+ require 'flipper/gate'
31
+ require 'flipper/registry'
32
+ require 'flipper/toggle'
33
+ require 'flipper/type'
@@ -1,5 +1,7 @@
1
1
  module Flipper
2
- # Internal: Adapter wrapper that wraps vanilla adapter instances with local caching.
2
+ # Internal: Adapter wrapper that wraps vanilla adapter instances. Adds things
3
+ # like local caching and convenience methods for adding/reading features from
4
+ # the adapter.
3
5
  #
4
6
  # So what is this local cache crap?
5
7
  #
@@ -17,6 +19,8 @@ module Flipper
17
19
  # To see an example adapter that this would wrap, checkout the [memory
18
20
  # adapter included with flipper](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adapters/memory.rb).
19
21
  class Adapter
22
+ FeaturesKey = 'features'
23
+
20
24
  # Internal: Wraps vanilla adapter instance for use internally in flipper.
21
25
  #
22
26
  # object - Either an instance of Flipper::Adapter or a vanilla adapter instance
@@ -65,35 +69,27 @@ module Flipper
65
69
  end
66
70
 
67
71
  def read(key)
68
- if using_local_cache?
69
- cache(key) { @adapter.read(key) }
70
- else
71
- @adapter.read(key)
72
- end
72
+ perform_read(key) { @adapter.read(key) }
73
73
  end
74
74
 
75
75
  def write(key, value)
76
- @adapter.write(key, value).tap { expire_local_cache(key) }
76
+ perform_update(key) { @adapter.write(key, value) }
77
77
  end
78
78
 
79
79
  def delete(key)
80
- @adapter.delete(key).tap { expire_local_cache(key) }
80
+ perform_update(key) { @adapter.delete(key) }
81
81
  end
82
82
 
83
83
  def set_members(key)
84
- if using_local_cache?
85
- cache(key) { @adapter.set_members(key) }
86
- else
87
- @adapter.set_members(key)
88
- end
84
+ perform_read(key) { @adapter.set_members(key) }
89
85
  end
90
86
 
91
87
  def set_add(key, value)
92
- @adapter.set_add(key, value).tap { expire_local_cache(key) }
88
+ perform_update(key) { @adapter.set_add(key, value) }
93
89
  end
94
90
 
95
91
  def set_delete(key, value)
96
- @adapter.set_delete(key, value).tap { expire_local_cache(key) }
92
+ perform_update(key) { @adapter.set_delete(key, value) }
97
93
  end
98
94
 
99
95
  def eql?(other)
@@ -101,14 +97,30 @@ module Flipper
101
97
  end
102
98
  alias :== :eql?
103
99
 
100
+ def features
101
+ set_members(FeaturesKey)
102
+ end
103
+
104
+ def feature_add(name)
105
+ set_add(FeaturesKey, name.to_s)
106
+ end
107
+
104
108
  private
105
109
 
106
- def cache(key)
107
- local_cache.fetch(key) { local_cache[key] = yield }
110
+ def perform_read(key)
111
+ if using_local_cache?
112
+ local_cache.fetch(key.to_s) { local_cache[key.to_s] = yield }
113
+ else
114
+ yield
115
+ end
108
116
  end
109
117
 
110
- def expire_local_cache(key)
111
- local_cache.delete(key) if using_local_cache?
118
+ def perform_update(key)
119
+ result = yield
120
+ if using_local_cache?
121
+ local_cache.delete(key.to_s)
122
+ end
123
+ result
112
124
  end
113
125
  end
114
126
  end
@@ -8,27 +8,36 @@ module Flipper
8
8
  end
9
9
 
10
10
  def read(key)
11
- @source[key]
11
+ @source[key.to_s]
12
12
  end
13
13
 
14
14
  def write(key, value)
15
- @source[key] = value
15
+ @source[key.to_s] = value
16
16
  end
17
17
 
18
18
  def delete(key)
19
- @source.delete(key)
19
+ @source.delete(key.to_s)
20
20
  end
21
21
 
22
22
  def set_add(key, value)
23
- set_members(key).add(value)
23
+ ensure_set_initialized(key)
24
+ @source[key.to_s].add(value)
24
25
  end
25
26
 
26
27
  def set_delete(key, value)
27
- set_members(key).delete(value)
28
+ ensure_set_initialized(key)
29
+ @source[key.to_s].delete(value)
28
30
  end
29
31
 
30
32
  def set_members(key)
31
- @source[key] ||= Set.new
33
+ ensure_set_initialized(key)
34
+ @source[key.to_s]
35
+ end
36
+
37
+ private
38
+
39
+ def ensure_set_initialized(key)
40
+ @source[key.to_s] ||= Set.new
32
41
  end
33
42
  end
34
43
  end
@@ -0,0 +1,57 @@
1
+ module Flipper
2
+ module Adapters
3
+ # Public: Adapter that wraps another adapter and stores the operations.
4
+ #
5
+ # Useful in tests to verify calls and such.
6
+ class OperationLogger
7
+ attr_reader :operations
8
+
9
+ Read = Struct.new(:key)
10
+ Write = Struct.new(:key, :value)
11
+ Delete = Struct.new(:key)
12
+ SetAdd = Struct.new(:key, :value)
13
+ SetDelete = Struct.new(:key, :value)
14
+ SetMember = Struct.new(:key)
15
+
16
+ def initialize(adapter)
17
+ @operations = []
18
+ @adapter = adapter
19
+ end
20
+
21
+ def read(key)
22
+ @operations << Read.new(key.to_s)
23
+ @adapter.read key
24
+ end
25
+
26
+ def write(key, value)
27
+ @operations << Write.new(key.to_s, value)
28
+ @adapter.write key, value
29
+ end
30
+
31
+ def delete(key)
32
+ @operations << Delete.new(key.to_s, nil)
33
+ @adapter.delete key
34
+ end
35
+
36
+ def set_add(key, value)
37
+ @operations << SetAdd.new(key.to_s, value)
38
+ @adapter.set_add key, value
39
+ end
40
+
41
+ def set_delete(key, value)
42
+ @operations << SetDelete.new(key.to_s, value)
43
+ @adapter.set_delete key, value
44
+ end
45
+
46
+ def set_members(key)
47
+ @operations << SetMembers.new(key.to_s)
48
+ @adapter.set_members key
49
+ end
50
+
51
+ # Public: Clears operation log
52
+ def reset
53
+ @operations.clear
54
+ end
55
+ end
56
+ end
57
+ end
@@ -25,7 +25,7 @@ module Flipper
25
25
  end
26
26
 
27
27
  def feature(name)
28
- features[name.to_sym] ||= Feature.new(name, @adapter)
28
+ memoized_features[name.to_sym] ||= Feature.new(name, @adapter)
29
29
  end
30
30
 
31
31
  alias :[] :feature
@@ -35,21 +35,27 @@ module Flipper
35
35
  end
36
36
 
37
37
  def actor(thing)
38
- Flipper::Types::Actor.new(thing)
38
+ Types::Actor.new(thing)
39
39
  end
40
40
 
41
41
  def random(number)
42
- Flipper::Types::PercentageOfRandom.new(number)
42
+ Types::PercentageOfRandom.new(number)
43
43
  end
44
+ alias :percentage_of_random :random
44
45
 
45
46
  def actors(number)
46
- Flipper::Types::PercentageOfActors.new(number)
47
+ Types::PercentageOfActors.new(number)
48
+ end
49
+ alias :percentage_of_actors :actors
50
+
51
+ def features
52
+ adapter.features.map { |name| feature(name) }.to_set
47
53
  end
48
54
 
49
55
  private
50
56
 
51
- def features
52
- @features ||= {}
57
+ def memoized_features
58
+ @memoized_features ||= {}
53
59
  end
54
60
  end
55
61
  end
@@ -30,13 +30,9 @@ module Flipper
30
30
  !enabled?(actor)
31
31
  end
32
32
 
33
- private
34
-
35
- def gate_for(thing)
36
- gates.detect { |gate| gate.protects?(thing) } ||
37
- raise(GateNotFound.new(thing))
38
- end
39
-
33
+ # Internal: Gates to check to see if feature is enabled/disabled
34
+ #
35
+ # Returns an array of gates
40
36
  def gates
41
37
  @gates ||= [
42
38
  Gates::Boolean.new(self),
@@ -46,5 +42,20 @@ module Flipper
46
42
  Gates::PercentageOfRandom.new(self),
47
43
  ]
48
44
  end
45
+
46
+ # Internal: Returns gate that protects thing
47
+ #
48
+ # thing - The object for which you would like to find a gate
49
+ #
50
+ # Raises Flipper::GateNotFound if no gate found for thing
51
+ def gate_for(thing)
52
+ find_gate(thing) || raise(GateNotFound.new(thing))
53
+ end
54
+
55
+ private
56
+
57
+ def find_gate(thing)
58
+ gates.detect { |gate| gate.protects?(thing) }
59
+ end
49
60
  end
50
61
  end
@@ -1,11 +1,10 @@
1
1
  require 'forwardable'
2
+ require 'flipper/key'
2
3
 
3
4
  module Flipper
4
5
  class Gate
5
6
  extend Forwardable
6
7
 
7
- Separator = '/'
8
-
9
8
  attr_reader :feature
10
9
 
11
10
  def_delegator :@feature, :adapter
@@ -14,12 +13,8 @@ module Flipper
14
13
  @feature = feature
15
14
  end
16
15
 
17
- def key_prefix
18
- @feature.name
19
- end
20
-
21
16
  def key
22
- "#{key_prefix}#{Separator}#{type_key}"
17
+ @key ||= Key.new(@feature.name, type_key)
23
18
  end
24
19
 
25
20
  def toggle_class
@@ -1,3 +1,5 @@
1
+ require 'zlib'
2
+
1
3
  module Flipper
2
4
  module Gates
3
5
  class PercentageOfActors < Gate
@@ -13,7 +15,7 @@ module Flipper
13
15
  if percentage.nil?
14
16
  false
15
17
  else
16
- actor.identifier % 100 < percentage
18
+ Zlib.crc32(actor.identifier.to_s) % 100 < percentage
17
19
  end
18
20
  end
19
21
 
@@ -0,0 +1,19 @@
1
+ module Flipper
2
+ class Key
3
+ Separator = '/'
4
+
5
+ attr_reader :prefix, :suffix
6
+
7
+ def initialize(prefix, suffix)
8
+ @prefix, @suffix = prefix, suffix
9
+ end
10
+
11
+ def separator
12
+ Separator.dup
13
+ end
14
+
15
+ def to_s
16
+ "#{prefix}#{separator}#{suffix}"
17
+ end
18
+ end
19
+ end
@@ -1,3 +1,5 @@
1
+ require 'thread'
2
+
1
3
  module Flipper
2
4
  class Registry
3
5
  include Enumerable
@@ -5,74 +5,69 @@ require 'set'
5
5
  # read_key(key)
6
6
  # write_key(key, value)
7
7
  shared_examples_for 'a flipper adapter' do
8
- describe "#write" do
9
- let(:separator) { Flipper::Gate::Separator }
8
+ let(:key) { Flipper::Key.new(:foo, :bar) }
10
9
 
10
+ describe "#write" do
11
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
12
+ subject.write(key, true)
13
+ read_key(key).should be_true
19
14
  end
20
15
  end
21
16
 
22
17
  describe "#read" do
23
18
  it "returns nil if key not in store" do
24
- subject.read('foo').should be_nil
19
+ subject.read(key).should be_nil
25
20
  end
26
21
 
27
22
  it "returns value if key in store" do
28
- write_key 'foo', 'bar'
29
- subject.read('foo').should eq('bar')
23
+ write_key key, 'bar'
24
+ subject.read(key).should eq('bar')
30
25
  end
31
26
  end
32
27
 
33
28
  describe "#delete" do
34
29
  it "deletes key" do
35
- write_key 'foo', 'bar'
36
- subject.delete('foo')
37
- read_key('foo').should be_nil
30
+ write_key key, 'bar'
31
+ subject.delete(key)
32
+ read_key(key).should be_nil
38
33
  end
39
34
  end
40
35
 
41
36
  describe "#set_add" do
42
37
  it "adds value to store" do
43
- subject.set_add('foo', 1)
44
- read_key('foo').should eq(Set[1])
38
+ subject.set_add(key, 1)
39
+ read_key(key).should eq(Set[1])
45
40
  end
46
41
 
47
42
  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])
43
+ subject.set_add(key, 1)
44
+ subject.set_add(key, 1)
45
+ subject.set_add(key, 1)
46
+ subject.set_add(key, 2)
47
+ read_key(key).should eq(Set[1, 2])
53
48
  end
54
49
  end
55
50
 
56
51
  describe "#set_delete" do
57
52
  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])
53
+ write_key key, Set[1, 2]
54
+ subject.set_delete(key, 1)
55
+ read_key(key).should eq(Set[2])
61
56
  end
62
57
 
63
58
  it "works fine if key not in store" do
64
- subject.set_delete('foo', 'bar')
59
+ subject.set_delete(key, 'bar')
65
60
  end
66
61
  end
67
62
 
68
63
  describe "#set_members" do
69
64
  it "defaults to empty set" do
70
- subject.set_members('foo').should eq(Set.new)
65
+ subject.set_members(key).should eq(Set.new)
71
66
  end
72
67
 
73
68
  it "returns set if in store" do
74
- write_key 'foo', Set[1, 2]
75
- subject.set_members('foo').should eq(Set[1, 2])
69
+ write_key key, Set[1, 2]
70
+ subject.set_members(key).should eq(Set[1, 2])
76
71
  end
77
72
  end
78
73