this_feature 0.6.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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