magick-feature-flags 0.7.2 → 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: ec2f109c9d472dc4f5718140075e9ce57f5479e851e347096f59315e8141429f
4
- data.tar.gz: 44cb1519755b8d51f397a6103e22822cadff80a663e58b1bc0f70dafa732a7e4
3
+ metadata.gz: a5124df88b7d429811d4b0f131d01aeeef2dedabc25b598da96fa5f01eb07163
4
+ data.tar.gz: 72c12e81d29990d6990f0f749effb978c7f9a4d7a316d3455e8ceb508269889d
5
5
  SHA512:
6
- metadata.gz: 198465d8f50acbbd62e19d0683a3bca9e20ec76c9e72aa8e00873da438e37f2e39efaf7d6740b618f8132c24b5f03b456d1a06b38b0a561d8f89a2e212d02ab7
7
- data.tar.gz: a6085c886c4e0461039f6412c14f3d302d7ebe8e50b80e1a65e9cc416cf6ab5bc3964d32c9c372e6b149a1295d0f59393926f779ea0ed84622d21acf08a219c5
6
+ metadata.gz: 880f57facbd900c562aa10d21708afe0100087a6160ac814cc9d3453233d62cb7b41de1bfe18a831f27ecbd14cfb2b59b238a92f91c4a8fa56d0ae30a62ba125
7
+ data.tar.gz: cd4addd623241b24d2bc3d08cab687233de382d3c1dc260461484e6b750c637f19dc51368c552694c021f503a30c85e1425016f1af4a7056b8f3cf19f816a299
data/README.md CHANGED
@@ -166,6 +166,55 @@ feature.enable_for_ip_addresses('192.168.1.0/24', '10.0.0.1')
166
166
  feature.enable_for_custom_attribute(:subscription_tier, ['premium', 'enterprise'])
