this_feature 0.9.0 → 0.11.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: 1038dc52ca2faa768e02dd16ac68f71551cad5405932575de6ffbb0be648896d
4
- data.tar.gz: 9c930c96c3f67226b1a3eef72661320cb77cc275d0ac0fb69f8bd2342eddecf6
3
+ metadata.gz: 4f8b9d3709cdadcd289f916ccaabf17d6163daa087a2952f08e6cb800769685b
4
+ data.tar.gz: ff11aeed6a2deba49bd5d1ca4517f73dce29d3238fceff6bee6f1a7735dedce6
5
5
  SHA512:
6
- metadata.gz: 96c1151cc8dd148de9c5ba09e62cb1071b6cf18ce168145f07fa27cc4cf0075690deecd3326c6529966c74463bb088112bb8dd5bb5ad61d089a9a528318928af
7
- data.tar.gz: b9c7ce61ee74dd0dbde08de215e72b8965fb9816a9a88cdfc0d68baa651db933d10801301a9ecb2f5de588d469870f2b479edc04a199f4aee71f94a28a0839d3
6
+ metadata.gz: fcdd8e883cf7ac9abf17d00b2051a3d23aebe1b84bff45d58ea730621f4a629f07a2bf649f8107f7e38544e9c0edb266e632681c7dea069378342f8f205032ca
7
+ data.tar.gz: 41e40d6517de30f310414987c16b91807271a69fa41c4a14204107cda7f3a94e85696ace92b968f0e5c8063af5dc2f739230b6dbeb9d6009d263e56a133da143
@@ -0,0 +1,13 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ commit-message:
6
+ prefix: "[bot] "
7
+ cooldown:
8
+ default-days: 7
9
+ schedule:
10
+ interval: "weekly"
11
+ day: "wednesday"
12
+ time: "11:00"
13
+ timezone: "America/Los_Angeles"
@@ -11,7 +11,7 @@ jobs:
11
11
  uses: actions/checkout@main
12
12
 
13
13
  - name: Set up Ruby
14
- uses: ruby/setup-ruby@v1
14
+ uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1
15
15
  with:
16
16
  bundler-cache: true
17
17
 
data/Gemfile.lock CHANGED
@@ -1,12 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- this_feature (0.9.0)
5
- this_feature-adapters-flipper (0.9.0)
4
+ this_feature (0.11.0)
5
+ this_feature-adapters-flipper (0.11.0)
6
6
  flipper
7
7
  flipper-active_record
8
8
  this_feature
9
- this_feature-adapters-split_io (0.9.0)
9
+ this_feature-adapters-split_io (0.11.0)
10
10
  splitclient-rb
11
11
  this_feature
12
12
 
data/README.md CHANGED
@@ -98,6 +98,10 @@ bundle install && bundle exec rspec
98
98
 
99
99
  To write a new adapter, check the [Guide](./docs/writing_an_adapter.md).
100
100
 
101
+ ## Deployment
102
+
103
+ If you are working at Hover, see [this confluence doc](https://hoverinc.atlassian.net/wiki/spaces/EN/pages/3149266988/deploying+this+feature+gem)
104
+
101
105
  ## License
102
106
 
103
107
  ThisFeature is released under the [MIT License](https://choosealicense.com/licenses/mit).
data/docs/memory.md CHANGED
@@ -12,6 +12,17 @@ Under the hood, the memory adapter stores data in a dictionary like so:
12
12
  User1: true,
13
13
  User2: false
14
14
  }
15
+ },
16
+ some_flag_name_with_treatments: {
17
+ treatment_contexts: {
18
+ User1: 'treatment_name_1',
19
+ User2: 'treatment_name_2',
20
+ User3: 'treatment_name_3'
21
+ },
22
+ config_contexts: {
23
+ User1: {},
24
+ User2: {}
25
+ }
15
26
  }
16
27
  }
17
28
  ```
@@ -79,6 +90,47 @@ ThisFeature.test_adapter.on!(:flag_name, context: user) # with context
79
90
  ThisFeature.test_adapter.off!(:flag_name) # without context
80
91
  ```
81
92
 
