rails-feature-flip 0.1.1 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 534e43865643944f6c7758b89e0788d25a92afbc73097bbaf924cebcdc970515
4
- data.tar.gz: a0bbde657ff531b12cda61e103caf8f1025e3b4d3befe54c57cd543f79c6a3ea
3
+ metadata.gz: 31fa37d8001db49630a6166dc989f7df70cd2e93bcd2d808a64ba2891a8ddf25
4
+ data.tar.gz: 00c2471a2ce557a8f3c4f82213ad53e277cc75a92469f575ec9ae50881fc9ea0
5
5
  SHA512:
6
- metadata.gz: bbaeb831c8f33edf473efc6784d72177e47276f9201340847089651b5f637db38f233e68305f88512abab0a38a8e7e0314b2eb1443d61f3be514e8b34f589702
7
- data.tar.gz: a8b975432fe88b54367100991fdc1ddb94bb12aee27cea3ab2011110cde0479c30a7ccd441123ba585de81b25751be6b425b63350318a715bad5a3b9670704a5
6
+ metadata.gz: 15ccb56aab858a4c5b4f40076c20b922561b6f4b071e9af57fcb719507b7e48bc5905ef88fc7c49c5aa61a295de8f5b6f764502f9e33899b4a86ea70fb23b0f3
7
+ data.tar.gz: fb2cb9a153aac70b04741afcf91f86952ecd53e02a64b09793fef493df90b0e89ab458c5eab725979630d3c87c05b1be1e2fc8df3c5b257b7622bad7b804048f
data/.rubocop.yml ADDED
@@ -0,0 +1,25 @@
1
+ inherit_mode:
2
+ merge:
3
+ - Exclude
4
+
5
+ plugins:
6
+ - rubocop-minitest
7
+ - rubocop-rake
8
+
9
+ AllCops:
10
+ TargetRubyVersion: 3.0
11
+ NewCops: enable
12
+ SuggestExtensions: false
13
+ Exclude:
14
+ - test/tmp/**/*
15
+ - '*.md'
16
+
17
+ Style/Documentation:
18
+ Enabled: false
19
+
20
+ Layout/LineLength:
21
+ Max: 120
22
+
23
+ Metrics/ClassLength:
24
+ Exclude:
25
+ - test/**/*
data/CHANGELOG.md CHANGED
@@ -1,9 +1,43 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2024-02-23
3
+ ## [1.0.0] - 2026-03-29
4
+
5
+ ### Breaking Changes
6
+
7
+ - Requires Ruby >= 3.0 (was >= 2.6)
8
+ - Requires Rails >= 7.0 (was >= 5.0)
9
+ - Generated feature classes now use ActiveModel::Attributes instead of hand-rolled attr_reader/initialize
10
+
11
+ ### Bug Fixes
12
+
13
+ - Fix generator namespace typo (`rails_feature_flag` → `rails_feature_flip`) in README, USAGE, and install message
14
+ - Fix `all.rb` loader using fragile relative path — now uses `File.expand_path(__dir__)`
15
+ - Fix singular/plural mismatch in install generator (`config/feature` → `config/features`)
16
+ - Fix install generator using `File.read` with CWD-relative path instead of destination root
17
+ - Use idiomatic `empty_directory` in install generator instead of `Dir.mkdir`
18
+
19
+ ### New Features
4
20
 