167
167
  ```
168
168
 
169
+ ### Checking Feature Enablement with Objects
170
+
171
+ You can check if a feature is enabled for an object (like a User model) and its fields:
172
+
173
+ ```ruby
174
+ # Using enabled_for? with an object
175
+ user = User.find(123)
176
+ if Magick.enabled_for?(:premium_features, user)
177
+ # Feature is enabled for this user
178
+ end
179
+
180
+ # Or using the feature directly
181
+ feature = Magick[:premium_features]
182
+ if feature.enabled_for?(user)
183
+ # Feature is enabled for this user
184
+ end
185
+
186
+ # With additional context
187
+ if Magick.enabled_for?(:premium_features, user, ip_address: request.remote_ip)
188
+ # Feature is enabled for this user and IP
189
+ end
190
+
191
+ # Works with ActiveRecord objects, hashes, or simple IDs
192
+ Magick.enabled_for?(:feature, user) # ActiveRecord object
193
+ Magick.enabled_for?(:feature, { id: 123, role: 'admin' }) # Hash
194
+ Magick.enabled_for?(:feature, 123) # Simple ID
195
+ ```
196
+
197
+ The `enabled_for?` method automatically extracts:
198
+ - `user_id` from `id` or `user_id` attribute
199
+ - `group` from `group` attribute
200
+ - `role` from `role` attribute
201
+ - `ip_address` from `ip_address` attribute
202
+ - All other attributes for custom attribute matching
203
+
204
+ ### Return Values
205
+
206
+ All enable/disable methods now return `true` to indicate success:
207
+
208
+ ```ruby
209
+ # All these methods return true on success
210
+ result = feature.enable # => true
211
+ result = feature.disable # => true
212
+ result = feature.enable_for_user(123) # => true
213
+ result = feature.enable_for_group('beta') # => true
214
+ result = feature.enable_percentage_of_users(25) # => true
215
+ result = feature.set_value(true) # => true
216
+ ```
217
+
169
218
  ### DSL Configuration
170
219
 
171
220
  Create `config/features.rb`:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Magick Configuration
2
4
  # Generated by: rails generate magick:install
3
5
  #
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'thread'
4
-
5
3
  module Magick
6
4
  module Adapters
7
5
  class Memory < Base
@@ -3,7 +3,7 @@
3
3
  module Magick
4
4
  module Adapters
5
5
  class Registry
6
- CACHE_INVALIDATION_CHANNEL = 'magick:cache:invalidate'.freeze
6
+ CACHE_INVALIDATION_CHANNEL = 'magick:cache:invalidate'
7
7
 
8
8
  def initialize(memory_adapter, redis_adapter = nil, circuit_breaker: nil, async: false)
9
9
  @memory_adapter = memory_adapter
@@ -43,36 +43,36 @@ module Magick
43
43
  memory_adapter.set(feature_name, key, value)
44
44
 
45
45
  # Update Redis if available
46
- if redis_adapter
47
- update_redis = proc do
48
- circuit_breaker.call do
49
- redis_adapter.set(feature_name, key, value)
50
- # Publish cache invalidation message to notify other processes
51
- publish_cache_invalidation(feature_name)
52
- end
53
- rescue AdapterError => e
54
- # Log error but don't fail - memory is updated
55
- warn "Failed to update Redis: #{e.message}" if defined?(Rails) && Rails.env.development?
56
- end
46
+ return unless redis_adapter
57
47
 
58
- if @async && defined?(Thread)
59
- Thread.new { update_redis.call }
60
- else
61
- update_redis.call
48
+ update_redis = proc do
49
+ circuit_breaker.call do
50
+ redis_adapter.set(feature_name, key, value)
51
+ # Publish cache invalidation message to notify other processes
52
+ publish_cache_invalidation(feature_name)
62
53
  end
54
+ rescue AdapterError => e
55
+ # Log error but don't fail - memory is updated
56
+ warn "Failed to update Redis: #{e.message}" if defined?(Rails) && Rails.env.development?
57
+ end
58
+
59
+ if @async && defined?(Thread)
60
+ Thread.new { update_redis.call }
61
+ else
62
+ update_redis.call
63
63
  end
64
64
  end
65
65
 
66
66
  def delete(feature_name)
67
67
  memory_adapter.delete(feature_name)
68
- if redis_adapter
69
- begin
70
- redis_adapter.delete(feature_name)
71
- # Publish cache invalidation message
72
- publish_cache_invalidation(feature_name)
73
- rescue AdapterError
74
- # Continue even if Redis fails
75
- end
68
+ return unless redis_adapter
69
+
70
+ begin
71
+ redis_adapter.delete(feature_name)
72
+ # Publish cache invalidation message
73
+ publish_cache_invalidation(feature_name)
74
+ rescue AdapterError
75
+ # Continue even if Redis fails
76
76
  end
77
77
  end
78
78
 
@@ -108,23 +108,21 @@ module Magick
108
108
  return unless redis_adapter && defined?(Thread)
109
109
 
110
110
  @subscriber_thread = Thread.new do
111
- begin
112
- redis_client = redis_adapter.instance_variable_get(:@redis)
113
- return unless redis_client
114
-
115
- @subscriber = redis_client.dup
116
- @subscriber.subscribe(CACHE_INVALIDATION_CHANNEL) do |on|
117
- on.message do |_channel, feature_name|
118
- # Invalidate memory cache for this feature
119
- memory_adapter.delete(feature_name)
120
- end
111
+ redis_client = redis_adapter.instance_variable_get(:@redis)
112
+ return unless redis_client
113
+
114
+ @subscriber = redis_client.dup
115
+ @subscriber.subscribe(CACHE_INVALIDATION_CHANNEL) do |on|
116
+ on.message do |_channel, feature_name|
117
+ # Invalidate memory cache for this feature
118
+ memory_adapter.delete(feature_name)
121
119
  end
122
- rescue StandardError => e
123
- # If subscription fails, log and retry after a delay
124
- warn "Cache invalidation subscriber error: #{e.message}" if defined?(Rails) && Rails.env.development?
125
- sleep 5
126
- retry
127
120
  end
121
+ rescue StandardError => e
122
+ # If subscription fails, log and retry after a delay
123
+ warn "Cache invalidation subscriber error: #{e.message}" if defined?(Rails) && Rails.env.development?
124
+ sleep 5
125
+ retry
128
126
  end
129
127
  @subscriber_thread.abort_on_exception = false
130
128
  end
data/lib/magick/config.rb CHANGED
@@ -2,9 +2,8 @@
2
2
 
3
3
  module Magick
4
4
  class Config
5
- attr_accessor :adapter_registry, :performance_metrics, :audit_log, :versioning
6
- attr_accessor :warn_on_deprecated, :async_updates, :memory_ttl, :circuit_breaker_threshold
7
- attr_accessor :circuit_breaker_timeout, :redis_url, :redis_namespace, :environment
5
+ attr_accessor :adapter_registry, :performance_metrics, :audit_log, :versioning, :warn_on_deprecated,
6
+ :async_updates, :memory_ttl, :circuit_breaker_threshold, :circuit_breaker_timeout, :redis_url, :redis_namespace, :environment
8
7
 
9
8
  def initialize
10
9
  @warn_on_deprecated = false
@@ -45,28 +44,18 @@ module Magick
45
44
  configure_redis_adapter(url: url, namespace: namespace, **options)
46
45
  end
47
46
 
48
- def performance_metrics(enabled: true, **options)
49
- if enabled
50
- @performance_metrics = PerformanceMetrics.new
51
- else
52
- @performance_metrics = nil
53
- end
47
+ def performance_metrics(enabled: true, **_options)
48
+ @performance_metrics = (PerformanceMetrics.new if enabled)
54
49
  end
55
50
 
56
51
  def audit_log(enabled: true, adapter: nil)
57
- if enabled
58
- @audit_log = adapter ? AuditLog.new(adapter) : AuditLog.new
59
- else
60
- @audit_log = nil
61
- end
52
+ @audit_log = if enabled
53
+ adapter ? AuditLog.new(adapter) : AuditLog.new
54
+ end
62
55
  end
63
56
 
64
57
  def versioning(enabled: true)
65
- if enabled
66
- @versioning = Versioning.new(adapter_registry || default_adapter_registry)
67
- else
68
- @versioning = nil
69
- end
58
+ @versioning = (Versioning.new(adapter_registry || default_adapter_registry) if enabled)
70
59
  end
71
60
 
72
61
  def circuit_breaker(threshold: nil, timeout: nil)
data/lib/magick/dsl.rb CHANGED
@@ -70,6 +70,15 @@ module Magick
70
70
  def disable_feature(feature_name, user_id: nil)
71
71
  Magick[feature_name].disable(user_id: user_id)
72
72
  end
73
+
74
+ # Check enablement for an object
75
+ def enabled_for?(feature_name, object, **additional_context)
76
+ Magick.enabled_for?(feature_name, object, **additional_context)
77
+ end
78
+
79
+ def disabled_for?(feature_name, object, **additional_context)
80
+ Magick.disabled_for?(feature_name, object, **additional_context)
81
+ end
73
82
  end
74
83
  end
75
84
 
@@ -5,7 +5,7 @@ require 'json'
5
5
  module Magick
6
6
  class ExportImport
7
7
  def self.export(features_hash)
8
- result = features_hash.map do |name, feature|
8
+ result = features_hash.map do |_name, feature|
9
9
  feature.to_h
10
10
  end
11
11
 
@@ -45,7 +45,9 @@ module Magick
45
45
  description: feature_data['description'] || feature_data[:description]
46
46
  )
47
47
 
48
- feature.set_value(feature_data['value'] || feature_data[:value]) if feature_data['value'] || feature_data[:value]
48
+ if feature_data['value'] || feature_data[:value]
49
+ feature.set_value(feature_data['value'] || feature_data[:value])
50
+ end
49
51
 
50
52
  # Import targeting
51
53
  if feature_data['targeting'] || feature_data[:targeting]
@@ -69,28 +69,20 @@ module Magick
69
69
  return false if status == :deprecated && !context[:allow_deprecated]
70
70
 
71
71
  # Check feature dependencies
72
- unless dependencies.empty?
73
- return false unless dependencies.all? { |dep_name| Magick.enabled?(dep_name, context) }
74
- end
72
+ return false if !dependencies.empty? && !dependencies.all? { |dep_name| Magick.enabled?(dep_name, context) }
75
73
 
76
74
  # Check date/time range targeting
77
- if targeting[:date_range] && !date_range_active?(targeting[:date_range])
78
- return false
79
- end
75
+ return false if targeting[:date_range] && !date_range_active?(targeting[:date_range])
80
76
 
81
77
  # Check IP address targeting
82
- if targeting[:ip_address] && context[:ip_address]
83
- return false unless ip_address_matches?(context[:ip_address])
84
- end
78
+ return false if targeting[:ip_address] && context[:ip_address] && !ip_address_matches?(context[:ip_address])
85
79
 
86
80
  # Check custom attributes
87
- if targeting[:custom_attributes]
88
- return false unless custom_attributes_match?(context, targeting[:custom_attributes])
89
- end
81
+ return false if targeting[:custom_attributes] && !custom_attributes_match?(context, targeting[:custom_attributes])
90
82
 
91
83
  # Check complex conditions
92
- if targeting[:complex_conditions]
93
- return false unless complex_conditions_match?(context, targeting[:complex_conditions])
84
+ if targeting[:complex_conditions] && !complex_conditions_match?(context, targeting[:complex_conditions])
85
+ return false
94
86
  end
95
87
 
96
88
  value = get_value(context)
@@ -100,7 +92,7 @@ module Magick
100
92
  when :string
101
93
  !value.nil? && value != ''
102
94
  when :number
103
- value.to_f > 0
95
+ value.to_f.positive?
104
96
  else
105
97
  false
106
98
  end
@@ -110,6 +102,18 @@ module Magick
110
102
  !enabled?(context)
111
103
  end
112
104
 
105
+ def enabled_for?(object, **additional_context)
106
+ # Extract context from object
107
+ context = extract_context_from_object(object)
108
+ # Merge with any additional context provided
109
+ context.merge!(additional_context)
110
+ enabled?(context)
111
+ end
112
+
113
+ def disabled_for?(object, **additional_context)
114
+ !enabled_for?(object, **additional_context)
115
+ end
116
+
113
117
  def value(context = {})
114
118
  get_value(context)
115
119
  end
@@ -125,26 +129,32 @@ module Magick
125
129
 
126
130
  def enable_for_user(user_id)
127
131
  enable_targeting(:user, user_id)
132
+ true
128
133
  end
129
134
 
130
135
  def disable_for_user(user_id)
131
136
  disable_targeting(:user, user_id)
137
+ true
132
138
  end
133
139
 
134
140
  def enable_for_group(group_name)
135
141
  enable_targeting(:group, group_name)
142
+ true
136
143
  end
137
144
 
138
145
  def disable_for_group(group_name)
139
146
  disable_targeting(:group, group_name)
147
+ true
140
148
  end
141
149
 
142
150
  def enable_for_role(role_name)
143
151
  enable_targeting(:role, role_name)
152
+ true
144
153
  end
145
154
 
146
155
  def disable_for_role(role_name)
147
156
  disable_targeting(:role, role_name)
157
+ true
148
158
  end
149
159
 
150
160
  def enable_percentage_of_users(percentage)
@@ -152,18 +162,19 @@ module Magick
152
162
  save_targeting
153
163
 
154
164
  # Update registered feature instance if it exists
155
- if Magick.features.key?(name)
156
- Magick.features[name].instance_variable_set(:@targeting, @targeting.dup)
157
- end
165
+ Magick.features[name].instance_variable_set(:@targeting, @targeting.dup) if Magick.features.key?(name)
158
166
 
159
167
  # Rails 8+ event
160
168
  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
161
169
  Magick::Rails::Events.targeting_added(name, targeting_type: :percentage_users, targeting_value: percentage)
162
170
  end
171
+
172
+ true
163
173
  end
164
174
 
165
175
  def disable_percentage_of_users
166
176
  disable_targeting(:percentage_users)
177
+ true
167
178
  end
168
179
 
169
180
  def enable_percentage_of_requests(percentage)
@@ -171,40 +182,46 @@ module Magick
171
182
  save_targeting
172
183
 
173
184
  # Update registered feature instance if it exists
174
- if Magick.features.key?(name)
175
- Magick.features[name].instance_variable_set(:@targeting, @targeting.dup)
176
- end
185
+ Magick.features[name].instance_variable_set(:@targeting, @targeting.dup) if Magick.features.key?(name)
177
186
 
178
187
  # Rails 8+ event
179
188
  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
180
189
  Magick::Rails::Events.targeting_added(name, targeting_type: :percentage_requests, targeting_value: percentage)
181
190
  end
191
+
192
+ true
182
193
  end
183
194
 
184
195
  def disable_percentage_of_requests
185
196
  disable_targeting(:percentage_requests)
197
+ true
186
198
  end
187
199
 
188
200
  def enable_for_date_range(start_date, end_date)
189
201
  enable_targeting(:date_range, { start: start_date, end: end_date })
202
+ true
190
203
  end
191
204
 
192
205
  def disable_date_range
193
206
  disable_targeting(:date_range)
207
+ true
194
208
  end
195
209
 
196
210
  def enable_for_ip_addresses(ip_addresses)
197
211
  enable_targeting(:ip_address, Array(ip_addresses))
212
+ true
198
213
  end
199
214
 
200
215
  def disable_ip_addresses
201
216
  disable_targeting(:ip_address)
217
+ true
202
218
  end
203
219
 
204
220
  def enable_for_custom_attribute(attribute_name, values, operator: :equals)
205
221
  custom_attrs = targeting[:custom_attributes] || {}
206
222
  custom_attrs[attribute_name.to_sym] = { values: Array(values), operator: operator }
207
223
  enable_targeting(:custom_attributes, custom_attrs)
224
+ true
208
225
  end
209
226
 
210
227
  def disable_custom_attribute(attribute_name)
@@ -215,6 +232,7 @@ module Magick
215
232
  else
216
233
  enable_targeting(:custom_attributes, custom_attrs)
217
234
  end
235
+ true
218
236
  end
219
237
 
220
238
  def set_variants(variants)
@@ -227,6 +245,8 @@ module Magick
227
245
  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
228
246
  Magick::Rails::Events.variant_set(name, variants: variants_array)
229
247
  end
248
+
249
+ true
230
250
  end
231
251
 
232
252
  def add_dependency(dependency_name)
@@ -237,6 +257,8 @@ module Magick
237
257
  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
238
258
  Magick::Rails::Events.dependency_added(name, dependency_name)
239
259
  end
260
+
261
+ true
240
262
  end
241
263
 
242
264
  def remove_dependency(dependency_name)
@@ -246,6 +268,8 @@ module Magick
246
268
  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
247
269
  Magick::Rails::Events.dependency_removed(name, dependency_name)
248
270
  end
271
+
272
+ true
249
273
  end
250
274
 
251
275
  def dependencies
@@ -257,26 +281,26 @@ module Magick
257
281
 
258
282
  variants = targeting[:variants]
259
283
  selected_variant = if variants.length == 1
260
- variants.first[:name]
261
- else
262
- # Weighted random selection
263
- total_weight = variants.sum { |v| v[:weight] || 0 }
264
- if total_weight == 0
265
- variants.first[:name]
266
- else
267
- random = rand(total_weight)
268
- current = 0
269
- selected = nil
270
- variants.each do |variant|
271
- current += (variant[:weight] || 0)
272
- if random < current
273
- selected = variant[:name]
274
- break
275
- end
276
- end
277
- selected || variants.first[:name]
278
- end
279
- end
284
+ variants.first[:name]
285
+ else
286
+ # Weighted random selection
287
+ total_weight = variants.sum { |v| v[:weight] || 0 }
288
+ if total_weight.zero?
289
+ variants.first[:name]
290
+ else
291
+ random = rand(total_weight)
292
+ current = 0
293
+ selected = nil
294
+ variants.each do |variant|
295
+ current += variant[:weight] || 0
296
+ if random < current
297
+ selected = variant[:name]
298
+ break
299
+ end
300
+ end
301
+ selected || variants.first[:name]
302
+ end
303
+ end
280
304
 
281
305
  # Rails 8+ event
282
306
  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
@@ -297,27 +321,25 @@ module Magick
297
321
  @stored_value = value
298
322
 
299
323
  # Update registered feature instance if it exists
300
- if Magick.features.key?(name)
301
- Magick.features[name].instance_variable_set(:@stored_value, value)
302
- end
324
+ Magick.features[name].instance_variable_set(:@stored_value, value) if Magick.features.key?(name)
303
325
 
304
326
  changes = { value: { from: old_value, to: value } }
305
327
 
306
328
  # Audit log
307
- if Magick.audit_log
308
- Magick.audit_log.log(
309
- name,
310
- 'set_value',
311
- user_id: user_id,
312
- changes: changes
313
- )
314
- end
329
+ Magick.audit_log&.log(
330
+ name,
331
+ 'set_value',
332
+ user_id: user_id,
333
+ changes: changes
334
+ )
315
335
 
316
336
  # Rails 8+ events
317
337
  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
318
338
  Magick::Rails::Events.feature_changed(name, changes: changes, user_id: user_id)
319
339
  Magick::Rails::Events.audit_logged(name, action: 'set_value', user_id: user_id, changes: changes)
320
340
  end
341
+
342
+ true
321
343
  end
322
344
 
323
345
  def enable(user_id: nil)
@@ -329,9 +351,9 @@ module Magick
329
351
  when :boolean
330
352
  set_value(true, user_id: user_id)
331
353
  when :string
332
- raise InvalidFeatureValueError, "Cannot enable string feature. Use set_value instead."
354
+ raise InvalidFeatureValueError, 'Cannot enable string feature. Use set_value instead.'
333
355
  when :number
334
- raise InvalidFeatureValueError, "Cannot enable number feature. Use set_value instead."
356
+ raise InvalidFeatureValueError, 'Cannot enable number feature. Use set_value instead.'
335
357
  else
336
358
  raise InvalidFeatureValueError, "Cannot enable feature of type #{type}"
337
359
  end
@@ -340,6 +362,8 @@ module Magick
340
362
  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
341
363
  Magick::Rails::Events.feature_enabled_globally(name, user_id: user_id)
342
364
  end
365
+
366
+ true
343
367
  end
344
368
 
345
369
  def disable(user_id: nil)
@@ -362,6 +386,8 @@ module Magick
362
386
  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
363
387
  Magick::Rails::Events.feature_disabled_globally(name, user_id: user_id)
364
388
  end
389
+
390
+ true
365
391
  end
366
392
 
367
393
  def set_status(new_status)
@@ -369,6 +395,7 @@ module Magick
369
395
 
370
396
  @status = new_status.to_sym
371
397
  adapter_registry.set(name, 'status', status)
398
+ true
372
399
  end
373
400
 
374
401
  def delete
@@ -377,6 +404,7 @@ module Magick
377
404
  @targeting = {}
378
405
  # Also remove from Magick.features if registered
379
406
  Magick.features.delete(name.to_s)
407
+ true
380
408
  end
381
409
 
382
410
  def to_h
@@ -438,7 +466,7 @@ module Magick
438
466
 
439
467
  case type
440
468
  when :boolean
441
- value == true || value == 'true' || value == 1
469
+ [true, 'true', 1].include?(value)
442
470
  when :string
443
471
  value.to_s
444
472
  when :number
@@ -588,6 +616,45 @@ module Magick
588
616
  (hash_value % 100) < percentage
589
617
  end
590
618
 
619
+ def extract_context_from_object(object)
620
+ context = {}
621
+
622
+ # Handle hash/struct-like objects
623
+ if object.is_a?(Hash)
624
+ context[:user_id] = object[:user_id] || object['user_id'] || object[:id] || object['id']
625
+ context[:group] = object[:group] || object['group']
626
+ context[:role] = object[:role] || object['role']
627
+ context[:ip_address] = object[:ip_address] || object['ip_address']
628
+ # Include all other attributes for custom attribute matching
629
+ object.each do |key, value|
630
+ next if %i[user_id id group role ip_address].include?(key.to_sym)
631
+ next if %w[user_id id group role ip_address].include?(key.to_s)
632
+
633
+ context[key.to_sym] = value
634
+ end
635
+ # Handle ActiveRecord-like objects (respond to methods)
636
+ elsif object.respond_to?(:id) || object.respond_to?(:user_id)
637
+ context[:user_id] = object.respond_to?(:user_id) ? object.user_id : object.id
638
+ context[:group] = object.group if object.respond_to?(:group)
639
+ context[:role] = object.role if object.respond_to?(:role)
640
+ context[:ip_address] = object.ip_address if object.respond_to?(:ip_address)
641
+
642
+ # For ActiveRecord objects, include all attributes
643
+ if object.respond_to?(:attributes)
644
+ object.attributes.each do |key, value|
645
+ next if %w[id user_id group role ip_address].include?(key.to_s)
646
+
647
+ context[key.to_sym] = value
648
+ end
649
+ end
650
+ # Handle simple values (like user_id directly)
651
+ elsif object.respond_to?(:to_i) && object.to_i.to_s == object.to_s
652
+ context[:user_id] = object.to_i
653
+ end
654
+
655
+ context
656
+ end
657
+
591
658
  def enable_targeting(type, value)
592
659
  @targeting[type] ||= []
593
660
  @targeting[type] << value.to_s unless @targeting[type].include?(value.to_s)
@@ -597,6 +664,8 @@ module Magick
597
664
  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
598
665
  Magick::Rails::Events.targeting_added(name, targeting_type: type, targeting_value: value)
599
666
  end
667
+
668
+ true
600
669
  end
601
670
 
602
671
  def disable_targeting(type, value = nil)
@@ -611,14 +680,16 @@ module Magick
611
680
  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
612
681
  Magick::Rails::Events.targeting_removed(name, targeting_type: type, targeting_value: value)
613
682
  end
683
+
684
+ true
614
685
  end
615
686
 
616
687
  def save_targeting
617
688
  adapter_registry.set(name, 'targeting', targeting)
618
689
  # Update the feature in Magick.features if it's registered
619
- if Magick.features.key?(name)
620
- Magick.features[name].instance_variable_set(:@targeting, targeting.dup)
621
- end
690
+ return unless Magick.features.key?(name)
691
+
692
+ Magick.features[name].instance_variable_set(:@targeting, targeting.dup)
622
693
  end
623
694
 
624
695
  def default_for_type
@@ -643,22 +714,30 @@ module Magick
643
714
  def validate_default_value!
644
715
  case type
645
716
  when :boolean
646
- raise InvalidFeatureValueError, "Default value must be boolean for type :boolean" unless [true, false].include?(default_value)
717
+ unless [true, false].include?(default_value)
718
+ raise InvalidFeatureValueError, 'Default value must be boolean for type :boolean'
719
+ end
647
720
  when :string
648
- raise InvalidFeatureValueError, "Default value must be a string for type :string" unless default_value.is_a?(String)
721
+ unless default_value.is_a?(String)
722
+ raise InvalidFeatureValueError,
723
+ 'Default value must be a string for type :string'
724
+ end
649
725
  when :number
650
- raise InvalidFeatureValueError, "Default value must be numeric for type :number" unless default_value.is_a?(Numeric)
726
+ unless default_value.is_a?(Numeric)
727
+ raise InvalidFeatureValueError,
728
+ 'Default value must be numeric for type :number'
729
+ end
651
730
  end
652
731
  end
653
732
 
654
733
  def validate_value!(value)
655
734
  case type
656
735
  when :boolean
657
- raise InvalidFeatureValueError, "Value must be boolean for type :boolean" unless [true, false].include?(value)
736
+ raise InvalidFeatureValueError, 'Value must be boolean for type :boolean' unless [true, false].include?(value)
658
737
  when :string
659
- raise InvalidFeatureValueError, "Value must be a string for type :string" unless value.is_a?(String)
738
+ raise InvalidFeatureValueError, 'Value must be a string for type :string' unless value.is_a?(String)
660
739
  when :number
661
- raise InvalidFeatureValueError, "Value must be numeric for type :number" unless value.is_a?(Numeric)
740
+ raise InvalidFeatureValueError, 'Value must be numeric for type :number' unless value.is_a?(Numeric)
662
741
  end
663
742
  end
664
743
  end
@@ -20,7 +20,7 @@ module Magick
20
20
  end
21
21
 
22
22
  # Subscribe to a specific event
23
- def subscribe_to(event_name, &block)
23
+ def subscribe_to(event_name)
24
24
  return unless Events.rails81?
25
25
 
26
26
  full_event_name = Events::EVENTS[event_name] || event_name.to_s
@@ -44,7 +44,7 @@ module Magick
44
44
  # Default log subscriber for Magick events
45
45
  class LogSubscriber
46
46
  def emit(event)
47
- payload = event[:payload].map { |key, value| "#{key}=#{value}" }.join(" ")
47
+ payload = event[:payload].map { |key, value| "#{key}=#{value}" }.join(' ')
48
48
  source_location = event[:source_location]
49
49
  log = "[#{event[:name]}] #{payload}"
50
50
  log += " at #{source_location[:filepath]}:#{source_location[:lineno]}" if source_location
@@ -43,20 +43,22 @@ if defined?(Rails)
43
43
  else
44
44
  # Fallback to config/initializers/features.rb (already loaded by Rails, but check anyway)
45
45
  initializer_file = Rails.root.join('config', 'initializers', 'features.rb')
46
- if File.exist?(initializer_file)
46
+ if File.exist?(initializer_file) && !defined?(Magick::Rails::FeaturesLoaded)
47
47
  # Only load if not already loaded (Rails may have already loaded it)
48
- load initializer_file unless defined?(Magick::Rails::FeaturesLoaded)
48
+ load initializer_file
49
49
  end
50
50
  end
51
- Magick::Rails.const_set(:FeaturesLoaded, true) rescue nil
51
+ begin
52
+ Magick::Rails.const_set(:FeaturesLoaded, true)
53
+ rescue StandardError
54
+ nil
55
+ end
52
56
  end
53
57
  end
54
58
 
55
59
  # Preload features in request store
56
60
  config.to_prepare do
57
- if defined?(RequestStore)
58
- RequestStore.store[:magick_features] ||= {}
59
- end
61
+ RequestStore.store[:magick_features] ||= {} if defined?(RequestStore)
60
62
  end
61
63
  end
62
64
  end
data/lib/magick/rails.rb CHANGED
@@ -2,6 +2,4 @@
2
2
 
3
3
  # Rails integration is now in lib/magick/rails/railtie.rb
4
4
  # This file is kept for backward compatibility
5
- if defined?(Rails)
6
- require_relative 'rails/railtie'
7
- end
5
+ require_relative 'rails/railtie' if defined?(Rails)
@@ -7,7 +7,7 @@ module Magick
7
7
  end
8
8
 
9
9
  module ClassMethods
10
- def with_feature_enabled(feature_name, &block)
10
+ def with_feature_enabled(feature_name)
11
11
  feature = Magick.features[feature_name.to_s] || Magick[feature_name]
12
12
  original_value = feature.value
13
13
  feature.set_value(true)
@@ -16,7 +16,7 @@ module Magick
16
16
  feature.set_value(original_value)
17
17
  end
18
18
 
19
- def with_feature_disabled(feature_name, &block)
19
+ def with_feature_disabled(feature_name)
20
20
  feature = Magick.features[feature_name.to_s] || Magick[feature_name]
21
21
  original_value = feature.value
22
22
  feature.set_value(false)
@@ -25,7 +25,7 @@ module Magick
25
25
  feature.set_value(original_value)
26
26
  end
27
27
 
28
- def with_feature_value(feature_name, value, &block)
28
+ def with_feature_value(feature_name, value)
29
29
  feature = Magick.features[feature_name.to_s] || Magick[feature_name]
30
30
  original_value = feature.value
31
31
  feature.set_value(value)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Magick
4
- VERSION = '0.7.2'
4
+ VERSION = '0.8.0'
5
5
  end
@@ -61,17 +61,15 @@ module Magick
61
61
  feature.set_status(feature_data[:status]) if feature_data[:status]
62
62
 
63
63
  # Restore targeting
64
- if feature_data[:targeting]
65
- feature_data[:targeting].each do |type, values|
66
- Array(values).each do |value|
67
- case type.to_sym
68
- when :user
69
- feature.enable_for_user(value)
70
- when :group
71
- feature.enable_for_group(value)
72
- when :role
73
- feature.enable_for_role(value)
74
- end
64
+ feature_data[:targeting]&.each do |type, values|
65
+ Array(values).each do |value|
66
+ case type.to_sym
67
+ when :user
68
+ feature.enable_for_user(value)
69
+ when :group
70
+ feature.enable_for_group(value)
71
+ when :role
72
+ feature.enable_for_role(value)
75
73
  end
76
74
  end
77
75
  end
data/lib/magick.rb CHANGED
@@ -39,7 +39,7 @@ module Magick
39
39
  # Support both old style and new DSL style
40
40
  return unless block_given?
41
41
 
42
- if block.arity == 0
42
+ if block.arity.zero?
43
43
  # New DSL style
44
44
  ConfigDSL.configure(&block)
45
45
  else
@@ -68,6 +68,15 @@ module Magick
68
68
  feature.enabled?(context)
69
69
  end
70
70
 
71
+ def enabled_for?(feature_name, object, **additional_context)
72
+ feature = features[feature_name.to_s] || self[feature_name]
73
+ feature.enabled_for?(object, **additional_context)
74
+ end
75
+
76
+ def disabled_for?(feature_name, object, **additional_context)
77
+ !enabled_for?(feature_name, object, **additional_context)
78
+ end
79
+
71
80
  # Reload a feature from the adapter (useful when feature is changed externally)
72
81
  def reload_feature(feature_name)
73
82
  feature = features[feature_name.to_s] || self[feature_name]
@@ -82,7 +91,7 @@ module Magick
82
91
  features.key?(feature_name.to_s) || (adapter_registry || default_adapter_registry).exists?(feature_name)
83
92
  end
84
93
 
85
- def bulk_enable(feature_names, context = {})
94
+ def bulk_enable(feature_names, _context = {})
86
95
  feature_names.map do |name|
87
96
  feature = features[name.to_s] || self[name]
88
97
  feature.set_value(true) if feature.type == :boolean
@@ -90,7 +99,7 @@ module Magick
90
99
  end
91
100
  end
92
101
 
93
- def bulk_disable(feature_names, context = {})
102
+ def bulk_disable(feature_names, _context = {})
94
103
  feature_names.map do |name|
95
104
  feature = features[name.to_s] || self[name]
96
105
  feature.set_value(false) if feature.type == :boolean
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Auto-require file for Rails
4
- # When gem 'magick-feature-flags' is in Gemfile, Rails will try to require this file
4
+ # When gem 'magick-feature-flags' is in Gemfile, Rails will try to require 'magick_feature_flags'
5
5
  require 'magick'
6
-
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: magick-feature-flags
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Lobanov
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-18 00:00:00.000000000 Z
11
+ date: 2025-11-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -65,7 +65,6 @@ files:
65
65
  - lib/generators/magick/install/install_generator.rb
66
66
  - lib/generators/magick/install/templates/README
67
67
  - lib/generators/magick/install/templates/magick.rb
68
- - lib/magick-feature-flags.rb
69
68
  - lib/magick.rb
70
69
  - lib/magick/adapters/base.rb
71
70
  - lib/magick/adapters/memory.rb
@@ -98,11 +97,12 @@ files:
98
97
  - lib/magick/testing_helpers.rb
99
98
  - lib/magick/version.rb
100
99
  - lib/magick/versioning.rb
100
+ - lib/magick_feature_flags.rb
101
101
  homepage: https://github.com/andrew-woblavobla/magick
102
102
  licenses:
103
103
  - MIT
104
104
  metadata: {}
105
- post_install_message:
105
+ post_install_message:
106
106
  rdoc_options: []
107
107
  require_paths:
108
108
  - lib
@@ -117,8 +117,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
117
117
  - !ruby/object:Gem::Version
118
118
  version: '0'
119
119
  requirements: []
120
- rubygems_version: 3.0.3.1
121
- signing_key:
120
+ rubygems_version: 3.5.19
121
+ signing_key:
122
122
  specification_version: 4
123
123
  summary: A performant and memory-efficient feature toggle gem
124
124
  test_files: []