this_feature 0.3.0 → 0.5.2

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.
@@ -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