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.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/Gemfile.lock +33 -5
- data/README.md +44 -29
- data/docs/flipper.html +1087 -0
- data/docs/flipper.md +41 -0
- data/docs/memory.md +88 -0
- data/docs/splitio.md +35 -0
- data/docs/writing_an_adapter.md +9 -0
- data/lib/this_feature.rb +6 -3
- data/lib/this_feature/adapters/base.rb +4 -17
- data/lib/this_feature/adapters/flipper.rb +20 -30
- data/lib/this_feature/adapters/memory.rb +22 -13
- data/lib/this_feature/adapters/split_io.rb +52 -0
- data/lib/this_feature/configuration.rb +8 -5
- data/lib/this_feature/errors.rb +6 -6
- data/lib/this_feature/flag.rb +0 -8
- data/lib/this_feature/version.rb +1 -1
- data/this_feature-adapters-flipper.gemspec +1 -0
- data/this_feature-adapters-split_io.gemspec +23 -0
- metadata +10 -3
data/docs/flipper.md
ADDED
@@ -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.
|
data/docs/memory.md
ADDED
@@ -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
|
+
```
|
data/docs/splitio.md
ADDED
@@ -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.
|
data/lib/this_feature.rb
CHANGED
@@ -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
|
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
|
8
|
+
def on?(flag_name, context: nil, data: {})
|
22
9
|
raise UnimplementedError.new(self, __method__)
|
23
10
|
end
|
24
11
|
|
25
|
-
def
|
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
|
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
|
9
|
-
|
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
|
22
|
-
|
13
|
+
def present?(flag_name)
|
14
|
+
client[flag_name].exist?
|
23
15
|
end
|
24
16
|
|
25
|
-
def
|
26
|
-
|
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
|
32
|
-
|
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
|
40
|
-
|
25
|
+
def off?(flag_name, context: nil, data: {})
|
26
|
+
!on?(flag_name, context: context)
|
41
27
|
end
|
42
28
|
|
43
|
-
|
44
|
-
flipper[flag_name].disable(*[context].compact)
|
45
|
-
end
|
29
|
+
private
|
46
30
|
|
47
|
-
def
|
48
|
-
|
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
|
6
|
-
@
|
5
|
+
def initialize(context_key_method: nil)
|
6
|
+
@context_key_method = context_key_method
|
7
7
|
end
|
8
8
|
|
9
|
-
def
|
9
|
+
def clear
|
10
10
|
storage.clear
|
11
11
|
end
|
12
12
|
|
13
|
-
def
|
13
|
+
def present?(flag_name)
|
14
14
|
!storage[flag_name].nil?
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
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
|
27
|
+
!!flag_data[:contexts][context_key(context)]
|
29
28
|
end
|
30
29
|
|
31
|
-
def
|
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
|
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
|
44
|
+
storage[flag_name][:contexts][context_key(context)] = true
|
46
45
|
end
|
47
46
|
|
48
|
-
def
|
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
|
53
|
+
storage[flag_name][:contexts][context_key(context)] = false
|
55
54
|
end
|
56
55
|
|
57
|
-
def
|
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
|