this_feature 0.3.0 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,41 @@
1
+ # ThisFeature - Flipper Adapter
2
+
3
+ ## Installation
4
+
5
+ ```ruby
6
+ gem 'this_feature-adapters-flipper
7
+ ```
8
+
9
+ ## Configuration
10
+
11
+ ```ruby
12
+ # config/initializers/this_feature.rb
13
+ require 'this_feature'
14
+ require 'this_feature/adapters/flipper'
15
+
16
+ ThisFeature.configure do |config|
17
+ adapter = ThisFeature::Adapters::Flipper.new
18
+ config.adapters = [adapter]
19
+ config.default_adapter = adapter
20
+ end
21
+ ```
22
+
23
+ An existing Flipper client can be optionally passed to the initializer:
24
+
25
+ ```
26
+ ThisFeature::Adapters::Flipper.new(client: my_existing_client)
27
+ ```
28
+
29
+
30
+ ## API
31
+
32
+ The Flipper adapter supports the public API of `ThisFeature`.
33
+
34
+ The `context` argument is supported, but not `data`.
35
+
36
+ Read the following notes as well:
37
+
38
+ - **on?** / **off?**: Under the hood, calls `flipper_id` method on the `context`, if one was given.
39
+ - **control?** / **present?**: Flipper doesn't have a concept of "control", so we just implement it as `!present?`
40
+
41
+ It is possible to support `on!` and `off!` from Flipper but that's not implemented yet.
@@ -0,0 +1,88 @@
1
+ # ThisFeature - Memory Adapter
2
+
3
+ ## Synopsis
4
+
5
+ Under the hood, the memory adapter stores data in a dictionary like so:
6
+
7
+ ```json
8
+ {
9
+ some_flag_name: {
10
+ global: false,
11
+ contexts: {
12
+ User1: true,
13
+ User2: false
14
+ }
15
+ }
16
+ }
17
+ ```
18
+
19
+ Since it doesn't require actual DB lookups, it's faster, and works well for use
20
+ in test suites.
21
+
22
+ ## Installation
23
+
24
+ This adapter is included with the core gem:
25
+
26
+ ```ruby
27
+ gem 'this_feature
28
+ ```
29
+
30
+ ## Configuration
31
+
32
+ ```ruby
33
+ # config/initializers/this_feature.rb
34
+ require 'this_feature'
35
+ require 'this_feature/adapters/memory'
36
+
37
+ ThisFeature.configure do |config|
38
+ config.test_adapter = ThisFeature::Adapters::Memory.new
39
+ config.adapters = [config.test_adapter]
40
+ config.default_adapter = config.test_adapter
41
+ end
42
+ ```
43
+
44
+ The initializer takes an optional `context_key_method` argument. This is only relevant when using `context` -
45
+ it specifies a method name which should be called on the context object to determine its identity.
46
+ For example:
47
+
48
+ ```ruby
49
+ # Say you have this method which you want to use as the "identity"
50
+ # of a context object (e.g. imagine this module is included onto User)
51
+ module FeatureFlaggable
52
+ def this_feature_id
53
+ "#{self.class}-#{self.id}"
54
+ end
55
+ end
56
+
57
+ # Then you would refer to it like so in the initializer:
58
+ ThisFeature::Adapters::Memory.new(context_key_method: :this_feature_id)
59
+ ```
60
+
61
+ If this option is ommitted, then the context object uses its `self` as its "identity".
62
+
63
+ **See below for example of how to use on! and off! from tests**
64
+
65
+ ## API
66
+
67
+ The Memory adapter supports the public API of `ThisFeature`.
68
+
69
+ The `context` argument is supported, but not `data`.
70
+
71
+ Read the following notes as well:
72
+
73
+ - **on?** / **off?**: If passed a `context` argument, uses the aformentioned logic
74
+ (`context_key_method`) to determine how it's handled.
75
+
76
+ - **control?** is not yet implemented
77
+
78
+ We also support two additional methods here that aren't present on the main adapter yet:
79
+
80
+ - **on!** / **off!**
81
+
82
+ Usage example of these:
83
+
84
+ ```ruby
85
+ # If you have configured the in-memory adapter as the default
86
+ ThisFeature.test_adapter.on!(:flag_name, context: user) # with context
87
+ ThisFeature.test_adapter.off!(:flag_name) # without context
88
+ ```
@@ -0,0 +1,35 @@
1
+ # ThisFeature - Split Adapter
2
+
3
+ ## Installation
4
+
5
+ ```ruby
6
+ gem 'this_feature-adapters-split-io
7
+ ```
8
+
9
+ ## Configuration
10
+
11
+ ```ruby
12
+ # config/initializers/this_feature.rb
13
+ require 'this_feature'
14
+ require 'this_feature/adapters/split_io'
15
+
16
+ ThisFeature.configure do |config|
17
+ adapter = ThisFeature::Adapters::SplitIo.new
18
+ config.adapters = [adapter]
19
+ config.default_adapter = adapter
20
+ end
21
+ ```
22
+
23
+ An existing Split client can be optionally passed to the initializer:
24
+
25
+ ```
26
+ ThisFeature::Adapters::SplitIo.new(client: my_existing_client)
27
+ ```
28
+
29
+ ## API
30
+
31
+ The SplitIo adapter supports the public API of `ThisFeature`.
32
+
33
+ Both `context` and `data` are supported.
34
+
35
+ `control` is a native Split feature, so we perform a query to Split to get this info.
@@ -0,0 +1,9 @@
1
+ Look at [lib/this_feature/adapters/base.rb](../lib/this_feature/adapters/base.rb) to see the methods that your class should implement.
2
+
3
+ Make sure your class inherits from `ThisFeature::Adapters::Base` - this is a requirement.
4
+
5
+ You may define a custom `initialize` method - this isn't used by `this_feature` internals because we require an already-constructed instance to be passed into `ThisFeature.configure`.
6
+
7
+ For an example, look at one of the existing adapters: [lib/this_feature/adapters/](../lib/this_feature/adapters/)
8
+
9
+ If you want to include your adapter in our README, just open up a PR.
@@ -13,19 +13,19 @@ class ThisFeature
13
13
 
