rails_feature_guard 0.1.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bdfc76503e1ab6424a9ed5ac61137f96182ec1c92521f7b18e0facffaaa792b1
4
+ data.tar.gz: 44d3aa0e13d1dbccd05a7025323fab483a9ca9f59b0b42e2f562dc49bec90d17
5
+ SHA512:
6
+ metadata.gz: 9e29b6d8620a16053849fc59dd5a20166bd7542cbd2f8f812e665b5368f354f446acbbf46a59da14e5735944ba2edfd4e8d6b4340236b0399c491cc0a71c8da9
7
+ data.tar.gz: 4430ab07f840ba963bbe591458a3c8d9db56da2790edc0073fd1f86a872822bcd26a0f20d62beecd5f1009fd1830fcc367997b110c82ec71c697cb3817232dad
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.2] - 2026-09-16
4
+
5
+ - Publish the gem as `rails_feature_guard`.
6
+ - Add a matching entrypoint for Bundler and Rails.
7
+
8
+ ## [0.1.1] - 2026-09-16
9
+
10
+ - Add MIT licensing and RubyGems metadata.
11
+
12
+ ## [0.1.0] - 2026-08-27
13
+
14
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jam Vito Cruz
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,165 @@
1
+ # FeatureGuard
2
+
3
+ FeatureGuard protects Rails controller actions with feature flags stored on an
4
+ account's settings record. It also exposes the same checks to views.
5
+
6
+ ## Installation
7
+
8
+ Add the gem to the Rails application's `Gemfile`:
9
+
10
+ ```ruby
11
+ gem "rails_feature_guard"
12
+ ```
13
+
14
+ Then install dependencies:
15
+
16
+ ```bash
17
+ bundle install
18
+ ```
19
+
20
+ ## Configuration
21
+
22
+ Create `config/initializers/feature_guard.rb`:
23
+
24
+ ```ruby
25
+ FeatureGuard.configure do |config|
26
+ config.redirect_method = :dashboards_path
27
+ end
28
+ ```
29
+
30
+ The default values are:
31
+
32
+ ```ruby
33
+ redirect_method: :root_path
34
+ ```
35
+
36
+ `redirect_method` is combined with the controller's first namespace. For
37
+ example, `:dashboards_path` in a `Partner` controller becomes
38
+ `partner_dashboards_path`.
39
+
40
+ ## Settings record
41
+
42
+ The account's settings record must have a boolean attribute for each feature:
43
+
44
+ ```ruby
45
+ # current_account.feature_setting
46
+ inventory_enabled # true or false
47
+ reports_enabled # true or false
48
+ ```
49
+
50
+ For example, an account can expose its settings record like this:
51
+
52
+ ```ruby
53
+ class Account < ApplicationRecord
54
+ has_one :feature_setting
55
+ end
56
+ ```
57
+
58
+ ## Protect a controller action
59
+
60
+ Declare a feature in a controller:
61
+
62
+ ```ruby
63
+ class InventoryController < ApplicationController
64
+ feature :inventory_enabled
65
+
66
+ def index
67
+ end
68
+ end
69
+ ```
70
+
71
+ The feature name must exactly match a boolean column on
72
+ `current_account.feature_setting`. `feature :inventory_enabled` calls
73
+ `inventory_enabled?` on the feature policy. When that method is not defined,
74
+ the policy reads `inventory_enabled?` from the settings record. If that column
75
+ is missing, FeatureGuard raises `FeatureGuard::MissingFeatureColumnError`.
76
+ FeatureGuard adds a `before_action`:
77
+
78
+ ```text
79
+ inventory_enabled is true -> the action runs
80
+ inventory_enabled is false -> the request redirects
81
+ ```
82
+
83
+ Limit a feature check to specific actions with normal `before_action` options:
84
+
85
+ ```ruby
86
+ class InventoryController < ApplicationController
87
+ feature :inventory_enabled, only: :index
88
+ end
89
+ ```
90
+
91
+ For example, `feature :inventory_access_enabled` requires an
92
+ `inventory_access_enabled` settings column and uses
93
+ `inventory_access_enabled?` on both the policy and settings record.
94
+
95
+ ## Feature policy
96
+
97
+ Create a policy in the Rails application and have the host controller provide
98
+ one instance per request. Include `FeatureGuard::Policy`; the module provides
99
+ the common feature-resolution behavior.
100
+
101
+ ```ruby
102
+ # app/policies/feature_policy.rb
103
+ class FeaturePolicy
104
+ include FeatureGuard::Policy
105
+
106
+ def inventory_enabled?
107
+ current_user.admin?
108
+ end
109
+ end
110
+ ```
111
+
112
+ ```ruby
113
+ # app/controllers/application_controller.rb
114
+ class ApplicationController < ActionController::Base
115
+ private
116
+
117
+ def feature_guard_policy
118
+ @feature_guard_policy ||= FeaturePolicy.new(
119
+ current_user: current_user,
120
+ feature_setting: current_account&.feature_setting
121
+ )
122
+ end
123
+ end
124
+ ```
125
+
126
+ The policy receives the account's `feature_setting` and `current_user`. A
127
+ defined policy method fully controls its declared feature. When it is absent,
128
+ `FeatureGuard::Policy#method_missing` reads the identically named setting
129
+ predicate. Policy and settings names must match exactly.
130
+
131
+ Define the private `#feature_guard_policy` instance method on the base
132
+ controller so all controllers can build a request policy. For namespaced
133
+ controllers, define it on that namespace's base controller. If it is missing,
134
+ the gem raises `FeatureGuard::MissingPolicyError`.
135
+
136
+ To use a differently named factory method, select it with the class-level
137
+ `feature_guard_policy_method` declaration:
138
+
139
+ ```ruby
140
+ feature_guard_policy_method :my_custom_feature_policy
141
+ ```
142
+
143
+ ## Use a feature in a view
144
+
145
+ `module_enabled?` is available in controller actions and views:
146
+
147
+ ```erb
148
+ <% if module_enabled?(:inventory_enabled) %>
149
+ <%= link_to "Inventory", inventory_path %>
150
+ <% end %>
151
+ ```
152
+
153
+ You may use `module_enabled?` without declaring `feature :inventory_enabled` in the
154
+ controller. Declaring `feature` is only required when you want FeatureGuard to
155
+ add the automatic redirect callback.
156
+
157
+ ## Errors and missing settings
158
+
159
+ - If the account has no settings record, `module_enabled?` returns `false`.
160
+ - If the settings record lacks the expected feature column, FeatureGuard raises
161
+ `FeatureGuard::MissingFeatureColumnError`.
162
+ - If the base controller does not implement `feature_guard_policy`, FeatureGuard
163
+ raises `FeatureGuard::MissingPolicyError`.
164
+ - If the namespaced dashboard route does not exist, FeatureGuard raises
165
+ `FeatureGuard::MissingRouteError`.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FeatureGuard
4
+ class Configuration
5
+ attr_accessor :redirect_method
6
+
7
+ def initialize
8
+ # Combined with the controller namespace to build the redirect helper.
9
+ @redirect_method = :root_path
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FeatureGuard
4
+ # Mixed into ActionController::Base by the Railtie. It provides the `feature`
5
+ # class method for declaring protected features and `module_enabled?` for
6
+ # checking those features in controller actions and views.
7
+ module Controller
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ class_attribute :policy_name, default: :feature_guard_policy
12
+
13
+ helper_method :module_enabled?
14
+ end
15
+
16
+ # Declares a feature that is protected by the persisted feature setting. The
17
+ # feature is checked before each action, and the user is redirected to the
18
+ # configured path if the feature is disabled.
19
+ class_methods do
20
+ # Selects the controller instance method that builds the request policy.
21
+ # Without this declaration, FeatureGuard calls `#feature_guard_policy`.
22
+ def feature_guard_policy_method(method_name)
23
+ self.policy_name = method_name.to_sym
24
+ end
25
+
26
+ def feature(feature_name, **options)
27
+ feature_name = feature_name.to_sym
28
+
29
+ if feature_name.to_s.end_with?('?')
30
+ raise ArgumentError,
31
+ 'Feature name must not end with a question mark. Use :inventory_enable instead of :inventory_enable?.'
32
+ end
33
+
34
+ before_action(**options) do
35
+ enforce_module_availability(feature_name)
36
+ end
37
+ end
38
+ end
39
+
40
+ # Returns whether a feature is enabled for the current account. Results are
41
+ # cached for the current controller instance, which lasts one request.
42
+ def module_enabled?(feature_name)
43
+ feature_name = feature_name.to_sym
44
+
45
+ @module_availability_cache ||= {}
46
+
47
+ return @module_availability_cache[feature_name] if @module_availability_cache.key?(feature_name)
48
+
49
+ @module_availability_cache[feature_name] = resolve_module(feature_name)
50
+ end
51
+
52
+ private
53
+
54
+ # Redirects instead of continuing to the action when the feature is off.
55
+ def enforce_module_availability(module_name)
56
+ return if module_enabled?(module_name)
57
+
58
+ redirect_to(
59
+ feature_guard_redirect_path,
60
+ alert: 'This feature is not available for your account.'
61
+ )
62
+ end
63
+
64
+ # Asks the controller-owned application policy whether the feature is enabled.
65
+ def resolve_module(feature_name)
66
+ policy = feature_guard_policy_instance
67
+
68
+ return false unless policy
69
+
70
+ policy.public_send(:"#{feature_name}?")
71
+ end
72
+
73
+ # Calls the policy factory selected by `feature_guard_policy`. Policy
74
+ # factories may be private methods on the application's base controller.
75
+ def feature_guard_policy_instance
76
+ policy_method = self.class.policy_name
77
+
78
+ return feature_guard_policy if policy_method == :feature_guard_policy
79
+
80
+ send(policy_method)
81
+ end
82
+
83
+ # Host controllers must provide the policy instance for the request
84
+ def feature_guard_policy
85
+ namespace = self.class.name.split('::').first
86
+ base_name = namespace.empty? ? 'ApplicationController' : "#{namespace}Controller"
87
+
88
+ raise MissingPolicyError,
89
+ "Define #feature_guard_policy in #{base_name}, or configure a custom policy " \
90
+ 'with feature_guard_policy_method'
91
+ end
92
+
93
+ def feature_guard_redirect_path
94
+ namespace = controller_path.split('/').first
95
+ dashboard_path = :"#{namespace}_#{FeatureGuard.configuration.redirect_method}"
96
+
97
+ return public_send(dashboard_path) if respond_to?(dashboard_path)
98
+
99
+ raise MissingRouteError,
100
+ "Expected route helper #{dashboard_path} for #{self.class.name}"
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FeatureGuard
4
+ module Policy
5
+ # `feature_setting` is used for declared feature methods that the policy
6
+ # does not override directly.
7
+ def initialize(current_user:, feature_setting:)
8
+ @current_user = current_user
9
+ @feature_setting = feature_setting
10
+ end
11
+
12
+ # A missing policy predicate falls back to the identically named persisted
13
+ # setting predicate. Defined policy methods fully override that setting.
14
+ def method_missing(method_name, ...)
15
+ return super unless method_name.to_s.end_with?('?')
16
+
17
+ persisted_feature_enabled?(method_name)
18
+ end
19
+
20
+ def respond_to_missing?(method_name, include_private = false)
21
+ return super unless method_name.to_s.end_with?('?')
22
+
23
+ @feature_setting.respond_to?(method_name, include_private) || super
24
+ end
25
+
26
+ private
27
+
28
+ def persisted_feature_enabled?(method_name)
29
+ return false unless @feature_setting
30
+
31
+ unless @feature_setting.respond_to?(method_name)
32
+ raise MissingFeatureColumnError,
33
+ "Missing setting column #{method_name.to_s.delete_suffix('?')} for #{@feature_setting.class.name}."
34
+ end
35
+
36
+ @feature_setting.public_send(method_name)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module FeatureGuard
6
+ class Railtie < Rails::Railtie
7
+ initializer 'feature_guard.controller' do
8
+ ActiveSupport.on_load(:action_controller_base) do
9
+ # Makes FeatureGuard available to every controller through inheritance.
10
+ # Applications can override methods such as `feature_guard_policy` in
11
+ # their base controller to provide request-specific behavior.
12
+ include FeatureGuard::Controller
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FeatureGuard
4
+ VERSION = '0.1.2'
5
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # `FeatureGuard::Controller` is an ActiveSupport concern and uses
4
+ # `class_attribute` to keep each controller's feature declarations.
5
+ require 'active_support/concern'
6
+ require 'active_support/core_ext/class/attribute'
7
+ require 'active_support/core_ext/string/inflections'
8
+
9
+ require_relative 'feature_guard/version'
10
+ require_relative 'feature_guard/configuration'
11
+ require_relative 'feature_guard/policy'
12
+ require_relative 'feature_guard/controller'
13
+ require_relative 'feature_guard/railtie'
14
+
15
+ module FeatureGuard
16
+ # Custom error classes for the gem
17
+ class MissingFeatureColumnError < StandardError; end
18
+ class MissingRouteError < StandardError; end
19
+ class MissingPolicyError < StandardError; end
20
+
21
+ class << self
22
+ attr_writer :configuration
23
+
24
+ # Returns one shared configuration object for the application. The object
25
+ # is created only on first use, then kept in memory for later calls.
26
+ def configuration
27
+ @configuration ||= Configuration.new
28
+ end
29
+
30
+ # Yields the shared configuration object so an initializer can customize
31
+ # the application policy and redirect path.
32
+ def configure
33
+ yield(configuration)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'feature_guard'
@@ -0,0 +1,4 @@
1
+ module FeatureGuard
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_feature_guard
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Jam Vito Cruz
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: actionpack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '7.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
32
+ - !ruby/object:Gem::Dependency
33
+ name: activesupport
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '7.1'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '9'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '7.1'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '9'
52
+ - !ruby/object:Gem::Dependency
53
+ name: railties
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '7.1'
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '9'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '7.1'
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '9'
72
+ description: Guards Rails features with account settings and optional policy overrides.
73
+ email:
74
+ - vitocruzjemmalyn@gmail.com
75
+ executables: []
76
+ extensions: []
77
+ extra_rdoc_files: []
78
+ files:
79
+ - CHANGELOG.md
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - lib/feature_guard.rb
84
+ - lib/feature_guard/configuration.rb
85
+ - lib/feature_guard/controller.rb
86
+ - lib/feature_guard/policy.rb
87
+ - lib/feature_guard/railtie.rb
88
+ - lib/feature_guard/version.rb
89
+ - lib/rails_feature_guard.rb
90
+ - sig/feature_guard.rbs
91
+ homepage: https://github.com/jamvitocruz/feature_guard
92
+ licenses:
93
+ - MIT
94
+ metadata:
95
+ homepage_uri: https://github.com/jamvitocruz/feature_guard
96
+ source_code_uri: https://github.com/jamvitocruz/feature_guard/tree/main
97
+ changelog_uri: https://github.com/jamvitocruz/feature_guard/blob/main/CHANGELOG.md
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: 3.2.0
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubygems_version: 4.0.9
113
+ specification_version: 4
114
+ summary: Feature flags for Rails controllers and views.
115
+ test_files: []