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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03e205e9b9a6a53ff76d8c93b315eb7e8cd2f05a96a9e7f4ae9ac434371155e9
4
- data.tar.gz: 1e5e996c866d55076f205a47685e295e4b04f0373aa545d183c0192d640ac8a0
3
+ metadata.gz: 4a2e4f072c94b68b9a9e4675ac663f4d6f451d1d7c854e457079d539bd16184e
4
+ data.tar.gz: 6bbda6b3a7257f0a2e1ebd13784c3a78d4eb4ba3340727b99aa9d9df90753ea3
5
5
  SHA512:
6
- metadata.gz: 4ea504d76f8f435166ba50bb4643065030e40ea44626ffd4222fe60a5f80588d3242fe89dc339b14cce7b8cc1657fef016c9c59e5decdc9879f3bf64f23581b5
7
- data.tar.gz: 523abe2a99278d2c559931e3b89ba0a946ca03f5e7a3fc92fd0e060555d0a0b25308d863f57904f4f4cc9ddb5fd93535e79bcee9164cff769b3d2ee61a6fcf0c
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.6.1)
5
- this_feature-adapters-flipper (0.6.1)
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.6.1)
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 (6.1.3)
17
- activesupport (= 6.1.3)
18
- activerecord (6.1.3)
19
- activemodel (= 6.1.3)
20
- activesupport (= 6.1.3)
21
- activesupport (6.1.3)
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.8)
30
- connection_pool (2.2.3)
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.3.0)
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
- flipper (0.20.3)
42
- flipper-active_record (0.20.3)
43
- activerecord (>= 5.0, < 7)
44
- flipper (~> 0.20.3)
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.8.9)
60
+ i18n (1.11.0)
48
61
  concurrent-ruby (~> 1.0)
49
62
  json (2.5.1)
50
- jwt (2.2.2)
63
+ jwt (2.2.3)
51
64
  lru_redux (1.1.0)
52
65
  method_source (1.0.0)
53
- minitest (5.14.4)
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.2.5)
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.4)
91
+ ruby2_keywords (0.0.5)
79
92
  socketry (0.5.1)
80
93
  hitimes (~> 1.2)
81
- splitclient-rb (7.2.3)
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.1.4
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
- Can be used to more easily migrate among providers.
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 having to do a bunch of find-and-replace.
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 is using the control?
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
- return true if flag_data[:global]
23
- return false if context.nil?
22
+ context_registered = flag_data[:contexts]&.key?(context_key(context))
24
23
 
25
- flag_data[:contexts] ||= {}
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
- # used as treatment key when none is given, it's required by split
7
- UNDEFINED_KEY = 'undefined_key'
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: UNDEFINED_KEY, data: {})
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: UNDEFINED_KEY, data: {})
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: UNDEFINED_KEY, data: {})
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: UNDEFINED_KEY, data: {})
37
- client.get_treatment(context_key(context), flag_name, data)
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 UNDEFINED_KEY if context.nil? || context.eql?(UNDEFINED_KEY)
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  class ThisFeature
2
- VERSION = "0.6.1"
2
+ VERSION = "0.8.0"
3
3
  end
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, context: nil, data: {})
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, context: nil, data: {})
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.6.1
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: 2021-03-04 00:00:00.000000000 Z
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.1.2
168
+ rubygems_version: 3.0.3
169
169
  signing_key:
170
170
  specification_version: 4
171
171
  summary: Feature flag control