magick-feature-flags 0.9.23 → 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.
- checksums.yaml +4 -4
- data/README.md +213 -5
- data/lib/generators/magick/active_record/active_record_generator.rb +44 -0
- data/lib/generators/magick/active_record/templates/create_magick_features.rb +20 -0
- data/lib/magick/adapters/active_record.rb +267 -0
- data/lib/magick/adapters/registry.rb +106 -33
- data/lib/magick/admin_ui/config/routes.rb +13 -0
- data/lib/magick/admin_ui/engine.rb +98 -0
- data/lib/magick/admin_ui/helpers.rb +33 -0
- data/lib/magick/admin_ui/routes.rb +17 -0
- data/lib/magick/admin_ui.rb +42 -0
- data/lib/magick/config.rb +115 -9
- data/lib/magick/documentation.rb +153 -0
- data/lib/magick/feature.rb +62 -23
- data/lib/magick/rails/railtie.rb +18 -7
- data/lib/magick/version.rb +1 -1
- data/lib/magick.rb +9 -0
- metadata +46 -6
|
@@ -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
|
data/lib/magick/feature.rb
CHANGED
|
@@ -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
|
|
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
|
data/lib/magick/rails/railtie.rb
CHANGED
|
@@ -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
|
|
@@ -25,7 +34,13 @@ if defined?(Rails)
|
|
|
25
34
|
if defined?(Redis)
|
|
26
35
|
begin
|
|
27
36
|
redis_url = app.config.respond_to?(:redis_url) ? app.config.redis_url : nil
|
|
28
|
-
|
|
37
|
+
# Use database 1 for Magick by default to avoid conflicts with Rails cache (which uses DB 0)
|
|
38
|
+
# Users can override this in their config/initializers/magick.rb
|
|
39
|
+
redis_db = 1
|
|
40
|
+
redis_options = {}
|
|
41
|
+
redis_options[:url] = redis_url if redis_url
|
|
42
|
+
redis_options[:db] = redis_db
|
|
43
|
+
redis_client = redis_url ? ::Redis.new(redis_options) : ::Redis.new(db: redis_db)
|
|
29
44
|
memory_adapter = Adapters::Memory.new
|
|
30
45
|
redis_adapter = Adapters::Redis.new(redis_client)
|
|
31
46
|
magick.adapter_registry = Adapters::Registry.new(memory_adapter, redis_adapter)
|
|
@@ -48,17 +63,13 @@ if defined?(Rails)
|
|
|
48
63
|
end
|
|
49
64
|
|
|
50
65
|
# Ensure adapter_registry is always set (fallback to default if not configured)
|
|
51
|
-
unless Magick.adapter_registry
|
|
52
|
-
Magick.adapter_registry = Magick.default_adapter_registry
|
|
53
|
-
end
|
|
66
|
+
Magick.adapter_registry = Magick.default_adapter_registry unless Magick.adapter_registry
|
|
54
67
|
|
|
55
68
|
# Ensure adapter_registry is set and Redis tracking is enabled after all initializers have run
|
|
56
69
|
# This ensures user's config/initializers/magick.rb has been loaded
|
|
57
70
|
config.after_initialize do
|
|
58
71
|
# Ensure adapter_registry is set (fallback to default if not configured)
|
|
59
|
-
unless Magick.adapter_registry
|
|
60
|
-
Magick.adapter_registry = Magick.default_adapter_registry
|
|
61
|
-
end
|
|
72
|
+
Magick.adapter_registry = Magick.default_adapter_registry unless Magick.adapter_registry
|
|
62
73
|
|
|
63
74
|
# Force enable Redis tracking if Redis adapter is available
|
|
64
75
|
# This is a final safety net to ensure stats are collected
|
data/lib/magick/version.rb
CHANGED
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.
|
|
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:
|
|
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.
|
|
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: []
|