magick-feature-flags 0.7.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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +363 -0
  4. data/lib/generators/magick/install/install_generator.rb +19 -0
  5. data/lib/generators/magick/install/templates/README +25 -0
  6. data/lib/generators/magick/install/templates/magick.rb +32 -0
  7. data/lib/magick/adapters/base.rb +27 -0
  8. data/lib/magick/adapters/memory.rb +113 -0
  9. data/lib/magick/adapters/redis.rb +97 -0
  10. data/lib/magick/adapters/registry.rb +133 -0
  11. data/lib/magick/audit_log.rb +65 -0
  12. data/lib/magick/circuit_breaker.rb +65 -0
  13. data/lib/magick/config.rb +179 -0
  14. data/lib/magick/dsl.rb +80 -0
  15. data/lib/magick/errors.rb +9 -0
  16. data/lib/magick/export_import.rb +82 -0
  17. data/lib/magick/feature.rb +665 -0
  18. data/lib/magick/feature_dependency.rb +28 -0
  19. data/lib/magick/feature_variant.rb +17 -0
  20. data/lib/magick/performance_metrics.rb +76 -0
  21. data/lib/magick/rails/event_subscriber.rb +55 -0
  22. data/lib/magick/rails/events.rb +236 -0
  23. data/lib/magick/rails/railtie.rb +94 -0
  24. data/lib/magick/rails.rb +7 -0
  25. data/lib/magick/targeting/base.rb +11 -0
  26. data/lib/magick/targeting/complex.rb +27 -0
  27. data/lib/magick/targeting/custom_attribute.rb +35 -0
  28. data/lib/magick/targeting/date_range.rb +17 -0
  29. data/lib/magick/targeting/group.rb +15 -0
  30. data/lib/magick/targeting/ip_address.rb +22 -0
  31. data/lib/magick/targeting/percentage.rb +24 -0
  32. data/lib/magick/targeting/request_percentage.rb +15 -0
  33. data/lib/magick/targeting/role.rb +15 -0
  34. data/lib/magick/targeting/user.rb +15 -0
  35. data/lib/magick/testing_helpers.rb +45 -0
  36. data/lib/magick/version.rb +5 -0
  37. data/lib/magick/versioning.rb +98 -0
  38. data/lib/magick.rb +143 -0
  39. metadata +123 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 26705304fe361a52c069dde57f271991a8e108bd83b812754928551fe8eb1c50
