simple_feature_flags 0.1.0 → 1.0.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: e5ef1f22981517d9de71ff3dd332c76b355ea7711b23954f49a1ffe080bbbc3f
4
- data.tar.gz: cd1df44c32ae4a3d320af005f34e08bee655e60912c9463069c112e926de2568
3
+ metadata.gz: 9d732d5d2141046aac302cce21de1487ca8c2d3526a64f7ad1845fc964ab8bec
4
+ data.tar.gz: 98677ad0f773881b575c7e3e3bb1c28b857544bdb6dbe370f8ad1d9f739848f0
5
5
  SHA512:
6
- metadata.gz: 653d6ad9e44a2700b20735feec5ae8e4a7a29ce1882c010ea8de203777579c0849dd32c5aa9660a66e9584d8ef63b806d1e3b883c09304bf51cf5674099887eb
7
- data.tar.gz: 0c3d7c8cbd3a3e2175a80f01cdb4d6c202d108c172451fefcefc06d7d9e0db8a58a87a46b0f8c624ee78f1bab930037493dee7b1feb3c83ec482849e507758f6
6
+ metadata.gz: 003561d885bf3e8ea25c6210c1f727453561c761a22bd08607d5b6e4cbfc36182b168b7ffa901d9c4ebc4e71b3499d1a7d4469ec1fd746c8b62ab4dab47a1abc
7
+ data.tar.gz: a7bc11c251004a5acfb190f435c993d2c8fd02136581ca74035abbdac174ed3345a6ef7f893048698196c437f22fbd925c14a28917f5bffcd18dc243618b9415
@@ -3,7 +3,12 @@
3
3
  "Espago",
4
4
  "autorun",
5
5
  "bindir",
6
+ "concat",
6
7
  "flushdb",
8
+ "hget",
9
+ "hgetall",
10
+ "hset",
11
+ "klass",
7
12
  "optparse",
8
13
  "solargraph",
9
14
  "testtask"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- simple_feature_flags (0.1.0)
