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
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Magick
4
+ module TestingHelpers
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def with_feature_enabled(feature_name, &block)
11
+ feature = Magick.features[feature_name.to_s] || Magick[feature_name]
12
+ original_value = feature.value
13
+ feature.set_value(true)
14
+ yield
15
+ ensure
16
+ feature.set_value(original_value)
17
+ end
18
+
19
+ def with_feature_disabled(feature_name, &block)
20
+ feature = Magick.features[feature_name.to_s] || Magick[feature_name]
21
+ original_value = feature.value
22
+ feature.set_value(false)
23
+ yield
24
+ ensure
25
+ feature.set_value(original_value)
26
+ end
27
+
28
+ def with_feature_value(feature_name, value, &block)
29
+ feature = Magick.features[feature_name.to_s] || Magick[feature_name]
30
+ original_value = feature.value
31
+ feature.set_value(value)
32
+ yield
33
+ ensure
34
+ feature.set_value(original_value)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ # Include in RSpec
41
+ if defined?(RSpec)
42
+ RSpec.configure do |config|
43
+ config.include Magick::TestingHelpers
44
+ end
45
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Magick
4
+ VERSION = '0.7.0'
5
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Magick
4
+ class Versioning
5
+ class Version
6
+ attr_reader :version, :feature_data, :timestamp, :created_by
7
+
8
+ def initialize(version, feature_data, created_by: nil)
9
+ @version = version
10
+ @feature_data = feature_data
11
+ @timestamp = Time.now
12
+ @created_by = created_by
13
+ end
14
+
15
+ def to_h
16
+ {
17
+ version: version,
18
+ feature_data: feature_data,
19
+ timestamp: timestamp.iso8601,
20
+ created_by: created_by
21
+ }
22
+ end
23
+ end
24
+
25
+ def initialize(adapter_registry)
26
+ @adapter_registry = adapter_registry
27
+ @versions = {}
28
+ @mutex = Mutex.new
29
+ end
30
+
31
+ def save_version(feature_name, version: nil, created_by: nil)
32
+ feature = Magick.features[feature_name.to_s] || Magick[feature_name]
33
+ version ||= next_version(feature_name)
34
+ version_data = Version.new(version, feature.to_h, created_by: created_by)
35
+
36
+ @mutex.synchronize do
37
+ @versions[feature_name.to_s] ||= []
38
+ @versions[feature_name.to_s] << version_data
39
+ # Store in adapter
40
+ @adapter_registry.set(feature_name, "version_#{version}", version_data.to_h)
41
+ end
42
+
43
+ # Rails 8+ event
44
+ if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
45
+ Magick::Rails::Events.version_saved(feature_name, version: version, created_by: created_by)
46
+ end
47
+
48
+ version_data
49
+ end
50
+
51
+ def rollback(feature_name, version)
52
+ versions = get_versions(feature_name)
53
+ target_version = versions.find { |v| v.version == version }
54
+ return false unless target_version
55
+
56
+ feature = Magick.features[feature_name.to_s] || Magick[feature_name]
57
+ feature_data = target_version.feature_data
58
+
59
+ # Restore feature state
60
+ feature.set_value(feature_data[:value]) if feature_data[:value]
61
+ feature.set_status(feature_data[:status]) if feature_data[:status]
62
+
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
75
+ end
76
+ end
77
+ end
78
+
79
+ # Rails 8+ event
80
+ if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
81
+ Magick::Rails::Events.rollback(feature_name, version: version)
82
+ end
83
+
84
+ true
85
+ end
86
+
87
+ def get_versions(feature_name)
88
+ @versions[feature_name.to_s] || []
89
+ end
90
+
91
+ private
92
+
93
+ def next_version(feature_name)
94
+ versions = get_versions(feature_name)
95
+ versions.empty? ? 1 : versions.last.version + 1
96
+ end
97
+ end
98
+ end
data/lib/magick.rb ADDED
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'magick/version'
4
+ require_relative 'magick/feature'
5
+ require_relative 'magick/adapters/base'
6
+ require_relative 'magick/adapters/memory'
7
+ require_relative 'magick/adapters/redis'
8
+ require_relative 'magick/adapters/registry'
9
+ require_relative 'magick/targeting/base'
10
+ require_relative 'magick/targeting/user'
11
+ require_relative 'magick/targeting/group'
12
+ require_relative 'magick/targeting/role'
13
+ require_relative 'magick/targeting/percentage'
14
+ require_relative 'magick/targeting/request_percentage'
15
+ require_relative 'magick/errors'
16
+
17
+ require_relative 'magick/audit_log'
18
+ require_relative 'magick/performance_metrics'
19
+ require_relative 'magick/export_import'
20
+ require_relative 'magick/versioning'
21
+ require_relative 'magick/circuit_breaker'
22
+ require_relative 'magick/testing_helpers'
23
+ require_relative 'magick/feature_dependency'
24
+ require_relative 'magick/config'
25
+
26
+ # Load DSL early if Rails is present, so it's available in initializers
27
+ require_relative 'magick/dsl' if defined?(Rails)
28
+
29
+ module Magick
30
+ class << self
31
+ attr_accessor :adapter_registry, :default_adapter, :performance_metrics, :audit_log, :versioning,
32
+ :warn_on_deprecated
33
+
34
+ def configure(&block)
35
+ @performance_metrics ||= PerformanceMetrics.new
36
+ @audit_log ||= AuditLog.new
37
+ @warn_on_deprecated ||= false
38
+
39
+ # Support both old style and new DSL style
40
+ return unless block_given?
41
+
42
+ if block.arity == 0
43
+ # New DSL style
44
+ ConfigDSL.configure(&block)
45
+ else
46
+ # Old style
47
+ yield self
48
+ end
49
+ end
50
+
51
+ def [](feature_name)
52
+ # Return registered feature if it exists, otherwise create new instance
53
+ features[feature_name.to_s] || Feature.new(feature_name, adapter_registry || default_adapter_registry)
54
+ end
55
+
56
+ def features
57
+ @features ||= {}
58
+ end
59
+
60
+ def register_feature(name, **options)
61
+ feature = Feature.new(name, adapter_registry || default_adapter_registry, **options)
62
+ features[name.to_s] = feature
63
+ feature
64
+ end
65
+
66
+ def enabled?(feature_name, context = {})
67
+ feature = features[feature_name.to_s] || self[feature_name]
68
+ feature.enabled?(context)
69
+ end
70
+
71
+ # Reload a feature from the adapter (useful when feature is changed externally)
72
+ def reload_feature(feature_name)
73
+ feature = features[feature_name.to_s] || self[feature_name]
74
+ feature.reload
75
+ end
76
+
77
+ def disabled?(feature_name, context = {})
78
+ !enabled?(feature_name, context)
79
+ end
80
+
81
+ def exists?(feature_name)
82
+ features.key?(feature_name.to_s) || (adapter_registry || default_adapter_registry).exists?(feature_name)
83
+ end
84
+
85
+ def bulk_enable(feature_names, context = {})
86
+ feature_names.map do |name|
87
+ feature = features[name.to_s] || self[name]
88
+ feature.set_value(true) if feature.type == :boolean
89
+ feature
90
+ end
91
+ end
92
+
93
+ def bulk_disable(feature_names, context = {})
94
+ feature_names.map do |name|
95
+ feature = features[name.to_s] || self[name]
96
+ feature.set_value(false) if feature.type == :boolean
97
+ feature
98
+ end
99
+ end
100
+
101
+ def export(format: :json)
102
+ case format
103
+ when :json
104
+ ExportImport.export_json(features)
105
+ when :hash
106
+ ExportImport.export(features)
107
+ else
108
+ ExportImport.export(features)
109
+ end
110
+ end
111
+
112
+ def import(data, format: :json)
113
+ imported = ExportImport.import(data, adapter_registry || default_adapter_registry)
114
+ imported.each { |name, feature| features[name] = feature }
115
+ imported
116
+ end
117
+
118
+ def versioning
119
+ @versioning ||= Versioning.new(adapter_registry || default_adapter_registry)
120
+ end
121
+
122
+ def reset!
123
+ @features = {}
124
+ @adapter_registry = nil
125
+ @default_adapter = nil
126
+ @performance_metrics&.clear!
127
+ end
128
+
129
+ private
130
+
131
+ def default_adapter_registry
132
+ @default_adapter_registry ||= begin
133
+ memory_adapter = Adapters::Memory.new
134
+ redis_adapter = begin
135
+ Adapters::Redis.new if defined?(Redis)
136
+ rescue AdapterError
137
+ nil
138
+ end
139
+ Adapters::Registry.new(memory_adapter, redis_adapter)
140
+ end
141
+ end
142
+ end
143
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: magick-feature-flags
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.7.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Lobanov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-11-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.50'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.50'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop-rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.22'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.22'
55
+ description: Magick is a better version of Flipper feature-toggle gem. It is absolutely
56
+ performant and memory efficient.
57
+ email:
58
+ - woblavobla@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - LICENSE
64
+ - README.md
65
+ - lib/generators/magick/install/install_generator.rb
66
+ - lib/generators/magick/install/templates/README
67
+ - lib/generators/magick/install/templates/magick.rb
68
+ - lib/magick.rb
69
+ - lib/magick/adapters/base.rb
70
+ - lib/magick/adapters/memory.rb
71
+ - lib/magick/adapters/redis.rb
72
+ - lib/magick/adapters/registry.rb
73
+ - lib/magick/audit_log.rb
74
+ - lib/magick/circuit_breaker.rb
75
+ - lib/magick/config.rb
76
+ - lib/magick/dsl.rb
77
+ - lib/magick/errors.rb
78
+ - lib/magick/export_import.rb
79
+ - lib/magick/feature.rb
80
+ - lib/magick/feature_dependency.rb
81
+ - lib/magick/feature_variant.rb
82
+ - lib/magick/performance_metrics.rb
83
+ - lib/magick/rails.rb
84
+ - lib/magick/rails/event_subscriber.rb
85
+ - lib/magick/rails/events.rb
86
+ - lib/magick/rails/railtie.rb
87
+ - lib/magick/targeting/base.rb
88
+ - lib/magick/targeting/complex.rb
89
+ - lib/magick/targeting/custom_attribute.rb
90
+ - lib/magick/targeting/date_range.rb
91
+ - lib/magick/targeting/group.rb
92
+ - lib/magick/targeting/ip_address.rb
93
+ - lib/magick/targeting/percentage.rb
94
+ - lib/magick/targeting/request_percentage.rb
95
+ - lib/magick/targeting/role.rb
96
+ - lib/magick/targeting/user.rb
97
+ - lib/magick/testing_helpers.rb
98
+ - lib/magick/version.rb
99
+ - lib/magick/versioning.rb
100
+ homepage: https://github.com/andrew-woblavobla/magick
101
+ licenses:
102
+ - MIT
103
+ metadata: {}
104
+ post_install_message:
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: 3.0.0
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ requirements: []
119
+ rubygems_version: 3.0.3.1
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: A performant and memory-efficient feature toggle gem
123
+ test_files: []