4
+ data.tar.gz: 025b53d5898a675b1d0dca7b66244ced692ad51fa1036ab0c5bdc1df929184e4
5
+ SHA512:
6
+ metadata.gz: 2f88df6bd3c9e236bb7a3cbffefa4b6a38e14b5d0d4ae3a85840c0473375c74bc0118c85d9646f2567e5615e581124d652ecd742c7ad89b70127cd443bae4f76
7
+ data.tar.gz: 26d30eb262dbd89fc78c3df40d46f8bd5f176ffd59720302bc9054f458cb4123de51d6b01051e8ad5e9eb3944bf4c430ddf1bf3f3f264369f3eb2764cdcf0d08
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Andrew Lobanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,363 @@
1
+ # Magick
2
+
3
+ A performant and memory-efficient feature toggle gem for Ruby and Rails applications.
4
+
5
+ ## Features
6
+
7
+ - **Multiple Feature Types**: Boolean, string, and number feature flags
8
+ - **Flexible Targeting**: Enable features for specific users, groups, roles, or percentages
9
+ - **Dual Backend**: Memory adapter (fast) with Redis fallback (persistent)
10
+ - **Rails Integration**: Seamless integration with Rails, including request store caching
11
+ - **DSL Support**: Define features in a Ruby DSL file (`config/features.rb`)
12
+ - **Thread-Safe**: All operations are thread-safe for concurrent access
13
+ - **Performance**: Optimized for speed with memory-first caching strategy
14
+ - **Advanced Features**: Circuit breaker, audit logging, performance metrics, versioning, and more
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem 'magick'
22
+ ```
23
+
24
+ And then execute:
25
+
26
+ ```bash
27
+ $ bundle install
28
+ ```
29
+
30
+ Or install it yourself as:
31
+
32
+ ```bash
33
+ $ gem install magick
34
+ ```
35
+
36
+ ## Installation
37
+
38
+ After adding the gem to your Gemfile and running `bundle install`, generate the configuration file:
39
+
40
+ ```bash
41
+ rails generate magick:install
42
+ ```
43
+
44
+ This will create `config/initializers/magick.rb` with a basic configuration.
45
+
46
+ ## Configuration
47
+
48
+ ### Basic Configuration
49
+
50
+ The generator creates `config/initializers/magick.rb` with sensible defaults. You can also create it manually:
51
+
52
+ ```ruby
53
+ Magick.configure do
54
+ # Configure Redis (optional)
55
+ redis url: ENV['REDIS_URL']
56
+
57
+ # Enable features
58
+ performance_metrics enabled: true
59
+ audit_log enabled: true
60
+ versioning enabled: true
61
+ warn_on_deprecated enabled: true
62
+ end
63
+ ```
64
+
65
+ ### Advanced Configuration
66
+
67
+ ```ruby
68
+ Magick.configure do
69
+ # Environment
70
+ environment Rails.env
71
+
72
+ # Memory TTL
73
+ memory_ttl 7200 # 2 hours
74
+
75
+ # Redis configuration
76
+ redis url: ENV['REDIS_URL'], namespace: 'magick:features'
77
+
78
+ # Circuit breaker settings
79
+ circuit_breaker threshold: 5, timeout: 60
80
+
81
+ # Async updates
82
+ async_updates enabled: true
83
+
84
+ # Enable services
85
+ performance_metrics enabled: true
86
+ audit_log enabled: true
87
+ versioning enabled: true
88
+ warn_on_deprecated enabled: true
89
+ end
90
+ ```
91
+
92
+ ## Usage
93
+
94
+ ### Basic Usage
95
+
96
+ ```ruby
97
+ # Check if a feature is enabled
98
+ if Magick.enabled?(:new_dashboard)
99
+ # Show new dashboard
100
+ end
101
+
102
+ # With context (user, role, etc.)
103
+ if Magick.enabled?(:premium_features, user_id: current_user.id, role: current_user.role)
104
+ # Show premium features
105
+ end
106
+ ```
107
+
108
+ ### Registering Features
109
+
110
+ ```ruby
111
+ # Register a boolean feature
112
+ Magick.register_feature(:new_dashboard,
113
+ type: :boolean,
114
+ default_value: false,
115
+ description: "New dashboard UI"
116
+ )
117
+
118
+ # Register a string feature
119
+ Magick.register_feature(:api_version,
120
+ type: :string,
121
+ default_value: "v1",
122
+ description: "API version to use"
123
+ )
124
+
125
+ # Register a number feature
126
+ Magick.register_feature(:max_results,
127
+ type: :number,
128
+ default_value: 10,
129
+ description: "Maximum number of results"
130
+ )
131
+ ```
132
+
133
+ ### Feature Targeting
134
+
135
+ ```ruby
136
+ feature = Magick[:new_dashboard]
137
+
138
+ # Enable globally (for everyone, no targeting)
139
+ feature.enable
140
+
141
+ # Disable globally (for everyone, no targeting)
142
+ feature.disable
143
+
144
+ # Enable for specific user
145
+ feature.enable_for_user(123)
146
+
147
+ # Enable for specific group
148
+ feature.enable_for_group("beta_testers")
149
+
150
+ # Enable for specific role
151
+ feature.enable_for_role("admin")
152
+
153
+ # Enable for percentage of users (consistent)
154
+ feature.enable_percentage_of_users(25) # 25% of users
155
+
156
+ # Enable for percentage of requests (random)
157
+ feature.enable_percentage_of_requests(50) # 50% of requests
158
+
159
+ # Enable for date range
160
+ feature.enable_for_date_range('2024-01-01', '2024-12-31')
161
+
162
+ # Enable for IP addresses
163
+ feature.enable_for_ip_addresses('192.168.1.0/24', '10.0.0.1')
164
+
165
+ # Enable for custom attributes
166
+ feature.enable_for_custom_attribute(:subscription_tier, ['premium', 'enterprise'])
167
+ ```
168
+
169
+ ### DSL Configuration
170
+
171
+ Create `config/features.rb`:
172
+
173
+ ```ruby
174
+ # Boolean features
175
+ boolean_feature :new_dashboard, default: false, description: "New dashboard UI"
176
+ boolean_feature :dark_mode, default: false, description: "Dark mode theme"
177
+
178
+ # String features
179
+ string_feature :api_version, default: "v1", description: "API version"
180
+
181
+ # Number features
182
+ number_feature :max_results, default: 10, description: "Maximum results per page"
183
+
184
+ # With status
185
+ feature :experimental_feature,
186
+ type: :boolean,
187
+ default_value: false,
188
+ status: :deprecated,
189
+ description: "Experimental feature (deprecated)"
190
+
191
+ # With dependencies (feature will only be enabled if dependencies are enabled)
192
+ boolean_feature :advanced_feature,
193
+ default: false,
194
+ description: "Advanced feature requiring base_feature",
195
+ dependencies: [:base_feature]
196
+
197
+ # Multiple dependencies
198
+ boolean_feature :premium_feature,
199
+ default: false,
200
+ description: "Premium feature requiring multiple features",
201
+ dependencies: [:base_feature, :auth_feature]
202
+
203
+ # Add dependencies after feature definition
204
+ add_dependency(:another_feature, :required_feature)
205
+ ```
206
+
207
+ ### In Controllers
208
+
209
+ ```ruby
210
+ class DashboardController < ApplicationController
211
+ def show
212
+ if Magick.enabled?(:new_dashboard, user_id: current_user.id, role: current_user.role)
213
+ render :new_dashboard
214
+ else
215
+ render :old_dashboard
216
+ end
217
+ end
218
+ end
219
+ ```
220
+
221
+ ### Advanced Features
222
+
223
+ #### Feature Variants (A/B Testing)
224
+
225
+ ```ruby
226
+ feature = Magick[:button_color]
227
+ feature.set_variants([
228
+ { name: 'blue', value: '#0066cc', weight: 50 },
229
+ { name: 'green', value: '#00cc66', weight: 30 },
230
+ { name: 'red', value: '#cc0000', weight: 20 }
231
+ ])
232
+
233
+ variant = feature.get_variant
234
+ # Returns 'blue', 'green', or 'red' based on weights
235
+ ```
236
+
237
+ #### Feature Dependencies
238
+
239
+ ```ruby
240
+ feature = Magick[:advanced_feature]
241
+ feature.add_dependency(:base_feature)
242
+ # advanced_feature will only be enabled if base_feature is also enabled
243
+ ```
244
+
245
+ #### Export/Import
246
+
247
+ ```ruby
248
+ # Export features
249
+ json_data = Magick.export(format: :json)
250
+ File.write('features.json', json_data)
251
+
252
+ # Import features
253
+ Magick.import(File.read('features.json'))
254
+ ```
255
+
256
+ #### Versioning and Rollback
257
+
258
+ ```ruby
259
+ # Save current state as version
260
+ Magick.versioning.save_version(:my_feature, created_by: current_user.id)
261
+
262
+ # Rollback to previous version
263
+ Magick.versioning.rollback(:my_feature, version: 2)
264
+ ```
265
+
266
+ #### Performance Metrics
267
+
268
+ ```ruby
269
+ # Get average duration for feature checks
270
+ avg_duration = Magick.performance_metrics.average_duration(feature_name: :my_feature)
271
+
272
+ # Get most used features
273
+ most_used = Magick.performance_metrics.most_used_features(limit: 10)
274
+
275
+ # Get usage count
276
+ count = Magick.performance_metrics.usage_count(:my_feature)
277
+ ```
278
+
279
+ #### Audit Logging
280
+
281
+ ```ruby
282
+ # View audit log entries
283
+ entries = Magick.audit_log.entries(feature_name: :my_feature, limit: 100)
284
+ entries.each do |entry|
285
+ puts "#{entry.timestamp}: #{entry.action} by #{entry.user_id}"
286
+ end
287
+ ```
288
+
289
+ ## Architecture
290
+
291
+ ### Adapters
292
+
293
+ Magick uses a dual-adapter strategy:
294
+
295
+ 1. **Memory Adapter**: Fast, in-memory storage with TTL support
296
+ 2. **Redis Adapter**: Persistent storage for distributed systems (optional)
297
+
298
+ The registry automatically falls back from memory to Redis if a feature isn't found in memory. When features are updated, both adapters are updated simultaneously.
299
+
300
+ #### Memory-Only Mode
301
+
302
+ If Redis is not configured, Magick works in **memory-only mode**:
303
+ - ✅ Fast, zero external dependencies
304
+ - ✅ Perfect for single-process applications or development
305
+ - ⚠️ **No cross-process cache invalidation** - each process has isolated cache
306
+ - ⚠️ Changes in one process won't be reflected in other processes
307
+
308
+ #### Redis Mode (Recommended for Production)
309
+
310
+ With Redis configured:
311
+ - ✅ Cross-process cache invalidation via Redis Pub/Sub
312
+ - ✅ Persistent storage across restarts
313
+ - ✅ Zero Redis calls on feature checks (only memory lookups)
314
+ - ✅ Automatic cache invalidation when features change in any process
315
+
316
+ ### Feature Types
317
+
318
+ - `:boolean` - True/false flags
319
+ - `:string` - String values
320
+ - `:number` - Numeric values
321
+
322
+ ### Feature Status
323
+
324
+ - `:active` - Feature is active and can be enabled
325
+ - `:inactive` - Feature is disabled for everyone
326
+ - `:deprecated` - Feature is deprecated (can be enabled with `allow_deprecated: true` in context)
327
+
328
+ ## Testing
329
+
330
+ Use the testing helpers in your RSpec tests:
331
+
332
+ ```ruby
333
+ RSpec.describe MyFeature do
334
+ it 'works with feature enabled' do
335
+ with_feature_enabled(:new_feature) do
336
+ # Test code here
337
+ end
338
+ end
339
+
340
+ it 'works with feature disabled' do
341
+ with_feature_disabled(:new_feature) do
342
+ # Test code here
343
+ end
344
+ end
345
+ end
346
+ ```
347
+
348
+ ## Development
349
+
350
+ After checking out the repo, run:
351
+
352
+ ```bash
353
+ bundle install
354
+ rspec
355
+ ```
356
+
357
+ ## Contributing
358
+
359
+ Bug reports and pull requests are welcome on GitHub.
360
+
361
+ ## License
362
+
363
+ The gem is available as open source under the terms of the [MIT License](LICENSE).
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Magick
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('templates', __dir__)
7
+
8
+ desc 'Creates a Magick configuration file at config/initializers/magick.rb'
9
+
10
+ def create_initializer
11
+ template 'magick.rb', 'config/initializers/magick.rb'
12
+ end
13
+
14
+ def show_readme
15
+ readme 'README' if behavior == :invoke
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ == Magick Install Generator
2
+
3
+ The generated configuration includes:
4
+ - Performance metrics tracking
5
+ - Audit logging
6
+ - Versioning support
7
+ - Deprecation warnings
8
+
9
+ Optional features (commented out by default):
10
+ - Redis adapter configuration
11
+ - Memory TTL customization
12
+ - Circuit breaker settings
13
+ - Async updates
14
+
15
+ You can customize the configuration by editing the generated file.
16
+
17
+ Usage:
18
+ rails generate magick:install
19
+
20
+ For more information, see:
21
+ - README.md
22
+ - config/magick.rb.example
23
+
24
+ This generator creates a basic Magick configuration file at:
25
+ config/initializers/magick.rb
@@ -0,0 +1,32 @@
1
+ # Magick Configuration
2
+ # Generated by: rails generate magick:install
3
+ #
4
+ # For more configuration options, see:
5
+ # https://github.com/andrew-woblavobla/magick
6
+
7
+ Magick.configure do
8
+ # Configure Redis (optional - gem works without it using memory-only adapter)
9
+ # Uncomment to use Redis:
10
+ # redis url: ENV['REDIS_URL'], namespace: 'magick:features'
11
+
12
+ # Configure memory adapter TTL (default: 3600 seconds = 1 hour)
13
+ # memory_ttl 7200 # 2 hours
14
+
15
+ # Configure circuit breaker for Redis failures
16
+ # circuit_breaker threshold: 5, timeout: 60
17
+
18
+ # Enable async updates for Redis (non-blocking, improves performance)
19
+ # async_updates enabled: true
20
+
21
+ # Enable performance metrics tracking
22
+ performance_metrics enabled: true
23
+
24
+ # Enable audit logging (tracks who changed what, when)
25
+ audit_log enabled: true
26
+
27
+ # Enable versioning support (allows rollback)
28
+ versioning enabled: true
29
+
30
+ # Enable deprecation warnings in logs
31
+ warn_on_deprecated enabled: true
32
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Magick
4
+ module Adapters
5
+ class Base
6
+ def get(feature_name, key)
7
+ raise NotImplementedError, "#{self.class} must implement #get"
8
+ end
9
+
10
+ def set(feature_name, key, value)
11
+ raise NotImplementedError, "#{self.class} must implement #set"
12
+ end
13
+
14
+ def delete(feature_name)
15
+ raise NotImplementedError, "#{self.class} must implement #delete"
16
+ end
17
+
18
+ def exists?(feature_name)
19
+ raise NotImplementedError, "#{self.class} must implement #exists?"
20
+ end
21
+
22
+ def all_features
23
+ raise NotImplementedError, "#{self.class} must implement #all_features"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thread'
4
+
5
+ module Magick
6
+ module Adapters
7
+ class Memory < Base
8
+ def initialize
9
+ @store = {}
10
+ @mutex = Mutex.new
11
+ @ttl = {}
12
+ @default_ttl = 3600 # 1 hour default TTL
13
+ end
14
+
15
+ def get(feature_name, key)
16
+ mutex.synchronize do
17
+ cleanup_expired
18
+ feature_data = store[feature_name.to_s]
19
+ return nil unless feature_data
20
+
21
+ value = feature_data[key.to_s]
22
+ deserialize_value(value)
23
+ end
24
+ end
25
+
26
+ def set(feature_name, key, value)
27
+ mutex.synchronize do
28
+ feature_name_str = feature_name.to_s
29
+ store[feature_name_str] ||= {}
30
+ store[feature_name_str][key.to_s] = serialize_value(value)
31
+ update_ttl(feature_name_str)
32
+ end
33
+ end
34
+
35
+ def delete(feature_name)
36
+ mutex.synchronize do
37
+ feature_name_str = feature_name.to_s
38
+ store.delete(feature_name_str)
39
+ ttl.delete(feature_name_str)
40
+ end
41
+ end
42
+
43
+ def exists?(feature_name)
44
+ mutex.synchronize do
45
+ cleanup_expired
46
+ store.key?(feature_name.to_s)
47
+ end
48
+ end
49
+
50
+ def all_features
51
+ mutex.synchronize do
52
+ cleanup_expired
53
+ store.keys
54
+ end
55
+ end
56
+
57
+ def clear
58
+ mutex.synchronize do
59
+ @store = {}
60
+ @ttl = {}
61
+ end
62
+ end
63
+
64
+ def set_ttl(feature_name, seconds)
65
+ mutex.synchronize do
66
+ ttl[feature_name.to_s] = Time.now.to_i + seconds
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ attr_reader :store, :mutex, :ttl, :default_ttl
73
+
74
+ def cleanup_expired
75
+ now = Time.now.to_i
76
+ expired_keys = ttl.select { |_key, expiry| expiry < now }.keys
77
+ expired_keys.each do |key|
78
+ store.delete(key)
79
+ ttl.delete(key)
80
+ end
81
+ end
82
+
83
+ def update_ttl(feature_name)
84
+ ttl[feature_name] = Time.now.to_i + default_ttl
85
+ end
86
+
87
+ def serialize_value(value)
88
+ case value
89
+ when Hash, Array
90
+ Marshal.dump(value)
91
+ else
92
+ value
93
+ end
94
+ end
95
+
96
+ def deserialize_value(value)
97
+ return nil if value.nil?
98
+
99
+ case value
100
+ when String
101
+ # Try to unmarshal if it's a serialized hash/array
102
+ begin
103
+ Marshal.load(value)
104
+ rescue StandardError
105
+ value
106
+ end
107
+ else
108
+ value
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Magick
4
+ module Adapters
5
+ class Redis < Base
6
+ def initialize(redis_client = nil)
7
+ @redis = redis_client || default_redis_client
8
+ @namespace = 'magick:features'
9
+ raise AdapterError, 'Redis client is required' unless @redis
10
+ rescue StandardError => e
11
+ raise AdapterError, "Failed to initialize Redis adapter: #{e.message}"
12
+ end
13
+
14
+ def get(feature_name, key)
15
+ value = redis.hget(key_for(feature_name), key.to_s)
16
+ deserialize_value(value)
17
+ rescue StandardError => e
18
+ raise AdapterError, "Failed to get from Redis: #{e.message}"
19
+ end
20
+
21
+ def set(feature_name, key, value)
22
+ redis.hset(key_for(feature_name), key.to_s, serialize_value(value))
23
+ rescue StandardError => e
24
+ raise AdapterError, "Failed to set in Redis: #{e.message}"
25
+ end
26
+
27
+ def delete(feature_name)
28
+ redis.del(key_for(feature_name))
29
+ rescue StandardError => e
30
+ raise AdapterError, "Failed to delete from Redis: #{e.message}"
31
+ end
32
+
33
+ def exists?(feature_name)
34
+ redis.exists?(key_for(feature_name))
35
+ rescue StandardError => e
36
+ raise AdapterError, "Failed to check existence in Redis: #{e.message}"
37
+ end
38
+
39
+ def all_features
40
+ pattern = "#{namespace}:*"
41
+ keys = redis.keys(pattern)
42
+ keys.map { |key| key.sub("#{namespace}:", '') }
43
+ rescue StandardError => e
44
+ raise AdapterError, "Failed to get all features from Redis: #{e.message}"
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :redis, :namespace
50
+
51
+ def key_for(feature_name)
52
+ "#{namespace}:#{feature_name}"
53
+ end
54
+
55
+ def default_redis_client
56
+ return nil unless defined?(Redis)
57
+
58
+ require 'redis'
59
+ ::Redis.new
60
+ rescue StandardError
61
+ nil
62
+ end
63
+
64
+ def serialize_value(value)
65
+ case value
66
+ when Hash, Array
67
+ Marshal.dump(value)
68
+ when true
69
+ 'true'
70
+ when false
71
+ 'false'
72
+ else
73
+ value.to_s
74
+ end
75
+ end
76
+
77
+ def deserialize_value(value)
78
+ return nil if value.nil?
79
+
80
+ # Try to unmarshal if it's a serialized hash/array
81
+ if value.is_a?(String) && value.start_with?("\x04\x08")
82
+ begin
83
+ Marshal.load(value)
84
+ rescue StandardError
85
+ value
86
+ end
87
+ elsif value == 'true'
88
+ true
89
+ elsif value == 'false'
90
+ false
91
+ else
92
+ value
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end