magick-feature-flags 1.1.3 → 1.2.2

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: e896cf3753f151fb2f1082a231c9af82095e3caea7716642a9f535e60d36e9e4
4
- data.tar.gz: 24992a605474319bd831fbd60a6d955ba05de266d7419f60065fef2a2d32d2d1
3
+ metadata.gz: 635570418a98cab1f8f6480a6834f4a803bd0a24d94bbdf96349f84a78e74278
4
+ data.tar.gz: b7d5b93af0c4fb1204b36e64561fe50452afbd85d2d115cf79aa85778346da2f
5
5
  SHA512:
6
- metadata.gz: ba376fb1fea1613bd7841fd4a56b8ed30833ab91c6563fad61abde4127caba802330b9bf14b9b0a52094addd7c58b92cb85d30af2b78c283647121de69fee19c
7
- data.tar.gz: cfe7ad50afee3b0c7b192a1bb8a5b035834672f21d29627f67a3eda2939330880806686c655ccc6d17d4875c7d6fd029fc1f1732ee68562cc520373a2d54190f
6
+ metadata.gz: 9f8f867de1a865f1e5deee8f2767fb834034f5f53c47c43be7182a4b06db735be412dd06bb09bf47cd901f8eb1f8c335c9adb1dae83adbebeb51822cf380aff6
7
+ data.tar.gz: cfce216854f22d787083a24c42e1995a313e96fbc79b4aa203d318021209e8bf4af4f28b8fa88846357adf31893c432650ceadcf54b7e1e5886a154457297ada
data/README.md CHANGED
@@ -6,6 +6,7 @@ A performant and memory-efficient feature toggle gem for Ruby and Rails applicat
6
6
 
7
7
  - **Multiple Feature Types**: Boolean, string, and number feature flags
8
8
  - **Flexible Targeting**: Enable features for specific users, groups, roles, tags, or percentages
9
+ - **Exclusions**: Exclude specific users, groups, roles, tags, or IPs — exclusions always take priority over inclusions
9
10
  - **Dual Backend**: Memory adapter (fast) with Redis fallback (persistent)
10
11
  - **Rails Integration**: Seamless integration with Rails, including request store caching
11
12
  - **DSL Support**: Define features in a Ruby DSL file (`config/features.rb`)
@@ -193,6 +194,76 @@ feature.enable_for_ip_addresses('192.168.1.0/24', '10.0.0.1')
193
194
  feature.enable_for_custom_attribute(:subscription_tier, ['premium', 'enterprise'])
194
195
  ```
195
196
 
197
+ ### Feature Exclusions
198
+
199
+ Exclusions let you block specific users, groups, roles, tags, or IP addresses from a feature — even if they match an inclusion rule. **Exclusions always take priority over inclusions.**
200
+
201
+ ```ruby
202
+ feature = Magick[:new_dashboard]
203
+
204
+ # Exclude specific users
205
+ feature.exclude_user(456)
206
+ feature.exclude_user(789)
207
+
208
+ # Exclude specific tags
209
+ feature.exclude_tag('legacy_tier')
210
+ feature.exclude_tag('banned')
211
+
212
+ # Exclude specific groups
213
+ feature.exclude_group('suspended_users')
214
+
215
+ # Exclude specific roles
216
+ feature.exclude_role('guest')
217
+
218
+ # Exclude IP addresses (supports CIDR notation)
219
+ feature.exclude_ip_addresses(['10.0.0.0/8', '192.168.1.100'])
220
+
221
+ # Remove exclusions
222
+ feature.remove_user_exclusion(456)
223
+ feature.remove_tag_exclusion('legacy_tier')
224
+ feature.remove_group_exclusion('suspended_users')
225
+ feature.remove_role_exclusion('guest')
226
+ feature.remove_ip_exclusion # Removes all IP exclusions
227
+ ```
228
+
229
+ **Exclusions win over inclusions:**
230
+
231
+ ```ruby
232
+ feature = Magick[:premium_features]
233
+ feature.enable # Enabled globally for everyone
234
+
235
+ feature.exclude_user(123)
236
+ Magick.enabled?(:premium_features, user_id: 123) # => false (excluded)
237
+ Magick.enabled?(:premium_features, user_id: 456) # => true (not excluded)
238
+
239
+ # Even percentage targeting is overridden
240
+ feature.enable_percentage_of_users(100) # 100% of users
241
+ feature.exclude_user(123)
242
+ Magick.enabled?(:premium_features, user_id: 123) # => false (still excluded)
243
+ ```
244
+
245
+ **Exclusions in DSL (`config/features.rb`):**
246
+
247
+ ```ruby
248
+ boolean_feature :new_dashboard, default: true
249
+
250
+ # Exclude problematic users
251
+ exclude_user :new_dashboard, 'user_123'
252
+ exclude_user :new_dashboard, 'user_456'
253
+
254
+ # Exclude legacy tiers
255
+ exclude_tag :new_dashboard, 'legacy_tier'
256
+
257
+ # Exclude groups
258
+ exclude_group :new_dashboard, 'banned_users'
259
+
260
+ # Exclude roles
261
+ exclude_role :new_dashboard, 'suspended'
262
+
263
+ # Exclude IPs
264
+ exclude_ip_addresses :new_dashboard, '10.0.0.0/8'
265
+ ```
266
+
196
267
  ### Checking Feature Enablement with Objects
197
268
 
198
269
  You can check if a feature is enabled for an object (like a User model) and its fields:
@@ -258,6 +329,12 @@ result = feature.enable_for_role('admin') # => true
258
329
  result = feature.enable_for_tag('premium') # => true
259
330
  result = feature.enable_percentage_of_users(25) # => true
260
331
  result = feature.set_value(true) # => true
332
+
333
+ # Exclusion methods also return true
334
+ result = feature.exclude_user(456) # => true
335
+ result = feature.exclude_tag('banned') # => true
336
+ result = feature.exclude_group('blocked') # => true
337
+ result = feature.exclude_role('guest') # => true
261
338
  ```