14
14
  def self.adapter_for(flag_name, context: nil, data: {})
15
15
  matching_adapter = adapters.find do |adapter|
16
- adapter.present?(flag_name, context: context, data: data)
16
+ adapter.present?(flag_name)
17
17
  end
18
18
 
19
19
  matching_adapter || configuration.default_adapter
20
20
  end
21
21
 
22
- # Configuration
23
-
24
22
  def self.configuration
25
23
  @configuration ||= Configuration.new
26
24
  end
27
25
 
28
26
  def self.configure
27
+ @configuration = Configuration.new
28
+
29
29
  yield(configuration)
30
30
 
31
31
  configuration.init
@@ -35,4 +35,7 @@ class ThisFeature
35
35
  configuration.adapters
36
36
  end
37
37
 
38
+ def self.test_adapter
39
+ configuration.test_adapter
40
+ end
38
41
  end
@@ -1,34 +1,21 @@
1
1
  class ThisFeature
2
2
  module Adapters
3
3
  class Base
4
-
5
- def self.setup
6
- raise UnimplementedError.new(self, __method__)
7
- end
8
-
9
- def self.present?(flag_name)
10
- raise UnimplementedError.new(self, __method__)
11
- end
12
-
13
- def self.on?(flag_name, context: nil, data: {})
14
- raise UnimplementedError.new(self, __method__)
15
- end
16
-
17
- def self.off?(flag_name, context: nil, data: {})
4
+ def present?(flag_name)
18
5
  raise UnimplementedError.new(self, __method__)
19
6
  end
20
7
 
21
- def self.on!(flag_name, context: nil, data: {})
8
+ def on?(flag_name, context: nil, data: {})
22
9
  raise UnimplementedError.new(self, __method__)
23
10
  end
24
11
 
25
- def self.off!(flag_name, context: nil, data: {})
12
+ def off?(flag_name, context: nil, data: {})
26
13
  raise UnimplementedError.new(self, __method__)
27
14
  end
28
15
 
29
16
  # OPTIONAL method
30
17
  # check to see if a control is being used
31
- def self.control?(flag_name, context: nil, data: {})
18
+ def control?(flag_name, context: nil, data: {})
32
19
  false
33
20
  end
34
21
  end
@@ -4,48 +4,38 @@ require "flipper/adapters/active_record"
4
4
  class ThisFeature
5
5
  module Adapters
6
6
  class Flipper < Base
7
+ attr_reader :client
7
8
 
8
- def self.setup(flipper = nil)
9
- return @flipper = flipper unless flipper.nil?
10
-
11
- @flipper = ::Flipper
12
-
13
- ::Flipper.configure do |config|
14
- config.default do
15
- adapter = ::Flipper::Adapters::ActiveRecord.new
16
- ::Flipper.new(adapter)
17
- end
18
- end
9
+ def initialize(client: nil)
10
+ @client = client || default_flipper_adapter
19
11
  end
20
12
 
21
- def self.present?(flag_name)
22
- flipper[flag_name].exist?
13
+ def present?(flag_name)
14
+ client[flag_name].exist?
23
15
  end
24
16
 
25
- def self.on?(flag_name, context: nil, data: {})
26
- return unless present?(flag_name)
27
-
28
- flipper[flag_name].enabled?(*[context].compact)
17
+ def control?(flag_name, **kwargs)
18
+ !present?(flag_name)
29
19
  end
30
20
 
31
- def self.off?(flag_name, context: nil, data: {})
32
- on_result = on?(flag_name, context: context)
33
-
34
- return if on_result.nil?
35
-
36
- !on_result
21
+ def on?(flag_name, context: nil, data: {})
22
+ client[flag_name].enabled?(*[context].compact)
37
23
  end
38
24
 
39
- def self.on!(flag_name, context: nil, data: {})
40
- flipper[flag_name].enable(*[context].compact)
25
+ def off?(flag_name, context: nil, data: {})
26
+ !on?(flag_name, context: context)
41
27
  end