93
+ ### **#enable_treatment!**
94
+
95
+ This method is useful when you need to enable a feature flag with a treatment (or multiple treatments), and not just `"on"` and `"off"`.
96
+
97
+ Usage example of these:
98
+
99
+ ```ruby
100
+ # If you have configured the in-memory adapter as the default
101
+ ThisFeature.test_adapter.enable_treatment!(:flag_name, treatment: 'treatment_name', context: user)
102
+ ```
103
+ #### This method requires 3 arguments:
104
+ 1. `flag_name`: String or Symbol (not a named argument)
105
+ 2. `treatment`: String
106
+ 3. `context`: User or Org object
107
+
108
+ Per flag name, there can only be one treatment per `context_key` (the ID of object that is provided as `context`). So multiple calls with the same `context`, but different treatment names, will be overwritten.
109
+
110
+ ```ruby
111
+ ThisFeature.test_adapter.enable_treatment!('flag_a', treatment: 'treatment_1', context: user1)
112
+ ThisFeature.test_adapter.storage # => { 'flag_a' => { :treatment_contexts => { 'User1': 'treatment_1' } } }
113
+
114
+ ThisFeature.test_adapter.enable_treatment!(:flag_a, treatment: 'treatment_2', context: user1)
115
+ ThisFeature.test_adapter.storage # => { 'flag_a' => { :treatment_contexts => { 'User1': 'treatment_2' } } }
116
+ ```
117
+
118
+ ### **#treatment_value**
119
+
120
+ You can retrieve the flag's treatment name for a specific context.
121
+
122
+ Usage example of these:
123
+
124
+ ```ruby
125
+ # If you have configured the in-memory adapter as the default
126
+ ThisFeature.test_adapter.treatment_value(:flag_name, context: user)
127
+ ```
128
+
129
+ #### This method requires 2 arguments:
130
+ 1. `flag_name`: String or Symbol (not a named argument)
131
+ 2. `context`: User or Org object
132
+
133
+ When the Memory storage does not contain the given flag_name or if there is no provided `context`, `"control"` is returned. This is meant to mimic what SplitIO would return in the case of no configured treatment for the given `context`.
82
134
  ### **#clear**
83
135
 
84
136
  Since the memory adapter stores flags in memory, it won't automatically get cleaned up in your tests. You can use this method to reset the memory adapter state.
@@ -34,6 +34,44 @@ class ThisFeature
34
34
  !present?(flag_name)
35
35
  end
36
36
 
37
+ def treatment_config(flag_name, context: nil, data: {}, record: nil)
38
+ return if !present?(flag_name) || context.nil?
39
+
40
+ flag_data = storage[flag_name][:config_contexts]
41
+ context_registered = flag_data&.key?(context_key(context))
42
+
43
+ return if !flag_data || !context_registered
44
+
45
+ flag_data[context_key(context)]
46
+ end
47
+
48
+ def enable_config!(flag_name, config: nil, context: nil)
49
+ return false if config.nil? || flag_name.nil? || context.nil?
50
+
51
+ storage[flag_name] ||= {}
52
+ storage[flag_name][:config_contexts] ||= {}
53
+ storage[flag_name][:config_contexts][context_key(context)] = config
54
+ end
55
+
56
+ def treatment_value(flag_name, context: nil, data: {}, record: nil)
57
+ return 'control' if !present?(flag_name) || context.nil?
58
+
59
+ flag_data = storage[flag_name][:treatment_contexts]
60
+ context_registered = flag_data&.key?(context_key(context))
61
+
62
+ return 'control' if !flag_data || !context_registered
63
+
64
+ flag_data[context_key(context)]
65
+ end
66
+
67
+ def enable_treatment!(flag_name, treatment: nil, context: nil)
68
+ return false if treatment.nil? || flag_name.nil? || context.nil?
69
+
70
+ storage[flag_name] ||= {}
71
+ storage[flag_name][:treatment_contexts] ||= {}
72
+ storage[flag_name][:treatment_contexts][context_key(context)] = treatment
73
+ end
74
+
37
75
  def on!(flag_name, context: nil, data: {})
38
76
  storage[flag_name] ||= {}
39
77
 
@@ -30,6 +30,14 @@ class ThisFeature
30
30
  treatment(flag_name, context: context, data: data, record: record).eql?('off')
31
31
  end
32
32
 
33
+ def treatment_value(flag_name, context: EMPTY_CONTEXT, data: {}, record: nil)
34
+ treatment_with_config(flag_name, context: context, data: data, record: record)[:treatment]
35
+ end
36
+
37
+ def treatment_config(flag_name, context: EMPTY_CONTEXT, data: {}, record: nil)
38
+ treatment_with_config(flag_name, context: context, data: data, record: record)[:config]
39
+ end
40
+
33
41
  private
34
42
 
35
43
  attr_reader :client, :context_key_method
@@ -39,6 +47,11 @@ class ThisFeature
39
47
  client.get_treatment(context_key(context), flag_name, base_data.merge(data))
40
48
  end
41
49
 
50
+ def treatment_with_config(flag_name, context: EMPTY_CONTEXT, data: {}, record: nil)
51
+ base_data = record ? ThisFeature.base_data_lambda.call(record) : {}
52
+ client.get_treatment_with_config(context_key(context), flag_name, base_data.merge(data))
53
+ end
54
+
42
55
  def context_key(context)
43
56
  return EMPTY_CONTEXT if context.nil? || context.eql?(EMPTY_CONTEXT)
44
57
  return context.send(context_key_method) unless context_key_method.nil?
@@ -1,6 +1,6 @@
1
1
  class ThisFeature
2
2
  class Configuration
3
- attr_writer :adapters, :default_adapter, :test_adapter, :base_data_lambda
3
+ attr_writer :adapters, :default_adapter, :test_adapter, :base_data_lambda, :on_nil_context
4
4
 
5
5
  def init
6
6
  validate_adapters!