262
339
 
263
340
  ### DSL Configuration
@@ -303,6 +380,13 @@ boolean_feature :premium_feature,
303
380
 
304
381
  # Add dependencies after feature definition
305
382
  add_dependency(:another_feature, :required_feature)
383
+
384
+ # Exclusions - block specific users/groups/roles/tags
385
+ exclude_user :new_dashboard, 'user_123'
386
+ exclude_tag :new_dashboard, 'legacy_tier'
387
+ exclude_group :new_dashboard, 'banned_users'
388
+ exclude_role :new_dashboard, 'suspended'
389
+ exclude_ip_addresses :new_dashboard, '10.0.0.0/8'
306
390
  ```
307
391
 
308
392
  ### In Controllers
@@ -443,8 +527,8 @@ end
443
527
 
444
528
  Magick uses a dual-adapter strategy:
445
529
 
446
- 1. **Memory Adapter**: Fast, in-memory storage with TTL support
447
- 2. **Redis Adapter**: Persistent storage for distributed systems (optional)
530
+ 1. **Memory Adapter**: Fast, in-memory storage with TTL support and JSON serialization
531
+ 2. **Redis Adapter**: Persistent storage for distributed systems (optional), uses SCAN instead of KEYS and pipelined bulk operations
448
532
 
449
533
  The registry automatically falls back from memory to Redis if a feature isn't found in memory. When features are updated:
450
534
  - Both adapters are updated simultaneously
@@ -589,6 +673,7 @@ Once mounted, visit `/magick` in your browser to access the Admin UI.
589
673
  - **Role Targeting**: Select roles from a configured list (checkboxes)
590
674
  - **Tag Targeting**: Select tags from a dynamically loaded list (checkboxes)
591
675
  - **User Targeting**: Enter user IDs (comma-separated)
676
+ - **Exclusions**: Exclude users, roles, and tags from a feature (exclusions override inclusions)
592
677
  - **Visual Display**: See all active targeting rules with badges
593
678
  - **Edit Features**: Update feature values (boolean, string, number) directly from the UI
594
679
  - **Statistics**: View performance metrics and usage statistics for each feature
@@ -641,7 +726,12 @@ The Admin UI provides a comprehensive targeting interface:
641
726
  - Add or remove users dynamically
642
727
  - Clear all user targeting by leaving the field empty
643
728
 
644
- 4. **Visual Feedback**:
729
+ 4. **Exclusion Targeting**:
730
+ - Exclude specific users (comma-separated IDs), roles, and tags
731
+ - Exclusions always take priority over inclusions
732
+ - Managed through the same targeting form
733
+
734
+ 5. **Visual Feedback**:
645
735
  - All targeting rules are displayed as badges in the feature details view
646
736
  - Easy to see which roles/tags/users have access to each feature
647
737
 
@@ -663,7 +753,27 @@ The Admin UI provides the following routes:
663
753
 
664
754
  **Security:**
665
755
 
666
- The Admin UI is a basic Rails Engine without built-in authentication. **You should add authentication/authorization** before mounting it in production. For example:
756
+ The Admin UI includes a built-in authentication hook via `require_role`. Configure it to gate access:
757
+
758
+ ```ruby
759
+ # config/initializers/magick.rb
760
+ Rails.application.config.after_initialize do
761
+ Magick::AdminUI.configure do |config|
762
+ # Option 1: Lambda-based authentication (recommended)
763
+ config.require_role = ->(controller) {
764
+ # Return true to allow, false to deny (returns 403 Forbidden)
765
+ controller.current_user&.admin?
766
+ }
767
+
768
+ # Option 2: Check for a specific role
769
+ config.require_role = ->(controller) {
770
+ controller.current_user&.role == 'admin'
771
+ }
772
+ end
773
+ end
774
+ ```
775
+
776
+ You can also gate access at the routing level:
667
777
 
668
778
  ```ruby