4
+ simple_feature_flags (1.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -127,30 +127,144 @@ FEATURE_FLAGS = ::SimpleFeatureFlags::RamStorage.new(config_file)
127
127
 
128
128
  ### Functionality
129
129
 
130
- #### Adding feature flags
130
+ #### Activate a feature
131
131
 
132
- You can add new feature flags programmatically, though we highly encourage you to use the generated `config/simple_feature_flags.yml` file instead. It will make it easier to add and/or remove feature flags automatically on app startup without having to add them manually after merging a branch with new feature flags.
132
+ Activates a feature in the global scope
133
133
 
134
- In case you'd like to add flags programmatically
135
134
  ```ruby
136
- FEATURE_FLAGS.add(:feature_name, 'Description')
137
135
  FEATURE_FLAGS.active?(:feature_name) #=> false
138
136
 
139
- # add a new active flag
140
- FEATURE_FLAGS.add(:active_feature_name, 'Description', true)
141
- FEATURE_FLAGS.active?(:active_feature_name) #=> true
137
+ FEATURE_FLAGS.activate(:feature_name)
138
+
139
+ FEATURE_FLAGS.active?(:feature_name) #=> true
142
140
  ```
143
141
 
144
- #### Removing feature flags
142
+ #### Deactivate a feature
145
143
 
146
- You can remove feature flags programmatically, though we highly encourage you to use the generated `config/simple_feature_flags.yml` file instead. It will make it easier to add and/or remove feature flags automatically on app startup without having to add them manually after merging a branch with new feature flags.
144
+ Deactivates a feature in the global scope
147
145
 
148
- In case you'd like to remove flags programmatically
149
146
  ```ruby
150
- FEATURE_FLAGS.remove(:feature_name)
147
+ FEATURE_FLAGS.active?(:feature_name) #=> true
148
+
149
+ FEATURE_FLAGS.deactivate(:feature_name)
150
+
151
151
  FEATURE_FLAGS.active?(:feature_name) #=> false
152
152
  ```
153
153
 
154
+ #### Activate a feature for a particular record/object
155
+
156
+ ```ruby
157
+ FEATURE_FLAGS.activate_for(:feature_name, User.first) #=> true
158
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
159
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
160
+ ```
161
+
162
+ Note that the flag itself has to be `active` in the global scope for any record/object specific settings to work.
163
+ When the flag is `deactivated` it is completely turned off globally and for every specific record/object.
164
+
165
+ ```ruby
166
+ # The flag is deactivated in the global scope to begin with
167
+ FEATURE_FLAGS.active?(:feature_name) #=> false
168
+
169
+ # We activate it for the first User
170
+ FEATURE_FLAGS.activate_for(:feature_name, User.first)
171
+
172
+ FEATURE_FLAGS.active?(:feature_name) #=> false
173
+ # It is globally `deactivated` though, so the feature stays inactive for all users
174
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
175
+
176
+ # Once we activate the flag in the global scope, record specific settings will be applied
177
+ FEATURE_FLAGS.activate(:feature_name)
178
+
179
+ FEATURE_FLAGS.active?(:feature_name) #=> true
180
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
181
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
182
+
183
+ FEATURE_FLAGS.deactivate(:feature_name)
184
+
185
+ FEATURE_FLAGS.active?(:feature_name) #=> false
186
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
187
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
188
+ ```
189
+
190
+ There is a convenience method `activate_for!`, which activates the feature in the global scope and for specific records/objects at the same time
191
+
192
+ ```ruby
193
+ # The flag is deactivated in the global scope to begin with
194
+ FEATURE_FLAGS.active?(:feature_name) #=> false
195
+
196
+ # We activate it in the global scope and for the first User
197
+ FEATURE_FLAGS.activate_for!(:feature_name, User.first)
198
+
199
+ FEATURE_FLAGS.active?(:feature_name) #=> true
200
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
201
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
202
+ ```
203
+
204
+ A feature that is `active` in the global scope is inactive for all specific records, unless it has been activated for them.
205
+
206
+ ```ruby
207
+ # The flag is active in the global scope to begin with
208
+ FEATURE_FLAGS.active?(:feature_name) #=> true
209
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
210
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
211
+
212
+ FEATURE_FLAGS.activate_for(:feature_name, User.first)
213
+
214
+ FEATURE_FLAGS.active?(:feature_name) #=> true
215
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
216
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
217
+ ```
218
+
219
+ You can also pass an array of objects to activate all of them simultaneously
220
+
221
+ ```ruby
222
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
223
+ FEATURE_FLAGS.active_for?(:feature_name, User.find(2)) #=> false
224
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
225
+
226
+ FEATURE_FLAGS.activate_for(:feature_name, User.first(2))
227
+
228
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
229
+ FEATURE_FLAGS.active_for?(:feature_name, User.find(2)) #=> true
230
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
231
+ ```
232
+
233
+ #### Activate the feature for every record
234
+
235
+ ```ruby
236
+ # The flag is active in the global scope to begin with
237
+ FEATURE_FLAGS.active?(:feature_name) #=> true
238
+ # It is also enabled for the first user
239
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
240
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
241
+
242
+ # We force it onto every user
243
+ FEATURE_FLAGS.activate!(:feature_name)
244
+
245
+ FEATURE_FLAGS.active?(:feature_name) #=> true
246
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
247
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> true
248
+
249
+ # We can easily return to the previous settings
250
+ FEATURE_FLAGS.activate(:feature_name)
251
+
252
+ FEATURE_FLAGS.active?(:feature_name) #=> true
253
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
254
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
255
+ ```
256
+
257
+ #### Deactivate a feature for a particular record/object
258
+
259
+ ```ruby
260
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
261
+
262
+ FEATURE_FLAGS.deactivate_for(:feature_name, User.first)
263
+
264
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
265
+ ```
266
+
267
+
154
268
  #### Run a block of code only when the flag is active
155
269
 
156
270
  There are two ways of running code only when the feature flag is active
@@ -158,18 +272,51 @@ There are two ways of running code only when the feature flag is active
158
272
  ```ruby
159
273
  number = 1
160
274
  if FEATURE_FLAGS.active?(:feature_name)
161
- number += 1
275
+ number += 1
162
276
  end
163
277
 
164
278
  # or using a block
165
279
 
166
280
  # this code will run only when the :feature_name flag is active
167
- FEATURE_FLAGS.with_feature(:feature_name) do
168
- number += 1
281
+ FEATURE_FLAGS.when_active(:feature_name) do
282
+ number += 1
169
283
  end
170
284
 
171
285
  # feature flags that don't exist will return false
172
286
  FEATURE_FLAGS.active?(:non_existant) #=> false
287
+
288
+ if FEATURE_FLAGS.active_for?(:feature_name, User.first)
289
+ number += 1
290
+ end
291
+
292
+ # this code will run only if the :feature_name flag is active for the first User
293
+ FEATURE_FLAGS.when_active_for(:feature_name, User.first) do
294
+ number += 1
295
+ end
296
+ ```
297
+
298
+ #### Adding feature flags
299
+
300
+ You can add new feature flags programmatically, though we highly encourage you to use the generated `config/simple_feature_flags.yml` file instead. It will make it easier to add and/or remove feature flags automatically on app startup without having to add them manually after merging a branch with new feature flags.
301
+
302
+ In case you'd like to add flags programmatically
303
+ ```ruby
304
+ FEATURE_FLAGS.add(:feature_name, 'Description')
305
+ FEATURE_FLAGS.active?(:feature_name) #=> false
306
+
307
+ # add a new active flag
308
+ FEATURE_FLAGS.add(:active_feature_name, 'Description', true)
309
+ FEATURE_FLAGS.active?(:active_feature_name) #=> true
310
+ ```
311
+
312
+ #### Removing feature flags
313
+
314
+ You can remove feature flags programmatically, though we highly encourage you to use the generated `config/simple_feature_flags.yml` file instead. It will make it easier to add and/or remove feature flags automatically on app startup without having to add them manually after merging a branch with new feature flags.
315
+
316
+ In case you'd like to remove flags programmatically
317
+ ```ruby
318
+ FEATURE_FLAGS.remove(:feature_name)
319
+ FEATURE_FLAGS.active?(:feature_name) #=> false
173
320
  ```
174
321
 
175
322
 
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module SimpleFeatureFlags
6
+ NOT_PRESENT = ::Object.new.freeze
7
+
4
8
  class NoSuchCommandError < StandardError; end
5
9
 
6
10
  class IncorrectWorkingDirectoryError < StandardError; end
@@ -17,6 +17,22 @@ module SimpleFeatureFlags
17
17
  __active__(feature)
18
18
  end
19
19
 
20
+ def active_globally?(feature)
21
+ flags.dig(feature.to_sym, 'active') == 'globally'
22
+ end
23
+
24
+ def active_for?(feature, object, object_id_method = :id)
25
+ return false unless active?(feature)
26
+ return true if active_globally?(feature)
27
+
28
+ active_objects_hash = active_objects(feature)
29
+ active_ids = active_objects_hash[object.class.to_s]
30
+
31
+ return false unless active_ids
32
+
33
+ active_ids.include? object.public_send(object_id_method)
34
+ end
35
+
20
36
  def exists?(feature)
21
37
  return false if [nil, ''].include? flags[feature.to_sym]
22
38
 
@@ -27,12 +43,26 @@ module SimpleFeatureFlags
27
43
  flags.dig(feature.to_sym, 'description')
28
44
  end
29
45
 
30
- def with_feature(feature, ignore_file = false, &block)
46
+ def when_active(feature, ignore_file = false, &block)
31
47
  return unless active?(feature, ignore_file)
32
48
 
33
49
  block.call
34
50
  end
35
51
 
52
+ def when_active_for(feature, object, object_id_method = :id, &block)
53
+ return unless active_for?(feature, object, object_id_method)
54
+
55
+ block.call
56
+ end
57
+
58
+ def activate!(feature)
59
+ return false unless exists?(feature)
60
+
61
+ flags[feature.to_sym]['active'] = 'globally'
62
+
63
+ true
64
+ end
65
+
36
66
  def activate(feature)
37
67
  return false unless exists?(feature)
38
68
 
@@ -41,6 +71,39 @@ module SimpleFeatureFlags
41
71
  true
42
72
  end
43
73
 
74
+ def activate_for(feature, objects, object_id_method = :id)
75
+ return false unless exists?(feature)
76
+
77
+ objects = [objects] unless objects.is_a? ::Array
78
+ to_activate_hash = objects_to_hash(objects, object_id_method)
79
+ active_objects_hash = active_objects(feature)
80
+
81
+ to_activate_hash.each do |klass, ids|
82
+ (active_objects_hash[klass] = ids) && next unless active_objects_hash[klass]
83
+
84
+ active_objects_hash[klass].concat(ids).sort!
85
+ end
86
+
87
+ flags[feature.to_sym]['active_for_objects'] = active_objects_hash
88
+
89
+ true
90
+ end
91
+
92
+ def activate_for!(feature, objects, object_id_method = :id)
93
+ return false unless activate_for(feature, objects, object_id_method)
94
+
95
+ activate(feature)
96
+ end
97
+
98
+ def deactivate!(feature)
99
+ return false unless exists?(feature)
100
+
101
+ flags[feature.to_sym]['active'] = 'false'
102
+ flags[feature.to_sym]['active_for_objects'] = nil
103
+
104
+ true
105
+ end
106
+
44
107
  def deactivate(feature)
45
108
  return false unless exists?(feature)
46
109
 
@@ -49,6 +112,29 @@ module SimpleFeatureFlags
49
112
  true
50
113
  end
51
114
 
115
+ def active_objects(feature)
116
+ flags.dig(feature.to_sym, 'active_for_objects') || {}
117
+ end
118
+
119
+ def deactivate_for(feature, objects, object_id_method = :id)
120
+ return false unless exists?(feature)
121
+
122
+ active_objects_hash = active_objects(feature)
123
+
124
+ objects_to_deactivate_hash = objects_to_hash(objects, object_id_method)
125
+
126
+ objects_to_deactivate_hash.each do |klass, ids_to_remove|
127
+ active_ids = active_objects_hash[klass]
128
+ next unless active_ids
129
+
130
+ active_ids.reject! { |id| ids_to_remove.include? id }
131
+ end
132
+
133
+ flags[feature.to_sym]['active_for_objects'] = active_objects_hash
134
+
135
+ true
136
+ end
137
+
52
138
  def get(feature)
53
139
  return unless exists?(feature)
54
140
 
@@ -64,6 +150,8 @@ module SimpleFeatureFlags
64
150
  active = case active
65
151
  when true, 'true'
66
152
  'true'
153
+ when 'globally', :globally
154
+ 'globally'
67
155
  else
68
156
  'false'
69
157
  end
@@ -102,13 +190,19 @@ module SimpleFeatureFlags
102
190
 
103
191
  private
104
192
 
193
+ def objects_to_hash(objects, object_id_method = :id)
194
+ objects = [objects] unless objects.is_a? ::Array
195
+
196
+ objects.group_by { |ob| ob.class.to_s }.transform_values { |arr| arr.map(&object_id_method) }
197
+ end
198
+
105
199
  def __active__(feature)
106
- flags.dig(feature.to_sym, 'active') == 'true'
200
+ %w[true globally].include? flags.dig(feature.to_sym, 'active')
107
201
  end
108
202
 
109
203
  def import_flags_from_file
110
204
  changes = YAML.load_file(file)
111
- changes = { mandatory: [], remove: [] } unless changes.is_a? Hash
205
+ changes = { mandatory: [], remove: [] } unless changes.is_a? ::Hash
112
206
 
113
207
  changes[:mandatory].each do |el|
114
208
  mandatory_flags << el['name']
@@ -16,6 +16,27 @@ module SimpleFeatureFlags
16
16
  __active__(feature)
17
17
  end
18
18
 
19
+ def active_globally?(feature)
20
+ case redis.hget(feature.to_s, 'active')
21
+ when 'globally'
22
+ true
23
+ else
24
+ false
25
+ end
26
+ end
27
+
28
+ def active_for?(feature, object, object_id_method = :id)
29
+ return false unless active?(feature)
30
+ return true if active_globally?(feature)
31
+
32
+ active_objects_hash = active_objects(feature)
33
+ active_ids = active_objects_hash[object.class.to_s]
34
+
35
+ return false unless active_ids
36
+
37
+ active_ids.include? object.public_send(object_id_method)
38
+ end
39
+
19
40
  def exists?(feature)
20
41
  return false if [nil, ''].include? redis.hget(feature.to_s, 'name')
21
42
 
@@ -26,12 +47,28 @@ module SimpleFeatureFlags
26
47
  redis.hget(feature.to_s, 'description')
27
48
  end
28
49
 
29
- def with_feature(feature, _ignore_file = false, &block)
50
+ def when_active(feature, _ignore_file = false, &block)
30
51
  return unless active?(feature)
31
52
 
32
53
  block.call
33
54
  end
34
55
 
56
+ def when_active_for(feature, object, object_id_method = :id, &block)
57
+ return unless active_for?(feature, object, object_id_method)
58
+
59
+ block.call
60
+ end
61
+
62
+ def activate!(feature)
63
+ return false unless exists?(feature)
64
+
65
+ redis.hset(feature.to_s, 'active', 'globally')
66
+
67
+ true
68
+ end
69
+
70
+ alias activate_globally activate!
71
+
35
72
  def activate(feature)
36
73
  return false unless exists?(feature)
37
74
 
@@ -40,6 +77,39 @@ module SimpleFeatureFlags
40
77
  true
41
78
  end
42
79
 
80
+ def activate_for(feature, objects, object_id_method = :id)
81
+ return false unless exists?(feature)
82
+
83
+ objects = [objects] unless objects.is_a? ::Array
84
+ to_activate_hash = objects_to_hash(objects, object_id_method)
85
+ active_objects_hash = active_objects(feature)
86
+
87
+ to_activate_hash.each do |klass, ids|
88
+ (active_objects_hash[klass] = ids) && next unless active_objects_hash[klass]
89
+
90
+ active_objects_hash[klass].concat(ids).sort!
91
+ end
92
+
93
+ redis.hset(feature.to_s, 'active_for_objects', active_objects_hash.to_json)
94
+
95
+ true
96
+ end
97
+
98
+ def activate_for!(feature, objects, object_id_method = :id)
99
+ return false unless activate_for(feature, objects, object_id_method)
100
+
101
+ activate(feature)
102
+ end
103
+
104
+ def deactivate!(feature)
105
+ return false unless exists?(feature)
106
+
107
+ redis.hset(feature.to_s, 'active', 'false')
108
+ redis.hset(feature.to_s, 'active_for_objects', '')
109
+
110
+ true
111
+ end
112
+
43
113
  def deactivate(feature)
44
114
  return false unless exists?(feature)
45
115
 
@@ -48,11 +118,37 @@ module SimpleFeatureFlags
48
118
  true
49
119
  end
50
120
 
121
+ def active_objects(feature)
122
+ ::JSON.parse(redis.hget(feature.to_s, 'active_for_objects').to_s)
123
+ rescue ::JSON::ParserError
124
+ {}
125
+ end
126
+
127
+ def deactivate_for(feature, objects, object_id_method = :id)
128
+ return false unless exists?(feature)
129
+
130
+ active_objects_hash = active_objects(feature)
131
+
132
+ objects_to_deactivate_hash = objects_to_hash(objects, object_id_method)
133
+
134
+ objects_to_deactivate_hash.each do |klass, ids_to_remove|
135
+ active_ids = active_objects_hash[klass]
136
+ next unless active_ids
137
+
138
+ active_ids.reject! { |id| ids_to_remove.include? id }
139
+ end
140
+
141
+ redis.hset(feature.to_s, 'active_for_objects', active_objects_hash.to_json)
142
+
143
+ true
144
+ end
145
+
51
146
  def get(feature)
52
147
  return unless exists?(feature)
53
148
 
54
149
  hash = redis.hgetall(feature.to_s)
55
150
  hash['mandatory'] = mandatory_flags.include?(feature.to_s)
151
+ hash['active_for_objects'] = ::JSON.parse(hash['active_for_objects']) rescue {}
56
152
 
57
153
  hash
58
154
  end
@@ -63,6 +159,8 @@ module SimpleFeatureFlags
63
159
  active = case active
64
160
  when true, 'true'
65
161
  'true'
162
+ when 'globally', :globally
163
+ 'globally'
66
164
  else
67
165
  'false'
68
166
  end
@@ -105,9 +203,15 @@ module SimpleFeatureFlags
105
203
 
106
204
  private
107
205
 
206
+ def objects_to_hash(objects, object_id_method = :id)
207
+ objects = [objects] unless objects.is_a? ::Array
208
+
209
+ objects.group_by { |ob| ob.class.to_s }.transform_values { |arr| arr.map(&object_id_method) }
210
+ end
211
+
108
212
  def __active__(feature)
109
213
  case redis.hget(feature.to_s, 'active')
110
- when 'true'
214
+ when 'true', 'globally'
111
215
  true
112
216
  when 'false'
113
217
  false
@@ -116,7 +220,7 @@ module SimpleFeatureFlags
116
220
 
117
221
  def import_flags_from_file
118
222
  changes = YAML.load_file(file)
119
- changes = { mandatory: [], remove: [] } unless changes.is_a? Hash
223
+ changes = { mandatory: [], remove: [] } unless changes.is_a? ::Hash
120
224
 
121
225
  changes[:mandatory].each do |el|
122
226
  mandatory_flags << el['name']
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleFeatureFlags
4
- VERSION = "0.1.0"
4
+ VERSION = "1.0.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_feature_flags
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Espago
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2021-08-13 00:00:00.000000000 Z
12
+ date: 2021-08-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler