magick-feature-flags 0.9.24 → 0.9.25

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.
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Magick
4
+ class Documentation
5
+ class << self
6
+ def generate(format: :markdown)
7
+ features = Magick.features.values
8
+ case format.to_sym
9
+ when :markdown
10
+ generate_markdown(features)
11
+ when :html
12
+ generate_html(features)
13
+ when :json
14
+ generate_json(features)
15
+ else
16
+ raise ArgumentError, "Unknown format: #{format}"
17
+ end
18
+ end
19
+
20
+ def generate_markdown(features = nil)
21
+ features ||= Magick.features.values
22
+ output = ["# Feature Flags Documentation\n", "Generated: #{Time.now}\n\n"]
23
+
24
+ features.each do |feature|
25
+ output << "## #{feature.display_name || feature.name}\n\n"
26
+ output << "**Name:** `#{feature.name}`\n\n"
27
+ output << "**Type:** #{feature.type.to_s.capitalize}\n\n"
28
+ output << "**Status:** #{feature.status.to_s.capitalize}\n\n"
29
+ output << "**Default Value:** `#{feature.default_value.inspect}`\n\n"
30
+ output << "**Description:** #{feature.description || 'No description'}\n\n"
31
+
32
+ # Access targeting via instance_variable_get since it's private
33
+ targeting = feature.instance_variable_get(:@targeting) || {}
34
+ if targeting.any?
35
+ output << "### Targeting Rules\n\n"
36
+ targeting.each do |key, value|
37
+ case key.to_sym
38
+ when :user
39
+ user_list = value.is_a?(Array) ? value : [value]
40
+ user_list.each do |user_id|
41
+ output << "- **user_id:** #{user_id}\n"
42
+ end
43
+ when :group
44
+ group_list = value.is_a?(Array) ? value : [value]
45
+ group_list.each do |group|
46
+ output << "- **group:** #{group}\n"
47
+ end
48
+ when :role
49
+ role_list = value.is_a?(Array) ? value : [value]
50
+ role_list.each do |role|
51
+ output << "- **role:** #{role}\n"
52
+ end
53
+ when :percentage_users
54
+ output << "- **percentage_users:** #{value}%\n"
55
+ when :percentage_requests
56
+ output << "- **percentage_requests:** #{value}%\n"
57
+ else
58
+ output << "- **#{key}:** #{value.inspect}\n"
59
+ end
60
+ end
61
+ output << "\n"
62
+ end
63
+
64
+ if feature.dependencies.any?
65
+ output << "### Dependencies\n\n"
66
+ feature.dependencies.each do |dep|
67
+ output << "- `#{dep}`\n"
68
+ end
69
+ output << "\n"
70
+ end
71
+
72
+ output << "---\n\n"
73
+ end
74
+
75
+ output.join
76
+ end
77
+
78
+ def generate_html(features = nil)
79
+ features ||= Magick.features.values
80
+ html = <<~HTML
81
+ <!DOCTYPE html>
82
+ <html>
83
+ <head>
84
+ <title>Feature Flags Documentation</title>
85
+ <style>
86
+ body { font-family: Arial, sans-serif; margin: 20px; }
87
+ table { border-collapse: collapse; width: 100%; margin: 20px 0; }
88
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
89
+ th { background-color: #f2f2f2; }
90
+ .status-active { color: green; }
91
+ .status-deprecated { color: orange; }
92
+ .status-inactive { color: red; }
93
+ </style>
94
+ </head>
95
+ <body>
96
+ <h1>Feature Flags Documentation</h1>
97
+ <p>Generated: #{Time.now}</p>
98
+ <table>
99
+ <thead>
100
+ <tr>
101
+ <th>Name</th>
102
+ <th>Type</th>
103
+ <th>Status</th>
104
+ <th>Default Value</th>
105
+ <th>Description</th>
106
+ </tr>
107
+ </thead>
108
+ <tbody>
109
+ HTML
110
+
111
+ features.each do |feature|
112
+ html << <<~HTML
113
+ <tr>
114
+ <td><code>#{feature.name}</code></td>
115
+ <td>#{feature.type.to_s.capitalize}</td>
116
+ <td class="status-#{feature.status}">#{feature.status.to_s.capitalize}</td>
117
+ <td><code>#{feature.default_value.inspect}</code></td>
118
+ <td>#{feature.description || 'No description'}</td>
119
+ </tr>
120
+ HTML
121
+ end
122
+
123
+ html << <<~HTML
124
+ </tbody>
125
+ </table>
126
+ </body>
127
+ </html>
128
+ HTML
129
+
130
+ html
131
+ end
132
+
133
+ def generate_json(features = nil)
134
+ features ||= Magick.features.values
135
+ features_data = features.map do |feature|
136
+ # Use to_h to get all feature data including targeting
137
+ feature_hash = feature.to_h
138
+ {
139
+ name: feature_hash[:name],
140
+ display_name: feature_hash[:display_name],
141
+ type: feature_hash[:type].to_s,
142
+ status: feature_hash[:status].to_s,
143
+ default_value: feature_hash[:default_value],
144
+ description: feature_hash[:description],
145
+ targeting: feature_hash[:targeting] || {},
146
+ dependencies: feature.dependencies
147
+ }
148
+ end
149
+ JSON.pretty_generate(features_data)
150
+ end
151
+ end
152
+ end
153
+ end
@@ -107,6 +107,20 @@ module Magick
107
107
  # Check complex conditions
108
108
  return false if targeting[:complex_conditions] && !complex_conditions_match?(context,
109
109
  targeting[:complex_conditions])
110
+
111
+ # Check user/group/role/percentage targeting
112
+ targeting_result = check_targeting(context)
113
+ if targeting_result.nil?
114
+ # Targeting doesn't match - return false
115
+ return false
116
+ else
117
+ # Targeting matches - for boolean features, return true directly
118
+ # For string/number features, still check the value
119
+ if type == :boolean
120
+ return true
121
+ end
122
+ # For string/number, continue to check value below
123
+ end
110
124
  end
111
125
 
112
126
  # Get value and check based on type
@@ -151,7 +165,30 @@ module Magick
151
165
  # Fast path: check targeting rules first (only if targeting exists)
152
166
  unless @_targeting_empty
153
167
  targeting_result = check_targeting(context)
154
- return targeting_result unless targeting_result.nil?
168
+ # If targeting matches (returns truthy), return the stored value
169
+ # If targeting doesn't match (returns nil), continue to return default value
170
+ unless targeting_result.nil?
171
+ # Targeting matches - return stored value (or load it if not initialized)
172
+ if @stored_value_initialized
173
+ return @stored_value
174
+ else
175
+ # Load from adapter
176
+ loaded_value = load_value_from_adapter
177
+ if loaded_value.nil?
178
+ # Value not found in adapter, use default and cache it
179
+ @stored_value = default_value
180
+ @stored_value_initialized = true
181
+ return default_value
182
+ else
183
+ # Value found in adapter, use it and mark as initialized
184
+ @stored_value = loaded_value
185
+ @stored_value_initialized = true
186
+ return loaded_value
187
+ end
188
+ end
189
+ end
190
+ # Targeting doesn't match - return default value
191
+ return default_value
155
192
  end
156
193
 
157
194
  # Fast path: use cached value if initialized (avoid adapter calls)
@@ -524,6 +561,30 @@ module Magick
524
561
  }
525
562
  end
526
563
 
564
+ def save_targeting
565
+ # Save targeting to adapter (this updates memory synchronously, then Redis/AR)
566
+ adapter_registry.set(name, 'targeting', targeting)
567
+
568
+ # Update the feature in Magick.features if it's registered
569
+ if Magick.features.key?(name)
570
+ Magick.features[name].instance_variable_set(:@targeting, targeting.dup)
571
+ # Update targeting empty cache for performance
572
+ Magick.features[name].instance_variable_set(:@_targeting_empty, targeting.empty?)
573
+ end
574
+
575
+ # Update local targeting empty cache for performance
576
+ @_targeting_empty = targeting.empty?
577
+
578
+ # Explicitly publish cache invalidation to other processes via Pub/Sub
579
+ # This ensures other Rails app instances/consoles invalidate their cache and reload
580
+ # Note: We don't invalidate local cache here because we just updated it above
581
+ # The set method publishes cache invalidation, but we also publish here to ensure
582
+ # it happens even if Redis update fails or is async
583
+ if adapter_registry.respond_to?(:publish_cache_invalidation)
584
+ adapter_registry.publish_cache_invalidation(name)
585
+ end
586
+ end
587
+
527
588
  private
528
589
 
529
590
  attr_reader :targeting
@@ -847,28 +908,6 @@ module Magick
847
908
  true
848
909
  end
849
910
 
850
- def save_targeting
851
- # Save targeting to adapter (this triggers cache invalidation via Pub/Sub)
852
- adapter_registry.set(name, 'targeting', targeting)
853
-
854
- # Update the feature in Magick.features if it's registered
855
- if Magick.features.key?(name)
856
- Magick.features[name].instance_variable_set(:@targeting, targeting.dup)
857
- # Update targeting empty cache for performance
858
- Magick.features[name].instance_variable_set(:@_targeting_empty, targeting.empty?)
859
- end
860
-
861
- # Update local targeting empty cache for performance
862
- @_targeting_empty = targeting.empty?
863
-
864
- # Explicitly trigger cache invalidation for targeting updates
865
- # Targeting changes affect enabled? checks, so we need immediate cache invalidation
866
- # even if async updates are enabled
867
- return unless adapter_registry.respond_to?(:invalidate_cache)
868
-
869
- adapter_registry.invalidate_cache(name)
870
- end
871
-
872
911
  def default_for_type
873
912
  case type
874
913
  when :boolean
@@ -10,6 +10,15 @@ if defined?(Rails)
10
10
  module Magick
11
11
  module Rails
12
12
  class Railtie < ::Rails::Railtie
13
+ # Configure inflector to keep AdminUI as AdminUI (not AdminUi)
14
+ # This must run very early, before any routes or constants are loaded
15
+ initializer 'magick.inflector', before: :set_load_path do
16
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
17
+ inflect.acronym 'AdminUI'
18
+ inflect.acronym 'UI'
19
+ end
20
+ end
21
+
13
22
  # Make DSL available early so it works in config/initializers/features.rb
14
23
  initializer 'magick.dsl', before: :load_config_initializers do
15
24
  # Ensure DSL is available globally for initializers
@@ -54,17 +63,13 @@ if defined?(Rails)
54
63
  end
55
64
 
56
65
  # Ensure adapter_registry is always set (fallback to default if not configured)
57
- unless Magick.adapter_registry
58
- Magick.adapter_registry = Magick.default_adapter_registry
59
- end
66
+ Magick.adapter_registry = Magick.default_adapter_registry unless Magick.adapter_registry
60
67
 
61
68
  # Ensure adapter_registry is set and Redis tracking is enabled after all initializers have run
62
69
  # This ensures user's config/initializers/magick.rb has been loaded
63
70
  config.after_initialize do
64
71
  # Ensure adapter_registry is set (fallback to default if not configured)
65
- unless Magick.adapter_registry
66
- Magick.adapter_registry = Magick.default_adapter_registry
67
- end
72
+ Magick.adapter_registry = Magick.default_adapter_registry unless Magick.adapter_registry
68
73
 
69
74
  # Force enable Redis tracking if Redis adapter is available
70
75
  # This is a final safety net to ensure stats are collected
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Magick
4
- VERSION = '0.9.24'
4
+ VERSION = '0.9.25'
5
5
  end
data/lib/magick.rb CHANGED
@@ -5,6 +5,12 @@ require_relative 'magick/feature'
5
5
  require_relative 'magick/adapters/base'
6
6
  require_relative 'magick/adapters/memory'
7
7
  require_relative 'magick/adapters/redis'
8
+ # Active Record adapter is loaded conditionally - only if ActiveRecord is available
9
+ begin
10
+ require_relative 'magick/adapters/active_record' if defined?(::ActiveRecord::Base)
11
+ rescue LoadError, NameError
12
+ # ActiveRecord not available, skip
13
+ end
8
14
  require_relative 'magick/adapters/registry'
9
15
  require_relative 'magick/targeting/base'
10
16
  require_relative 'magick/targeting/user'
@@ -21,6 +27,9 @@ require_relative 'magick/versioning'
21
27
  require_relative 'magick/circuit_breaker'
22
28
  require_relative 'magick/testing_helpers'
23
29
  require_relative 'magick/feature_dependency'
30
+ require_relative 'magick/documentation'
31
+ # AdminUI is loaded conditionally via configuration
32
+ # It is not loaded by default - must be enabled in Magick.configure
24
33
  require_relative 'magick/config'
25
34
 
26
35
  # Always load DSL - it will make itself available when Rails is detected
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: magick-feature-flags
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.24
4
+ version: 0.9.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Lobanov
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-12-02 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rspec
@@ -52,6 +51,40 @@ dependencies:
52
51
  - - "~>"
53
52
  - !ruby/object:Gem::Version
54
53
  version: '2.22'
54
+ - !ruby/object:Gem::Dependency
55
+ name: activerecord
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '6.0'
61
+ - - "<"
62
+ - !ruby/object:Gem::Version
63
+ version: '9.0'
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '6.0'
71
+ - - "<"
72
+ - !ruby/object:Gem::Version
73
+ version: '9.0'
74
+ - !ruby/object:Gem::Dependency
75
+ name: sqlite3
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '1.6'
81
+ type: :development
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '1.6'
55
88
  description: Magick is a better free version of Flipper feature-toggle gem. It is
56
89
  absolutely performant and memory efficient (by my opinion).
57
90
  email:
@@ -62,17 +95,26 @@ extra_rdoc_files: []
62
95
  files:
63
96
  - LICENSE
64
97
  - README.md
98
+ - lib/generators/magick/active_record/active_record_generator.rb
99
+ - lib/generators/magick/active_record/templates/create_magick_features.rb
65
100
  - lib/generators/magick/install/install_generator.rb
66
101
  - lib/generators/magick/install/templates/README
67
102
  - lib/generators/magick/install/templates/magick.rb
68
103
  - lib/magick.rb
104
+ - lib/magick/adapters/active_record.rb
69
105
  - lib/magick/adapters/base.rb
70
106
  - lib/magick/adapters/memory.rb
71
107
  - lib/magick/adapters/redis.rb
72
108
  - lib/magick/adapters/registry.rb
109
+ - lib/magick/admin_ui.rb
110
+ - lib/magick/admin_ui/config/routes.rb
111
+ - lib/magick/admin_ui/engine.rb
112
+ - lib/magick/admin_ui/helpers.rb
113
+ - lib/magick/admin_ui/routes.rb
73
114
  - lib/magick/audit_log.rb
74
115
  - lib/magick/circuit_breaker.rb
75
116
  - lib/magick/config.rb
117
+ - lib/magick/documentation.rb
76
118
  - lib/magick/dsl.rb
77
119
  - lib/magick/errors.rb
78
120
  - lib/magick/export_import.rb
@@ -102,7 +144,6 @@ homepage: https://github.com/andrew-woblavobla/magick
102
144
  licenses:
103
145
  - MIT
104
146
  metadata: {}
105
- post_install_message:
106
147
  rdoc_options: []
107
148
  require_paths:
108
149
  - lib
@@ -117,8 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
117
158
  - !ruby/object:Gem::Version
118
159
  version: '0'
119
160
  requirements: []
120
- rubygems_version: 3.5.19
121
- signing_key:
161
+ rubygems_version: 3.7.2
122
162
  specification_version: 4
123
163
  summary: A performant and memory-efficient feature toggle gem
124
164
  test_files: []