669
779
  # config/routes.rb
@@ -680,8 +790,6 @@ Rails.application.routes.draw do
680
790
  end
681
791
  ```
682
792
 
683
- Or use a before_action in your ApplicationController if you mount it at the application level.
684
-
685
793
  **Note:** The Admin UI is optional and only loaded when explicitly enabled in configuration. It requires Rails to be available.
686
794
 
687
795
  ### Feature Types
@@ -208,6 +208,49 @@ module Magick
208
208
  @feature.disable_percentage_of_requests if current_targeting[:percentage_requests]
209
209
  end
210
210
 
211
+ # Handle excluded user IDs
212
+ if targeting_params[:excluded_user_ids].present?
213
+ excluded_user_ids = targeting_params[:excluded_user_ids].split(',').map(&:strip).reject(&:blank?)
214
+ current_excluded_users = current_targeting[:excluded_users].is_a?(Array) ? current_targeting[:excluded_users] : (current_targeting[:excluded_users] ? [current_targeting[:excluded_users]] : [])
215
+
216
+ (current_excluded_users - excluded_user_ids).each do |user_id|
217
+ @feature.remove_user_exclusion(user_id) if user_id.present?
218
+ end
219
+
220
+ (excluded_user_ids - current_excluded_users).each do |user_id|
221
+ @feature.exclude_user(user_id) if user_id.present?
222
+ end
223
+ elsif targeting_params.key?(:excluded_user_ids) && targeting_params[:excluded_user_ids].blank?
224
+ current_excluded_users = current_targeting[:excluded_users].is_a?(Array) ? current_targeting[:excluded_users] : (current_targeting[:excluded_users] ? [current_targeting[:excluded_users]] : [])
225
+ current_excluded_users.each do |user_id|
226
+ @feature.remove_user_exclusion(user_id) if user_id.present?
227
+ end
228
+ end
229
+
230
+ # Handle excluded roles
231
+ current_excluded_roles = current_targeting[:excluded_roles].is_a?(Array) ? current_targeting[:excluded_roles] : (current_targeting[:excluded_roles] ? [current_targeting[:excluded_roles]] : [])
232
+ selected_excluded_roles = Array(targeting_params[:excluded_roles]).reject(&:blank?)
233
+
234
+ (current_excluded_roles - selected_excluded_roles).each do |role|
235
+ @feature.remove_role_exclusion(role) if role.present?
236
+ end
237
+
238
+ (selected_excluded_roles - current_excluded_roles).each do |role|
239
+ @feature.exclude_role(role) if role.present?
240
+ end
241
+
242
+ # Handle excluded tags
243
+ current_excluded_tags = current_targeting[:excluded_tags].is_a?(Array) ? current_targeting[:excluded_tags] : (current_targeting[:excluded_tags] ? [current_targeting[:excluded_tags]] : [])
244
+ selected_excluded_tags = Array(targeting_params[:excluded_tags]).reject(&:blank?)
245
+
246
+ (current_excluded_tags - selected_excluded_tags).each do |tag|
247
+ @feature.remove_tag_exclusion(tag) if tag.present?
248
+ end
249
+
250
+ (selected_excluded_tags - current_excluded_tags).each do |tag|
251
+ @feature.exclude_tag(tag) if tag.present?
252
+ end
253
+
211
254
  # After all targeting updates, ensure we're using the registered feature instance
212
255
  # and reload it to get the latest state from adapter
213
256
  feature_name = @feature.name.to_s