this_feature 0.6.1 → 0.8.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.
- checksums.yaml +4 -4
- data/Gemfile.lock +38 -26
- data/README.md +30 -6
- data/docs/splitio.md +28 -1
- data/lib/this_feature/adapters/base.rb +2 -2
- data/lib/this_feature/adapters/flipper.rb +2 -2
- data/lib/this_feature/adapters/memory.rb +6 -5
- data/lib/this_feature/adapters/split_io.rb +13 -11
- data/lib/this_feature/configuration.rb +5 -1
- data/lib/this_feature/flag.rb +6 -5
- data/lib/this_feature/version.rb +1 -1
- data/lib/this_feature.rb +8 -4
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a2e4f072c94b68b9a9e4675ac663f4d6f451d1d7c854e457079d539bd16184e
|
4
|
+
data.tar.gz: 6bbda6b3a7257f0a2e1ebd13784c3a78d4eb4ba3340727b99aa9d9df90753ea3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c0e69e00abb16fc2e781b4fdeb63664af7705280095c1f0a12cb660c5195b886463195e0830c91382474f62cd5c9c0b777aeddb14b494f48040cc2522d238c9
|
7
|
+
data.tar.gz: 6b1e05ba21a6409f413ba5b11167f1c17e561a9c2989edde58a1b2f50d1c368c4b76fde8153db4588757d6e4bd93a0f9e36d2fa4874e7190fa8c4347699e9c53
|
data/Gemfile.lock
CHANGED
@@ -1,56 +1,69 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
this_feature (0.
|
5
|
-
this_feature-adapters-flipper (0.
|
4
|
+
this_feature (0.8.0)
|
5
|
+
this_feature-adapters-flipper (0.8.0)
|
6
6
|
flipper (~> 0.16)
|
7
7
|
flipper-active_record (~> 0.16)
|
8
8
|
this_feature
|
9
|
-
this_feature-adapters-split_io (0.
|
9
|
+
this_feature-adapters-split_io (0.8.0)
|
10
10
|
splitclient-rb
|
11
11
|
this_feature
|
12
12
|
|
13
13
|
GEM
|
14
14
|
remote: https://rubygems.org/
|
15
15
|
specs:
|
16
|
-
activemodel (
|
17
|
-
activesupport (=
|
18
|
-
activerecord (
|
19
|
-
activemodel (=
|
20
|
-
activesupport (=
|
21
|
-
activesupport (
|
16
|
+
activemodel (7.0.3.1)
|
17
|
+
activesupport (= 7.0.3.1)
|
18
|
+
activerecord (7.0.3.1)
|
19
|
+
activemodel (= 7.0.3.1)
|
20
|
+
activesupport (= 7.0.3.1)
|
21
|
+
activesupport (7.0.3.1)
|
22
22
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
23
23
|
i18n (>= 1.6, < 2)
|
24
24
|
minitest (>= 5.1)
|
25
25
|
tzinfo (~> 2.0)
|
26
|
-
zeitwerk (~> 2.3)
|
27
26
|
byebug (11.1.2)
|
28
27
|
coderay (1.1.2)
|
29
|
-
concurrent-ruby (1.1.
|
30
|
-
connection_pool (2.2.
|
28
|
+
concurrent-ruby (1.1.10)
|
29
|
+
connection_pool (2.2.5)
|
31
30
|
database_cleaner (1.8.4)
|
32
31
|
database_cleaner-active_record (1.8.0)
|
33
32
|
activerecord
|
34
33
|
database_cleaner (~> 1.8.0)
|
35
34
|
diff-lcs (1.3)
|
36
|
-
faraday (1.
|
35
|
+
faraday (1.7.0)
|
36
|
+
faraday-em_http (~> 1.0)
|
37
|
+
faraday-em_synchrony (~> 1.0)
|
38
|
+
faraday-excon (~> 1.1)
|
39
|
+
faraday-httpclient (~> 1.0.1)
|
37
40
|
faraday-net_http (~> 1.0)
|
41
|
+
faraday-net_http_persistent (~> 1.1)
|
42
|
+
faraday-patron (~> 1.0)
|
43
|
+
faraday-rack (~> 1.0)
|
38
44
|
multipart-post (>= 1.2, < 3)
|
39
|
-
ruby2_keywords
|
45
|
+
ruby2_keywords (>= 0.0.4)
|
46
|
+
faraday-em_http (1.0.0)
|
47
|
+
faraday-em_synchrony (1.0.0)
|
48
|
+
faraday-excon (1.1.0)
|
49
|
+
faraday-httpclient (1.0.1)
|
40
50
|
faraday-net_http (1.0.1)
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
51
|
+
faraday-net_http_persistent (1.2.0)
|
52
|
+
faraday-patron (1.0.0)
|
53
|
+
faraday-rack (1.0.0)
|
54
|
+
flipper (0.25.0)
|
55
|
+
flipper-active_record (0.25.0)
|
56
|
+
activerecord (>= 4.2, < 8)
|
57
|
+
flipper (~> 0.25.0)
|
45
58
|
gem-release (2.2.1)
|
46
59
|
hitimes (1.3.1)
|
47
|
-
i18n (1.
|
60
|
+
i18n (1.11.0)
|
48
61
|
concurrent-ruby (~> 1.0)
|
49
62
|
json (2.5.1)
|
50
|
-
jwt (2.2.
|
63
|
+
jwt (2.2.3)
|
51
64
|
lru_redux (1.1.0)
|
52
65
|
method_source (1.0.0)
|
53
|
-
minitest (5.
|
66
|
+
minitest (5.16.2)
|
54
67
|
multipart-post (2.1.1)
|
55
68
|
net-http-persistent (4.0.1)
|
56
69
|
connection_pool (~> 2.2)
|
@@ -61,7 +74,7 @@ GEM
|
|
61
74
|
byebug (~> 11.0)
|
62
75
|
pry (~> 0.13.0)
|
63
76
|
rake (13.0.1)
|
64
|
-
redis (4.
|
77
|
+
redis (4.4.0)
|
65
78
|
rspec (3.9.0)
|
66
79
|
rspec-core (~> 3.9.0)
|
67
80
|
rspec-expectations (~> 3.9.0)
|
@@ -75,10 +88,10 @@ GEM
|
|
75
88
|
diff-lcs (>= 1.2.0, < 2.0)
|
76
89
|
rspec-support (~> 3.9.0)
|
77
90
|
rspec-support (3.9.2)
|
78
|
-
ruby2_keywords (0.0.
|
91
|
+
ruby2_keywords (0.0.5)
|
79
92
|
socketry (0.5.1)
|
80
93
|
hitimes (~> 1.2)
|
81
|
-
splitclient-rb (7.
|
94
|
+
splitclient-rb (7.3.1)
|
82
95
|
concurrent-ruby (~> 1.0)
|
83
96
|
faraday (>= 0.8)
|
84
97
|
json (>= 1.8)
|
@@ -92,7 +105,6 @@ GEM
|
|
92
105
|
thread_safe (0.3.6)
|
93
106
|
tzinfo (2.0.4)
|
94
107
|
concurrent-ruby (~> 1.0)
|
95
|
-
zeitwerk (2.4.2)
|
96
108
|
|
97
109
|
PLATFORMS
|
98
110
|
ruby
|
@@ -110,4 +122,4 @@ DEPENDENCIES
|
|
110
122
|
this_feature-adapters-split_io!
|
111
123
|
|
112
124
|
BUNDLED WITH
|
113
|
-
2.
|
125
|
+
2.3.21
|
data/README.md
CHANGED
@@ -2,17 +2,27 @@
|
|
2
2
|
|
3
3
|
**A common interface to interact with many feature flag providers.**
|
4
4
|
|
5
|
-
|
5
|
+
ThisFeature can be used to more easily migrate from one feature flag service to another
|
6
6
|
|
7
7
|
If your code uses ThisFeature,
|
8
|
-
then you can just swap out the adapter without
|
8
|
+
then you can just swap out the vendor adapter without needing to do a bunch of find-and-replace in your codebase
|
9
|
+
from one vendor's class/method signature to the another's.
|
9
10
|
|
10
11
|
## Installation
|
11
12
|
|
13
|
+
Add ThisFeature to your `Gemfile`:
|
14
|
+
|
12
15
|
```ruby
|
16
|
+
# Gemfile
|
13
17
|
gem 'this_feature'
|
14
18
|
```
|
15
19
|
|
20
|
+
Then from your Rails app's root directory:
|
21
|
+
|
22
|
+
```sh
|
23
|
+
bundle install
|
24
|
+
```
|
25
|
+
|
16
26
|
## Configuration
|
17
27
|
|
18
28
|
```ruby
|
@@ -36,14 +46,13 @@ end
|
|
36
46
|
```ruby
|
37
47
|
ThisFeature.flag('flag_name').on? # is the flag is turned on?
|
38
48
|
ThisFeature.flag('flag_name').off? # is the flag is turned off?
|
39
|
-
ThisFeature.flag('flag_name').control? # is the adapter
|
40
|
-
ThisFeature.flag('flag_name').present? # is the flag set at all?
|
49
|
+
ThisFeature.flag('flag_name').control? # is the adapter using the control?
|
41
50
|
ThisFeature.default_adapter # access the default adapter directly if needed
|
42
51
|
```
|
43
52
|
|
44
53
|
### Context
|
45
54
|
|
46
|
-
You can also pass a context to the flag, many feature flagging systems support this.
|
55
|
+
You can also pass a `context` to the flag, many feature flagging systems support this.
|
47
56
|
|
48
57
|
```ruby
|
49
58
|
ThisFeature.flag('flag_name', context: current_user).on?
|
@@ -51,12 +60,17 @@ ThisFeature.flag('flag_name', context: current_user).on?
|
|
51
60
|
|
52
61
|
### Data
|
53
62
|
|
54
|
-
In case context is not sufficient, you can also pass a data hash.
|
63
|
+
In case `context` is not sufficient, you can also pass a `data` hash.
|
55
64
|
|
56
65
|
```ruby
|
57
66
|
ThisFeature.flag('flag_name', context: context, data: { org_id: 1 }).on?
|
58
67
|
```
|
59
68
|
|
69
|
+
### Avoid Pitfalls
|
70
|
+
|
71
|
+
1. If your flag has context-specific rules (e.g. on for some orgs, off for others), make sure that the code does a context-specific check. `ThisFeature.flag("flag_name").on?` may return true, while `ThisFeature.flag("flag_name", context: Org.first).on?` would return false.
|
72
|
+
2. Related to the previous bullet point, if you are checking whether a flag is "globally enabled" (and thus may be removed from the codebase), do not just use `ThisFeature.flag("flag_name").on?`, it won't tell you the whole story. Go to the vendor console and check whether there are context-specific rules enabled.
|
73
|
+
|
60
74
|
## Available Adapters
|
61
75
|
|
62
76
|
These adapters do behave slightly differently, so make sure to read the following docs:
|
@@ -65,6 +79,16 @@ These adapters do behave slightly differently, so make sure to read the followin
|
|
65
79
|
- [Split.io adapter](./docs/splitio.md)
|
66
80
|
- [Memory adapter](./docs/memory.md) - **designed for use in tests**
|
67
81
|
|
82
|
+
### Needed Adapters
|
83
|
+
|
84
|
+
We'd like to add more adapters for more vendors.
|
85
|
+
If you're using a different backend and write your own adapter,
|
86
|
+
please submit a pull request to upstream that adaptor into this repo.
|
87
|
+
|
88
|
+
- Launch Darkly
|
89
|
+
- YAML files
|
90
|
+
- ...
|
91
|
+
|
68
92
|
## Development
|
69
93
|
|
70
94
|
The tests are a good reflection of the current development state.
|
data/docs/splitio.md
CHANGED
@@ -32,4 +32,31 @@ The SplitIo adapter supports the public API of `ThisFeature`.
|
|
32
32
|
|
33
33
|
Both `context` and `data` are supported.
|
34
34
|
|
35
|
-
`control` is a native Split feature, so we perform a query to Split to get this info.
|
35
|
+
`control` is a native Split feature, so we perform a query to Split to get this info.
|
36
|
+
|
37
|
+
We've also added `record`, which is a helper to easily and consistently add
|
38
|
+
attributes to the `data` hash. To take advantage of this, the application must
|
39
|
+
set a `base_data_lambda` in the config. An example—
|
40
|
+
```ruby
|
41
|
+
ThisFeature.configure do |config|
|
42
|
+
config.base_data_lambda = -> (record) do
|
43
|
+
case record
|
44
|
+
when Org
|
45
|
+
{
|
46
|
+
org_id: record.id,
|
47
|
+
org_name: record.name
|
48
|
+
}
|
49
|
+
when User
|
50
|
+
{
|
51
|
+
org_id: record.org.id,
|
52
|
+
org_name: record.org.name,
|
53
|
+
user_email: record.email,
|
54
|
+
user_id: record.id,
|
55
|
+
user_name: record.name,
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
Then `ThisFeature.flag("my-flag", record: user).on?` will automatically include
|
62
|
+
org_id, org_name, user_email, user_id, and user_name in the data attributes.
|
@@ -5,11 +5,11 @@ class ThisFeature
|
|
5
5
|
raise UnimplementedError.new(self, __method__)
|
6
6
|
end
|
7
7
|
|
8
|
-
def on?(flag_name, context: nil, data: {})
|
8
|
+
def on?(flag_name, context: nil, data: {}, record: nil)
|
9
9
|
raise UnimplementedError.new(self, __method__)
|
10
10
|
end
|
11
11
|
|
12
|
-
def off?(flag_name, context: nil, data: {})
|
12
|
+
def off?(flag_name, context: nil, data: {}, record: nil)
|
13
13
|
raise UnimplementedError.new(self, __method__)
|
14
14
|
end
|
15
15
|
|
@@ -18,11 +18,11 @@ class ThisFeature
|
|
18
18
|
!present?(flag_name)
|
19
19
|
end
|
20
20
|
|
21
|
-
def on?(flag_name, context: nil, data: {})
|
21
|
+
def on?(flag_name, context: nil, data: {}, record: nil)
|
22
22
|
client[flag_name].enabled?(*[context].compact)
|
23
23
|
end
|
24
24
|
|
25
|
-
def off?(flag_name, context: nil, data: {})
|
25
|
+
def off?(flag_name, context: nil, data: {}, record: nil)
|
26
26
|
!on?(flag_name, context: context)
|
27
27
|
end
|
28
28
|
|
@@ -14,20 +14,20 @@ class ThisFeature
|
|
14
14
|
!storage[flag_name].nil?
|
15
15
|
end
|
16
16
|
|
17
|
-
def on?(flag_name, context: nil, data: {})
|
17
|
+
def on?(flag_name, context: nil, data: {}, record: nil)
|
18
18
|
return false unless present?(flag_name)
|
19
19
|
|
20
20
|
flag_data = storage[flag_name]
|
21
21
|
|
22
|
-
|
23
|
-
return false if context.nil?
|
22
|
+
context_registered = flag_data[:contexts]&.key?(context_key(context))
|
24
23
|
|
25
|
-
flag_data[:
|
24
|
+
return !!flag_data[:global] if !context || (context && !context_registered)
|
26
25
|
|
26
|
+
flag_data[:contexts] ||= {}
|
27
27
|
!!flag_data[:contexts][context_key(context)]
|
28
28
|
end
|
29
29
|
|
30
|
-
def off?(flag_name, context: nil, data: {})
|
30
|
+
def off?(flag_name, context: nil, data: {}, record: nil)
|
31
31
|
!on?(flag_name, context: context, data: data)
|
32
32
|
end
|
33
33
|
|
@@ -62,6 +62,7 @@ class ThisFeature
|
|
62
62
|
attr_reader :context_key_method
|
63
63
|
|
64
64
|
def context_key(context)
|
65
|
+
return nil unless context
|
65
66
|
return context if context_key_method.nil?
|
66
67
|
|
67
68
|
context.send(context_key_method)
|
@@ -3,8 +3,9 @@ require 'splitclient-rb'
|
|
3
3
|
class ThisFeature
|
4
4
|
module Adapters
|
5
5
|
class SplitIo < Base
|
6
|
-
#
|
7
|
-
|
6
|
+
# Used as the context key when none is given. This arg is required by
|
7
|
+
# Split, but it's nice not to have to pass it when the context is empty.
|
8
|
+
EMPTY_CONTEXT = 'undefined_key'
|
8
9
|
|
9
10
|
def initialize(client: nil, context_key_method: nil)
|
10
11
|
@client = client || default_split_client
|
@@ -17,28 +18,29 @@ class ThisFeature
|
|
17
18
|
!control?(flag_name)
|
18
19
|
end
|
19
20
|
|
20
|
-
def control?(flag_name, context:
|
21
|
-
treatment(flag_name, context: context, data: data).include?('control')
|
21
|
+
def control?(flag_name, context: EMPTY_CONTEXT, data: {}, record: nil)
|
22
|
+
treatment(flag_name, context: context, data: data, record: record).include?('control')
|
22
23
|
end
|
23
24
|
|
24
|
-
def on?(flag_name, context:
|
25
|
-
treatment(flag_name, context: context, data: data).eql?('on')
|
25
|
+
def on?(flag_name, context: EMPTY_CONTEXT, data: {}, record: nil)
|
26
|
+
treatment(flag_name, context: context, data: data, record: record).eql?('on')
|
26
27
|
end
|
27
28
|
|
28
|
-
def off?(flag_name, context:
|
29
|
-
treatment(flag_name, context: context, data: data).eql?('off')
|
29
|
+
def off?(flag_name, context: EMPTY_CONTEXT, data: {}, record: nil)
|
30
|
+
treatment(flag_name, context: context, data: data, record: record).eql?('off')
|
30
31
|
end
|
31
32
|
|
32
33
|
private
|
33
34
|
|
34
35
|
attr_reader :client, :context_key_method
|
35
36
|
|
36
|
-
def treatment(flag_name, context:
|
37
|
-
|
37
|
+
def treatment(flag_name, context: EMPTY_CONTEXT, data: {}, record: nil)
|
38
|
+
base_data = record ? ThisFeature.base_data_lambda.call(record) : {}
|
39
|
+
client.get_treatment(context_key(context), flag_name, base_data.merge(data))
|
38
40
|
end
|
39
41
|
|
40
42
|
def context_key(context)
|
41
|
-
return
|
43
|
+
return EMPTY_CONTEXT if context.nil? || context.eql?(EMPTY_CONTEXT)
|
42
44
|
return context.send(context_key_method) unless context_key_method.nil?
|
43
45
|
return context.to_s if context.respond_to?(:to_s)
|
44
46
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
class ThisFeature
|
2
2
|
class Configuration
|
3
|
-
attr_writer :adapters, :default_adapter, :test_adapter
|
3
|
+
attr_writer :adapters, :default_adapter, :test_adapter, :base_data_lambda
|
4
4
|
|
5
5
|
def init
|
6
6
|
validate_adapters!
|
@@ -25,5 +25,9 @@ class ThisFeature
|
|
25
25
|
def test_adapter
|
26
26
|
@test_adapter ||= Adapters::Memory.new
|
27
27
|
end
|
28
|
+
|
29
|
+
def base_data_lambda
|
30
|
+
@base_data_lambda ||= -> (record) { {} }
|
31
|
+
end
|
28
32
|
end
|
29
33
|
end
|
data/lib/this_feature/flag.rb
CHANGED
@@ -1,24 +1,25 @@
|
|
1
1
|
class ThisFeature
|
2
2
|
class Flag
|
3
|
-
attr_reader :flag_name, :context, :data, :adapter
|
3
|
+
attr_reader :flag_name, :context, :data, :adapter, :record
|
4
4
|
|
5
|
-
def initialize(flag_name, adapter:, context: nil, data: {})
|
5
|
+
def initialize(flag_name, adapter:, context: nil, data: {}, record: nil)
|
6
6
|
@flag_name = flag_name
|
7
7
|
@adapter = adapter
|
8
8
|
@context = context
|
9
9
|
@data = data
|
10
|
+
@record = record
|
10
11
|
end
|
11
12
|
|
12
13
|
def on?
|
13
|
-
adapter.on?(flag_name, context: context, data: data)
|
14
|
+
adapter.on?(flag_name, context: context, data: data, record: record)
|
14
15
|
end
|
15
16
|
|
16
17
|
def off?
|
17
|
-
adapter.off?(flag_name, context: context, data: data)
|
18
|
+
adapter.off?(flag_name, context: context, data: data, record: record)
|
18
19
|
end
|
19
20
|
|
20
21
|
def control?
|
21
|
-
adapter.control?(flag_name, context: context, data: data)
|
22
|
+
adapter.control?(flag_name, context: context, data: data, record: record)
|
22
23
|
end
|
23
24
|
end
|
24
25
|
end
|
data/lib/this_feature/version.rb
CHANGED
data/lib/this_feature.rb
CHANGED
@@ -5,13 +5,13 @@ require 'this_feature/configuration'
|
|
5
5
|
require 'this_feature/flag'
|
6
6
|
|
7
7
|
class ThisFeature
|
8
|
-
def self.flag(flag_name, context: nil, data: {})
|
9
|
-
adapter = adapter_for(flag_name
|
8
|
+
def self.flag(flag_name, context: nil, data: {}, record: nil)
|
9
|
+
adapter = adapter_for(flag_name)
|
10
10
|
|
11
|
-
Flag.new(flag_name, adapter: adapter, context: context, data: data)
|
11
|
+
Flag.new(flag_name, adapter: adapter, context: context, data: data, record: record)
|
12
12
|
end
|
13
13
|
|
14
|
-
def self.adapter_for(flag_name
|
14
|
+
def self.adapter_for(flag_name)
|
15
15
|
matching_adapter = adapters.find do |adapter|
|
16
16
|
adapter.present?(flag_name)
|
17
17
|
end
|
@@ -38,4 +38,8 @@ class ThisFeature
|
|
38
38
|
def self.test_adapter
|
39
39
|
configuration.test_adapter
|
40
40
|
end
|
41
|
+
|
42
|
+
def self.base_data_lambda
|
43
|
+
configuration.base_data_lambda
|
44
|
+
end
|
41
45
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: this_feature
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Max Pleaner
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-01-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -165,7 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: '0'
|
167
167
|
requirements: []
|
168
|
-
rubygems_version: 3.
|
168
|
+
rubygems_version: 3.0.3
|
169
169
|
signing_key:
|
170
170
|
specification_version: 4
|
171
171
|
summary: Feature flag control
|