42
28
 
43
- def self.off!(flag_name, context: nil, data: {})
44
- flipper[flag_name].disable(*[context].compact)
45
- end
29
+ private
46
30
 
47
- def self.flipper
48
- @flipper
31
+ def default_flipper_adapter
32
+ ::Flipper.configure do |config|
33
+ config.default do
34
+ adapter = ::Flipper::Adapters::ActiveRecord.new
35
+ ::Flipper.new(adapter)
36
+ end
37
+ end
38
+ ::Flipper
49
39
  end
50
40
  end
51
41
  end
@@ -2,20 +2,19 @@ class ThisFeature
2
2
  module Adapters
3
3
  class Memory < Base
4
4
 
5
- def self.setup(context_id_method: :id)
6
- @context_id_method = context_id_method
5
+ def initialize(context_key_method: nil)
6
+ @context_key_method = context_key_method
7
7
  end
8
8
 
9
- def self.clear
9
+ def clear
10
10
  storage.clear
11
11
  end
12
12
 
13
- def self.present?(flag_name)
13
+ def present?(flag_name)
14
14
  !storage[flag_name].nil?
15
15
  end
16
16
 
17
- def self.on?(flag_name, context: nil, data: {})
18
- # binding.pry
17
+ def on?(flag_name, context: nil, data: {})
19
18
  return unless present?(flag_name)
20
19
 
21
20
  flag_data = storage[flag_name]
@@ -25,10 +24,10 @@ class ThisFeature
25
24
 
26
25
  flag_data[:contexts] ||= {}
27
26
 
28
- !!flag_data[:contexts][context.send(@context_id_method)]
27
+ !!flag_data[:contexts][context_key(context)]
29
28
  end
30
29
 
31
- def self.off?(flag_name, context: nil, data: {})
30
+ def off?(flag_name, context: nil, data: {})
32
31
  on_result = on?(flag_name, context: context)
33
32
 
34
33
  return if on_result.nil?
@@ -36,27 +35,37 @@ class ThisFeature
36
35
  !on_result
37
36
  end
38
37
 
39
- def self.on!(flag_name, context: nil, data: {})
38
+ def on!(flag_name, context: nil, data: {})
40
39
  storage[flag_name] ||= {}
41
40
 
42
41
  return storage[flag_name][:global] = true if context.nil?
43
42
 
44
43
  storage[flag_name][:contexts] ||= {}
45
- storage[flag_name][:contexts][context.send(@context_id_method)] = true
44
+ storage[flag_name][:contexts][context_key(context)] = true
46
45
  end
47
46
 
48
- def self.off!(flag_name, context: nil, data: {})
47
+ def off!(flag_name, context: nil, data: {})
49
48
  storage[flag_name] ||= {}
50
49
 
51
50
  return storage[flag_name][:global] = false if context.nil?
52
51
 
53
52
  storage[flag_name][:contexts] ||= {}
54
- storage[flag_name][:contexts][context.send(@context_id_method)] = false
53
+ storage[flag_name][:contexts][context_key(context)] = false
55
54
  end
56
55
 
57
- def self.storage
56
+ def storage
58
57
  @storage ||= {}
59
58
  end
59
+
60
+ private
61
+
62
+ attr_reader :context_key_method
63
+
64
+ def context_key(context)
65
+ return context if context_key_method.nil?
66
+
67
+ context.send(context_key_method)
68
+ end
60
69
  end
61
70
  end
62
71
  end
@@ -0,0 +1,52 @@
1
+ require 'splitclient-rb'
2
+
3
+ class ThisFeature
4
+ module Adapters
5
+ class SplitIo < Base
6
+ # used as treatment key when none is given, it's required by split
7
+ UNDEFINED_KEY = 'undefined_key'
8
+
9
+ def initialize(client: nil)
10
+ @client = client || default_split_client
11
+
12
+ @client.block_until_ready
13
+ end
14
+
15
+ def present?(flag_name)
16
+ !control?(flag_name)
17
+ end
18
+
19
+ def control?(flag_name, context: UNDEFINED_KEY, data: {})
20
+ treatment(flag_name, context: context, data: data).include?('control')
21
+ end
22
+
23
+ def on?(flag_name, context: UNDEFINED_KEY, data: {})
24
+ treatment(flag_name, context: context, data: data).eql?('on')
25
+ end
26
+
27
+ def off?(flag_name, context: UNDEFINED_KEY, data: {})
28
+ treatment(flag_name, context: context, data: data).eql?('off')
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :client
34
+
35
+ def treatment(flag_name, context: UNDEFINED_KEY, data: {})
36
+ key = if context.nil?
37
+ UNDEFINED_KEY
38
+ elsif context.respond_to?(:to_s)
39
+ context.to_s
40
+ else
41
+ context
42
+ end
43
+
44
+ client.get_treatment(key, flag_name, data)
45
+ end
46
+
47
+ def default_split_client
48
+ SplitIoClient::SplitFactory.new(ENV.fetch('SPLIT_IO_AUTH_KEY')).client
49
+ end
50
+ end
51
+ end
52
+ end