@@ -29,5 +29,9 @@ class ThisFeature
29
29
  def base_data_lambda
30
30
  @base_data_lambda ||= ->(_record) { {} }
31
31
  end
32
+
33
+ def on_nil_context
34
+ @on_nil_context ||= ->(_flag_name, _caller_location) {}
35
+ end
32
36
  end
33
37
  end
@@ -21,5 +21,13 @@ class ThisFeature
21
21
  def control?
22
22
  adapter.control?(flag_name, context: context, data: data, record: record)
23
23
  end
24
+
25
+ def treatment_value
26
+ adapter.treatment_value(flag_name, context: context, data: data, record: record)
27
+ end
28
+
29
+ def treatment_config
30
+ adapter.treatment_config(flag_name, context: context, data: data, record: record)
31
+ end
24
32
  end
25
33
  end
@@ -1,3 +1,3 @@
1
1
  class ThisFeature
2
- VERSION = '0.9.0'.freeze
2
+ VERSION = '0.11.0'.freeze
3
3
  end
data/lib/this_feature.rb CHANGED
@@ -5,7 +5,20 @@ 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: {}, record: nil)
8
+ OMITTED_CONTEXT = Object.new.freeze
9
+ private_constant :OMITTED_CONTEXT
10
+
11
+ def self.flag(flag_name, context: OMITTED_CONTEXT, data: {}, record: nil)
12
+ if context.nil?
13
+ begin
14
+ configuration.on_nil_context.call(flag_name, caller_locations(1, 1)&.first)
15
+ rescue StandardError => e
16
+ warn("ThisFeature on_nil_context callback failed: #{e.class}: #{e.message}")
17
+ end
18
+ end
19
+ # OMITTED_CONTEXT used to distinguish omission from passing nil; restore nil for the rest of the gem.
20
+ context = nil if context.equal?(OMITTED_CONTEXT)
21
+
9
22
  adapter = adapter_for(flag_name)
10
23
 
11
24
  Flag.new(flag_name, adapter: adapter, context: context, data: data, record: record)
@@ -10,17 +10,16 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.authors = ['Max Pleaner', 'Matt Fong']
12
12
  spec.email = ['maxpleaner@gmail.com', 'matthewjf@gmail.com']
13
- spec.homepage = 'http://hover.to'
14
-
15
- spec.metadata['homepage_uri'] = spec.homepage
16
- spec.metadata['source_code_uri'] = spec.homepage
17
- spec.metadata['rubygems_mfa_required'] = 'true'
18
13
 
19
14
  spec.licenses = ['MIT']
20
15
  spec.summary = 'A ThisFeature adapter to Flipper'
21
16
  spec.description = ''
22
17
  spec.homepage = 'https://github.com/hoverinc/this_feature'
23
18
 
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = spec.homepage
21
+ spec.metadata['rubygems_mfa_required'] = 'true'
22
+
24
23
  spec.files = Dir.glob('{bin/*,lib/**/*,[A-Z]*}')
25
24
  spec.platform = Gem::Platform::RUBY
26
25
  spec.require_paths = ['lib']
@@ -10,17 +10,16 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.authors = ['Max Pleaner', 'Matt Fong']
12
12
  spec.email = ['maxpleaner@gmail.com', 'matthewjf@gmail.com']
13
- spec.homepage = 'http://hover.to'
14
-
15
- spec.metadata['homepage_uri'] = spec.homepage
16
- spec.metadata['source_code_uri'] = spec.homepage
17
- spec.metadata['rubygems_mfa_required'] = 'true'
18
13
 
19
14
  spec.licenses = ['MIT']
20
15
  spec.summary = 'A ThisFeature adapter to Split.io'
21
16
  spec.description = ''
22
17
  spec.homepage = 'https://github.com/hoverinc/this_feature'
23
18
 
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = spec.homepage
21
+ spec.metadata['rubygems_mfa_required'] = 'true'
22
+
24
23
  spec.files = Dir.glob('{bin/*,lib/**/*,[A-Z]*}')
25
24
  spec.platform = Gem::Platform::RUBY
26
25
  spec.require_paths = ['lib']
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: this_feature
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Max Pleaner
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: exe
13
13
  cert_chain: []
14
- date: 2023-05-31 00:00:00.000000000 Z
14
+ date: 2026-05-05 00:00:00.000000000 Z
15
15
  dependencies: []
16
16
  description: ''
17
17
  email:
@@ -23,6 +23,7 @@ executables: []
23
23
  extensions: []
24
24
  extra_rdoc_files: []
25
25
  files:
26
+ - ".github/dependabot.yml"
26
27
  - ".github/workflows/linter-rubocop.yml"
27
28
  - ".github/workflows/test.yml"
28
29
  - ".gitignore"
@@ -78,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
79
  - !ruby/object:Gem::Version
79
80
  version: '0'
80
81
  requirements: []
81
- rubygems_version: 3.4.13
82
+ rubygems_version: 3.4.10
82
83
  signing_key:
83
84
  specification_version: 4
84
85
  summary: A common interface to interact with many feature flag providers