5
- - start of the gem
21
+ - **ActiveModel::Attributes**: Generated features include type casting, built-in defaults, and attribute introspection
22
+ - **Default values**: `--defaults enabled:false amount:100` flag on the feature generator
23
+ - **Auto-enabled attribute**: `enabled:boolean` is added by default (skip with `--no-enabled`)
24
+ - **Feature namespacing**: `Billing::InvoicePdf` generates nested modules in subdirectories
25
+ - **Controller/view helpers**: `feature_enabled?(:name)` and `require_feature :name` via Railtie
26
+ - **Test helper**: `with_feature(:name, enabled: true) { ... }` for overriding features in tests
27
+ - **Feature registry**: `RailsFeatureFlip::Registry.all`, `.names`, `.find(:name)`
28
+ - **Runtime toggles**: `RailsFeatureFlip.enable(:name)`, `.disable(:name)`, `.toggle(:name)`
29
+ - **Rake task**: `rake features` lists all registered features with status and attributes
30
+
31
+ ### Quality
32
+
33
+ - Added Rubocop with rubocop-minitest and rubocop-rake
34
+ - Added 40 tests with 152 assertions covering generators, helpers, registry, and toggles
35
+ - CI matrix expanded to Ruby 3.0, 3.1, 3.2, 3.3, 3.4, 4.0
6
36
 
7
37
  ## [0.1.1] - 2024-02-23
8
38
 
9
- - initial release on rubygems.org
39
+ - Initial release on rubygems.org
40
+
41
+ ## [0.1.0] - 2024-02-23
42
+
43
+ - Start of the gem
data/CODE_OF_CONDUCT.md CHANGED
@@ -39,7 +39,7 @@ This Code of Conduct applies within all community spaces, and also applies when
39
39
 
40
40
  ## Enforcement
41
41
 
42
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at ben@byder.io. All complaints will be reviewed and investigated promptly and fairly.
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at ben@bdeutscher.org. All complaints will be reviewed and investigated promptly and fairly.
43
43
 
44
44
  All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
45
 
data/README.md CHANGED
@@ -1,8 +1,38 @@
1
1
  # RailsFeatureFlip
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ This gem provides a generator to create feature flip config classes that can be used to define what features can be used in what stage and/or how they should behave in what stage.
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/rails/feature/flip`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+
6
+ ## Example
7
+
8
+ bin/rails generate rails_feature_flip:feature Thing enabled:boolean foo bar
9
+
10
+ This will create:
11
+ config/features/thing_feature.rb
12
+
13
+ Then in application.rb:
14
+ config.x.features.help_center = Features::Thing.new(enabled: false, foo: 'bar', bar: 'baz')
15
+ With that you can use:
16
+ App.features.thing.enabled?
17
+ # => false
18
+ App.features.thing.foo
19
+ # => bar
20
+
21
+ The main benefit over the default `config.x` namespace is the readability in code whereas
22
+
23
+ if App.features.thing.enabled?
24
+ # do something with feature "thing"
25
+ end
26
+
27
+ is more readable than
28
+
29
+ if Rails.env.development?
30
+ # do something with feature "thing" in development stage
31
+ elsif Rails.env.test?
32
+ # do something with feature "thing" in test stage
33
+ else
34
+ # do something with feature "thing" in production stage
35
+ end
6
36
 
7
37
  ## Installation
8
38
 
data/Rakefile CHANGED
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rake/testtask"
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+ require 'rubocop/rake_task'
5
6
 
6
7
  Rake::TestTask.new(:test) do |t|
7
- t.libs << "test"
8
- t.libs << "lib"
9
- t.test_files = FileList["test/**/test_*.rb"]
8
+ t.libs << 'test'
9
+ t.libs << 'lib'
10
+ t.test_files = FileList['test/**/test_*.rb']
10
11
  end
11
12
 
12
- task default: :test
13
+ RuboCop::RakeTask.new
14
+
15
+ task default: %i[test rubocop]
@@ -1,16 +1,28 @@
1
1
  Description:
2
- Generates a feature file to manage a feature of the app
2
+ Generates a feature file to manage a feature of the app.
3
+ Uses ActiveModel::Attributes for type casting and defaults.
3
4
 
4
5
  Example:
5
- bin/rails generate rails_feature_flag:feature Thing enabled:boolean foo bar
6
+ bin/rails generate rails_feature_flip:feature Thing max_results:integer
7
+
8
+ With default values:
9
+ bin/rails generate rails_feature_flip:feature Thing max_results:integer --defaults enabled:false max_results:10
10
+
11
+ Namespaced feature (creates config/features/billing/invoice_pdf_feature.rb):
12
+ bin/rails generate rails_feature_flip:feature Billing::InvoicePdf max:integer
13
+
14
+ Without the auto-added enabled attribute:
15
+ bin/rails generate rails_feature_flip:feature Thing foo --no-enabled
6
16
 
7
17
  This will create:
8
18
  config/features/thing_feature.rb
9
19
 
10
20
  Then in application.rb:
11
- config.x.features.help_center = Features::Thing.new(enabled: false, foo: 'bar', bar: 'baz')
21
+ config.x.features.thing = Features::ThingFeature.new(enabled: true, max_results: 10)
12
22
  With that you can use:
13
23
  App.features.thing.enabled?
14
- # => false
15
- App.features.thing.foo
16
- # => bar
24
+ # => true
25
+ App.features.thing.max_results
26
+ # => 10
27
+
28
+ Supported types: boolean, integer, float, string (default)
@@ -6,18 +6,68 @@ module RailsFeatureFlip
6
6
  class FeatureGenerator < Rails::Generators::NamedBase
7
7
  source_root File.expand_path('templates', __dir__)
8
8
 
9
- argument :attributes, type: :array, default: [], banner: 'config_key[:boolean] (:boolean will generate a ? method)'
9
+ argument :attributes, type: :array, default: [],
10
+ banner: 'config_key[:type] (supported types: boolean, integer, float, string)'
11
+
12
+ class_option :defaults, type: :hash, default: {},
13
+ desc: 'Default values for attributes (e.g. --defaults enabled:false amount:100)'
14
+ class_option :enabled, type: :boolean, default: true,
15
+ desc: 'Add enabled:boolean attribute (use --no-enabled to skip)'
10
16
 
11
17
  # Generate a feature
12
18
  def generate_feature
13
19
  @name = name.underscore.downcase
14
- @klass_name = class_name
15
- @attributes = attributes
16
- @boolean_attributes = @attributes.collect { |attr| attr if attr.type == :boolean }.compact
17
- feature_file_path = Rails.root.join('config', 'features')
18
- feature_path = feature_file_path.join("#{@name}_feature.rb")
20
+ parts = class_name.split('::')
21
+ @namespace_parts = parts[0..-2]
22
+ @feature_class_name = "#{parts.last}Feature"
23
+ @attributes = prepend_enabled_attribute(attributes)
24
+ @boolean_attributes = @attributes.select { |attr| attr.type == :boolean }
25
+ @defaults = options[:defaults]
26
+
27
+ template('feature.erb', "config/features/#{@name}_feature.rb")
28
+ end
29
+
30
+ private
31
+
32
+ # Prepends enabled:boolean unless --no-enabled or already specified
33
+ def prepend_enabled_attribute(attrs)
34
+ return attrs unless options[:enabled]
35
+ return attrs if attrs.any? { |attr| attr.name == 'enabled' }
36
+
37
+ enabled_attr = Rails::Generators::GeneratedAttribute.parse('enabled:boolean')
38
+ [enabled_attr] + attrs
39
+ end
40
+
41
+ # Renders an ActiveModel attribute declaration line
42
+ def attribute_declaration(attr)
43
+ type = activemodel_type(attr)
44
+ default = @defaults[attr.name]
45
+ parts = ["attribute :#{attr.name}, :#{type}"]
46
+ parts << "default: #{format_default(type, default)}" if default
47
+ parts.join(', ')
48
+ end
49
+
50
+ def activemodel_type(attr)
51
+ case attr.type.to_s
52
+ when 'boolean' then 'boolean'
53
+ when 'integer' then 'integer'
54
+ when 'float' then 'float'
55
+ else 'string'
56
+ end
57
+ end
19
58
 
20
- template('feature.erb', feature_path)
59
+ # Formats a default value as valid Ruby for the attribute declaration
60
+ def format_default(type, value)
61
+ case type
62
+ when 'boolean'
63
+ value.to_s == 'true' ? 'true' : 'false'
64
+ when 'integer'
65
+ value.to_s.to_i.to_s
66
+ when 'float'
67
+ value.to_s.to_f.to_s
68
+ else
69
+ "'#{value}'"
70
+ end
21
71
  end
22
72
  end
23
73
  end
@@ -1,26 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_model'
4
+
3
5
  # Manages features of the application
4
6
  module Features
5
- # Settings for the feature <%= @klass_name %>
6
- class <%= @klass_name %>Feature
7
- attr_reader <%= @attributes.collect(&:name).collect{ |attr| ":#{attr}" }.join(', ') %>
7
+ <%- @namespace_parts.each_with_index do |ns, i| -%>
8
+ <%= ' ' * (i + 1) %>module <%= ns %>
9
+ <%- end -%>
10
+ <%= ' ' * (@namespace_parts.size + 1) %># Settings for the feature <%= @feature_class_name %>
11
+ <%= ' ' * (@namespace_parts.size + 1) %>class <%= @feature_class_name %>
12
+ <%= ' ' * (@namespace_parts.size + 2) %>include ActiveModel::Attributes
13
+ <%= ' ' * (@namespace_parts.size + 2) %>include ActiveModel::AttributeAssignment
14
+
15
+ <%- @attributes.each do |attribute| -%>
16
+ <%= ' ' * (@namespace_parts.size + 2) %><%= attribute_declaration(attribute) %>
17
+ <%- end -%>
8
18
 
9
- # Constructor for the feature settings
10
- <%- @attributes.each do |attribute| -%>
11
- # @param <%= attribute.name %> [<%= attribute.type %>] setting of the feature
12
- <%- end -%>
13
- def initialize(<%= @attributes.collect(&:name).collect{ |attr| "#{attr}:" }.join(', ') %>)
14
- <%- @attributes.each do |attribute| -%>
15
- @<%= attribute.name %> = <%= attribute.name %>
16
- <%- end -%>
17
- end
19
+ <%= ' ' * (@namespace_parts.size + 2) %># @param attrs [Hash] attribute values to initialize with
20
+ <%= ' ' * (@namespace_parts.size + 2) %>def initialize(**attrs)
21
+ <%= ' ' * (@namespace_parts.size + 3) %>super()
22
+ <%= ' ' * (@namespace_parts.size + 3) %>assign_attributes(attrs) unless attrs.empty?
23
+ <%= ' ' * (@namespace_parts.size + 2) %>end
24
+ <%- @boolean_attributes.each do |attribute| -%>
18
25
 
19
- <%- @boolean_attributes.each do |attribute| -%>
20
- # ?-method for the attribute <%= attribute.name %>
21
- def <%= attribute.name %>?
22
- <%= attribute.name %>
23
- end
24
- <%- end -%>
25
- end
26
- end
26
+ <%= ' ' * (@namespace_parts.size + 2) %># ?-method for the attribute <%= attribute.name %>
27
+ <%= ' ' * (@namespace_parts.size + 2) %>def <%= attribute.name %>?
28
+ <%= ' ' * (@namespace_parts.size + 3) %><%= attribute.name %>
29
+ <%= ' ' * (@namespace_parts.size + 2) %>end
30
+ <%- end -%>
31
+ <%= ' ' * (@namespace_parts.size + 1) %>end
32
+ <%- @namespace_parts.size.downto(1) do |i| -%>
33
+ <%= ' ' * i %>end
34
+ <%- end -%>
35
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rails/generators'
2
4
 
3
5
  module RailsFeatureFlip
@@ -11,7 +13,7 @@ module RailsFeatureFlip
11
13
 
12
14
  # Creates RAILS_ROOT/config/features folder unless it exists
13
15
  def setup_features_dir
14
- Dir.mkdir(Rails.root.join('config/feature')) unless Dir.exist?(Rails.root.join('config/feature'))
16
+ empty_directory 'config/features'
15
17
  end
16
18
 
17
19
  # Copy default loading file to autoload all features inside the features folder
@@ -21,20 +23,12 @@ module RailsFeatureFlip
21
23
 
22
24
  def insert_feature_load_statement
23
25
  application_rb = 'config/application.rb'
24
- content = File.read(application_rb)
25
- unless content.include?('require_relative \'features/all\'')
26
- insert_into_file application_rb, after: /^Bundler.require.*$/ do
27
- <<-RUBY.strip_heredoc
28
-
29
-
30
- # Automatically load all feature configs - you can load the features manually one by one if you want to.
31
- # Be sure to comment out require_relative 'features/all' when doing so.
32
- # example:
33
- # require_relative 'features/foo'
34
- # require_relative 'feature/bar'
35
- require_relative 'features/all'
36
- RUBY
37
- end
26
+ application_rb_path = File.join(destination_root, application_rb)
27
+ return display_post_install_message unless File.exist?(application_rb_path)
28
+
29
+ content = File.read(application_rb_path)
30
+ unless content.include?("require_relative 'features/all'")
31
+ insert_into_file application_rb, feature_load_snippet, after: /^Bundler.require.*$/
38
32
  end
39
33
 
40
34
  display_post_install_message
@@ -42,10 +36,23 @@ module RailsFeatureFlip
42
36
 
43
37
  private
44
38
 
39
+ def feature_load_snippet
40
+ <<~RUBY
41
+
42
+
43
+ # Automatically load all feature configs - you can load the features manually one by one if you want to.
44
+ # Be sure to comment out require_relative 'features/all' when doing so.
45
+ # example:
46
+ # require_relative 'features/foo'
47
+ # require_relative 'feature/bar'
48
+ require_relative 'features/all'
49
+ RUBY
50
+ end
51
+
45
52
  def display_post_install_message
46
- say "RailsFeatureFlip has been successfully installed!"
47
- say "You can now generate feature setting by running the 'rails_feature_flag:feature' generator."
48
- say "Run 'bin/rails generate rails_feature_flag:feature -h' for usage details."
53
+ say 'RailsFeatureFlip has been successfully installed!'
54
+ say "You can now generate feature setting by running the 'rails_feature_flip:feature' generator."
55
+ say "Run 'bin/rails generate rails_feature_flip:feature -h' for usage details."
49
56
  end
50
57
  end
51
58
  end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Load all features from config/features
4
- Dir.entries('config/features').reject { |f| File.directory?(f) }.sort.each do |feature|
4
+ features_dir = File.expand_path(__dir__)
5
+ Dir.entries(features_dir).reject { |f| File.directory?(f) }.sort.each do |feature|
5
6
  next if feature == 'all.rb'
6
7
 
7
8
  require_relative feature.chomp('.rb')
8
9
  rescue LoadError => e
9
10
  puts e.message
10
- end
11
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsFeatureFlip
4
+ # Provides require_feature class method for controllers.
5
+ #
6
+ # @example
7
+ # class HelpCenterController < ApplicationController
8
+ # require_feature :help_center
9
+ # end
10
+ module ControllerMethods
11
+ extend ActiveSupport::Concern
12
+
13
+ class_methods do
14
+ # Adds a before_action that returns 404 unless the feature is enabled.
15
+ #
16
+ # @param name [Symbol, String] the feature name
17
+ # @param options [Hash] options passed to before_action (e.g. only:, except:)
18
+ def require_feature(name, **options)
19
+ before_action(**options) do
20
+ head :not_found unless feature_enabled?(name)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsFeatureFlip
4
+ # Provides feature_enabled? helper for controllers and views.
5
+ #
6
+ # @example In a view
7
+ # <% if feature_enabled?(:help_center) %>
8
+ # <%= render 'help_center_widget' %>
9
+ # <% end %>
10
+ module Helper
11
+ # Checks whether a feature is enabled.
12
+ #
13
+ # @param name [Symbol, String] the feature name
14
+ # @return [Boolean]
15
+ def feature_enabled?(name)
16
+ feature = Rails.configuration.x.features.public_send(name)
17
+ feature.respond_to?(:enabled?) && feature.enabled?
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require_relative 'controller_methods'
5
+
6
+ module RailsFeatureFlip
7
+ # Railtie that auto-includes helpers into controllers and views and provides rake tasks.
8
+ class Railtie < Rails::Railtie
9
+ rake_tasks do
10
+ load 'rails_feature_flip/tasks.rake'
11
+ end
12
+
13
+ initializer 'rails_feature_flip.helpers' do
14
+ ActiveSupport.on_load(:action_controller_base) do
15
+ include RailsFeatureFlip::Helper
16
+ include RailsFeatureFlip::ControllerMethods
17
+ end
18
+
19
+ ActiveSupport.on_load(:action_view) do
20
+ include RailsFeatureFlip::Helper
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsFeatureFlip
4
+ # Provides a clean API to query all registered features.
5
+ #
6
+ # Features are stored in Rails.configuration.x.features (an OrderedOptions hash).
7
+ # This module wraps that hash with convenience methods.
8
+ #
9
+ # @example
10
+ # RailsFeatureFlip::Registry.all # => { chat: #<Features::ChatFeature>, ... }
11
+ # RailsFeatureFlip::Registry.names # => [:chat, :billing]
12
+ # RailsFeatureFlip::Registry.find(:chat) # => #<Features::ChatFeature>
13
+ module Registry
14
+ # Returns all registered features as a hash.
15
+ #
16
+ # @return [Hash{Symbol => Object}]
17
+ def self.all
18
+ features_store.to_h
19
+ end
20
+
21
+ # Returns the names of all registered features.
22
+ #
23
+ # @return [Array<Symbol>]
24
+ def self.names
25
+ features_store.keys
26
+ end
27
+
28
+ # Finds a feature by name.
29
+ #
30
+ # @param name [Symbol, String] the feature name
31
+ # @return [Object] the feature instance
32
+ # @raise [RailsFeatureFlip::Error] if the feature is not registered
33
+ def self.find(name)
34
+ feature = features_store[name.to_sym]
35
+ raise RailsFeatureFlip::Error, "Feature '#{name}' is not registered" if feature.nil?
36
+
37
+ feature
38
+ end
39
+
40
+ def self.features_store
41
+ Rails.configuration.x.features
42
+ end
43
+ private_class_method :features_store
44
+ end
45
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :features do
4
+ desc 'List all registered features and their current values'
5
+ task list: :environment do
6
+ features = RailsFeatureFlip::Registry.all
7
+
8
+ if features.empty?
9
+ puts 'No features registered.'
10
+ next
11
+ end
12
+
13
+ rows = features.map do |name, feature|
14
+ enabled = feature.respond_to?(:enabled?) ? feature.enabled?.to_s : 'n/a'
15
+ attrs = feature_attributes(feature)
16
+ [name.to_s, feature.class.name, enabled, attrs]
17
+ end
18
+
19
+ print_feature_table(%w[Feature Class Enabled Attributes], rows)
20
+ end
21
+ end
22
+
23
+ desc 'List all registered features (alias for features:list)'
24
+ task features: 'features:list'
25
+
26
+ def feature_attributes(feature)
27
+ if feature.respond_to?(:attributes)
28
+ feature.attributes.except('enabled').map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
29
+ else
30
+ ivars = feature.instance_variables - [:@enabled]
31
+ ivars.map { |ivar| "#{ivar.to_s.delete_prefix('@')}: #{feature.instance_variable_get(ivar).inspect}" }.join(', ')
32
+ end
33
+ end
34
+
35
+ def print_feature_table(headers, rows)
36
+ widths = column_widths(headers, rows)
37
+ template = widths.map { |w| "%-#{w}s" }.join(' ')
38
+
39
+ puts format(template, *headers)
40
+ puts widths.map { |w| '-' * w }.join(' ')
41
+ rows.each { |row| puts format(template, *row) }
42
+ end
43
+
44
+ def column_widths(headers, rows)
45
+ headers.each_with_index.map do |header, i|
46
+ [header.length, *rows.map { |r| r[i].to_s.length }].max
47
+ end
48
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsFeatureFlip
4
+ # Test helper for temporarily overriding feature values in tests.
5
+ #
6
+ # @example Include in your test class
7
+ # class MyTest < ActiveSupport::TestCase
8
+ # include RailsFeatureFlip::TestHelper
9
+ #
10
+ # test "shows widget when enabled" do
11
+ # with_feature(:help_center, enabled: true) do
12
+ # # test code that checks feature behavior
13
+ # end
14
+ # end
15
+ # end
16
+ module TestHelper
17
+ # Temporarily overrides attributes on a feature and restores them after the block.
18
+ #
19
+ # @param name [Symbol, String] the feature name
20
+ # @param overrides [Hash] attribute values to set (e.g. enabled: true)
21
+ # @yield the block to execute with the overridden feature values
22
+ def with_feature(name, **overrides)
23
+ feature = Rails.configuration.x.features.public_send(name)
24
+ originals = save_feature_state(feature, overrides)
25
+ yield
26
+ ensure
27
+ restore_feature_state(feature, originals)
28
+ end
29
+
30
+ private
31
+
32
+ def save_feature_state(feature, overrides)
33
+ overrides.each_with_object({}) do |(attr, value), originals|
34
+ originals[attr] = feature.public_send(attr)
35
+ feature.public_send(:"#{attr}=", value)
36
+ end
37
+ end
38
+
39
+ def restore_feature_state(feature, originals)
40
+ originals&.each do |attr, value|
41
+ feature.public_send(:"#{attr}=", value)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsFeatureFlip
4
- VERSION = "0.1.1"
4
+ VERSION = '1.0.0'
5
5
  end
@@ -1,7 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'rails_feature_flip/version'
4
+ require_relative 'rails_feature_flip/registry'
5
+ require_relative 'rails_feature_flip/railtie' if defined?(Rails::Railtie)
4
6
 
5
7
  module RailsFeatureFlip
6
8
  class Error < StandardError; end
9
+
10
+ # Enables a feature at runtime (in-memory, resets on restart).
11
+ #
12
+ # @param name [Symbol, String] the feature name
13
+ def self.enable(name)
14
+ Registry.find(name).enabled = true
15
+ end
16
+
17
+ # Disables a feature at runtime (in-memory, resets on restart).
18
+ #
19
+ # @param name [Symbol, String] the feature name
20
+ def self.disable(name)
21
+ Registry.find(name).enabled = false
22
+ end
23
+
24
+ # Toggles a feature at runtime (in-memory, resets on restart).
25
+ #
26
+ # @param name [Symbol, String] the feature name
27
+ # @return [Boolean] the new enabled state
28
+ def self.toggle(name)
29
+ feature = Registry.find(name)
30
+ feature.enabled = !feature.enabled?
31
+ end
7
32
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-feature-flip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Deutscher
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-02-23 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: rails
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '5.0'
18
+ version: '7.0'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '5.0'
25
+ version: '7.0'
27
26
  description: A more flexible way to manage settings and features in you rails app.
28
27
  email:
29
28
  - ben@bdeutscher.org
@@ -31,6 +30,7 @@ executables: []
31
30
  extensions: []
32
31
  extra_rdoc_files: []
33
32
  files:
33
+ - ".rubocop.yml"
34
34
  - CHANGELOG.md
35
35
  - CODE_OF_CONDUCT.md
36
36
  - LICENSE.txt
@@ -43,8 +43,13 @@ files:
43
43
  - lib/generators/rails_feature_flip/install/templates/all.rb
44
44
  - lib/generators/rails_feature_flip/install/templates/initializer.rb
45
45
  - lib/rails_feature_flip.rb
46
+ - lib/rails_feature_flip/controller_methods.rb
47
+ - lib/rails_feature_flip/helper.rb
48
+ - lib/rails_feature_flip/railtie.rb
49
+ - lib/rails_feature_flip/registry.rb
50
+ - lib/rails_feature_flip/tasks.rake
51
+ - lib/rails_feature_flip/test_helper.rb
46
52
  - lib/rails_feature_flip/version.rb
47
- - rails-feature-flip.gemspec
48
53
  homepage: https://github.com/its-bede/rails-feature-flip
49
54
  licenses:
50
55
  - MIT
@@ -52,7 +57,7 @@ metadata:
52
57
  homepage_uri: https://github.com/its-bede/rails-feature-flip
53
58
  source_code_uri: https://github.com/its-bede/rails-feature-flip
54
59
  changelog_uri: https://github.com/its-bede/rails-feature-flip/blob/main/CHANGELOG.md
55
- post_install_message:
60
+ rubygems_mfa_required: 'true'
56
61
  rdoc_options: []
57
62
  require_paths:
58
63
  - lib
@@ -60,15 +65,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
60
65
  requirements:
61
66
  - - ">="
62
67
  - !ruby/object:Gem::Version
63
- version: 2.6.0
68
+ version: '3.0'
64
69
  required_rubygems_version: !ruby/object:Gem::Requirement
65
70
  requirements:
66
71
  - - ">="
67
72
  - !ruby/object:Gem::Version
68
73
  version: '0'
69
74
  requirements: []
70
- rubygems_version: 3.4.10
71
- signing_key:
75
+ rubygems_version: 4.0.3
72
76
  specification_version: 4
73
77
  summary: Feature flipping for rails apps.
74
78
  test_files: []
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/rails_feature_flip/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "rails-feature-flip"
7
- spec.version = RailsFeatureFlip::VERSION
8
- spec.authors = ["Benjamin Deutscher"]
9
- spec.email = ["ben@bdeutscher.org"]
10
-
11
- spec.summary = "Feature flipping for rails apps."
12
- spec.description = "A more flexible way to manage settings and features in you rails app."
13
- spec.homepage = "https://github.com/its-bede/rails-feature-flip"
14
- spec.license = "MIT"
15
- spec.required_ruby_version = ">= 2.6.0"
16
-
17
- # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
18
-
19
- spec.metadata["homepage_uri"] = spec.homepage
20
- spec.metadata["source_code_uri"] = "https://github.com/its-bede/rails-feature-flip"
21
- spec.metadata["changelog_uri"] = "https://github.com/its-bede/rails-feature-flip/blob/main/CHANGELOG.md"
22
-
23
- # Specify which files should be added to the gem when it is released.
24
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
- spec.files = Dir.chdir(__dir__) do
26
- `git ls-files -z`.split("\x0").reject do |f|
27
- (File.expand_path(f) == __FILE__) ||
28
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
29
- end
30
- end
31
-
32
- spec.files += Dir["lib/generators/**/*"]
33
-
34
- spec.bindir = "exe"
35
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
36
- spec.require_paths = ["lib"]
37
-
38
- # Uncomment to register a new dependency of your gem
39
- spec.add_dependency 'rails', '>= 5.0'
40
-
41
- # For more information and examples about making a new gem, check out our
42
- # guide at: https://bundler.io/guides/creating_gem.html
43
- end