durable_parameters 0.2.3

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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +853 -0
  4. data/Rakefile +29 -0
  5. data/app/params/account_params.rb.example +38 -0
  6. data/app/params/application_params.rb +16 -0
  7. data/lib/durable_parameters/adapters/hanami.rb +138 -0
  8. data/lib/durable_parameters/adapters/rage.rb +124 -0
  9. data/lib/durable_parameters/adapters/rails.rb +280 -0
  10. data/lib/durable_parameters/adapters/sinatra.rb +91 -0
  11. data/lib/durable_parameters/core/application_params.rb +334 -0
  12. data/lib/durable_parameters/core/configuration.rb +83 -0
  13. data/lib/durable_parameters/core/forbidden_attributes_protection.rb +48 -0
  14. data/lib/durable_parameters/core/parameters.rb +643 -0
  15. data/lib/durable_parameters/core/params_registry.rb +110 -0
  16. data/lib/durable_parameters/core.rb +15 -0
  17. data/lib/durable_parameters/log_subscriber.rb +34 -0
  18. data/lib/durable_parameters/railtie.rb +65 -0
  19. data/lib/durable_parameters/version.rb +7 -0
  20. data/lib/durable_parameters.rb +41 -0
  21. data/lib/generators/rails/USAGE +12 -0
  22. data/lib/generators/rails/durable_parameters_controller_generator.rb +17 -0
  23. data/lib/generators/rails/templates/controller.rb +94 -0
  24. data/lib/legacy/action_controller/application_params.rb +235 -0
  25. data/lib/legacy/action_controller/parameters.rb +524 -0
  26. data/lib/legacy/action_controller/params_registry.rb +108 -0
  27. data/lib/legacy/active_model/forbidden_attributes_protection.rb +40 -0
  28. data/test/action_controller_required_params_test.rb +36 -0
  29. data/test/action_controller_tainted_params_test.rb +29 -0
  30. data/test/active_model_mass_assignment_taint_protection_test.rb +25 -0
  31. data/test/application_params_array_test.rb +245 -0
  32. data/test/application_params_edge_cases_test.rb +361 -0
  33. data/test/application_params_test.rb +893 -0
  34. data/test/controller_generator_test.rb +31 -0
  35. data/test/core_parameters_test.rb +2376 -0
  36. data/test/durable_parameters_test.rb +115 -0
  37. data/test/enhanced_error_messages_test.rb +120 -0
  38. data/test/gemfiles/Gemfile.rails-3.0.x +14 -0
  39. data/test/gemfiles/Gemfile.rails-3.1.x +14 -0
  40. data/test/gemfiles/Gemfile.rails-3.2.x +14 -0
  41. data/test/log_on_unpermitted_params_test.rb +49 -0
  42. data/test/metadata_validation_test.rb +294 -0
  43. data/test/multi_parameter_attributes_test.rb +38 -0
  44. data/test/parameters_core_methods_test.rb +503 -0
  45. data/test/parameters_integration_test.rb +553 -0
  46. data/test/parameters_permit_test.rb +491 -0
  47. data/test/parameters_require_test.rb +9 -0
  48. data/test/parameters_taint_test.rb +98 -0
  49. data/test/params_registry_concurrency_test.rb +422 -0
  50. data/test/params_registry_test.rb +112 -0
  51. data/test/permit_by_model_test.rb +227 -0
  52. data/test/raise_on_unpermitted_params_test.rb +32 -0
  53. data/test/test_helper.rb +38 -0
  54. data/test/transform_params_edge_cases_test.rb +526 -0
  55. data/test/transformation_test.rb +360 -0
  56. metadata +223 -0
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrongParameters
4
+ module Core
5
+ # Singleton registry for storing and retrieving param class definitions.
6
+ #
7
+ # ParamsRegistry provides a central location to register and look up params
8
+ # classes for models. This enables automatic inference of params classes in
9
+ # transform_params based on the model name.
10
+ #
11
+ # @example Registering a params class
12
+ # StrongParameters::Core::ParamsRegistry.register(:user, UserParams)
13
+ #
14
+ # @example Looking up a params class
15
+ # StrongParameters::Core::ParamsRegistry.lookup(:user) # => UserParams
16
+ #
17
+ # @example Getting permitted attributes
18
+ # StrongParameters::Core::ParamsRegistry.permitted_attributes_for(:user, action: :create)
19
+ class ParamsRegistry
20
+ class << self
21
+ # Register a params class for a model.
22
+ #
23
+ # The model name is normalized (underscored and symbolized) before storage.
24
+ #
25
+ # @param model_name [String, Symbol] the model name (e.g., 'User', 'Account')
26
+ # @param params_class [Class] the params class (e.g., UserParams)
27
+ # @return [Class] the registered params class
28
+ # @example
29
+ # ParamsRegistry.register(:user, UserParams)
30
+ # ParamsRegistry.register('BlogPost', BlogPostParams)
31
+ def register(model_name, params_class)
32
+ registry[normalize_key(model_name)] = params_class
33
+ end
34
+
35
+ # Look up the params class for a model.
36
+ #
37
+ # @param model_name [String, Symbol] the model name
38
+ # @return [Class, nil] the params class or nil if not found
39
+ # @example
40
+ # ParamsRegistry.lookup(:user) # => UserParams
41
+ # ParamsRegistry.lookup(:unknown) # => nil
42
+ def lookup(model_name)
43
+ registry[normalize_key(model_name)]
44
+ end
45
+
46
+ # Get permitted attributes for a model.
47
+ #
48
+ # @param model_name [String, Symbol] the model name
49
+ # @param action [Symbol, String, nil] optional action name for filtering
50
+ # @return [Array<Symbol, Hash>] array of permitted attributes
51
+ # @example
52
+ # ParamsRegistry.permitted_attributes_for(:user)
53
+ # ParamsRegistry.permitted_attributes_for(:post, action: :create)
54
+ def permitted_attributes_for(model_name, action: nil)
55
+ params_class = lookup(model_name)
56
+ return [] unless params_class
57
+
58
+ params_class.permitted_attributes(action: action)
59
+ end
60
+
61
+ # Check if a model has registered params.
62
+ #
63
+ # @param model_name [String, Symbol] the model name
64
+ # @return [Boolean] true if registered, false otherwise
65
+ # @example
66
+ # ParamsRegistry.registered?(:user) # => true
67
+ # ParamsRegistry.registered?(:unknown) # => false
68
+ def registered?(model_name)
69
+ registry.key?(normalize_key(model_name))
70
+ end
71
+
72
+ # Clear the registry.
73
+ #
74
+ # This is primarily useful for testing to ensure a clean slate between tests.
75
+ #
76
+ # @return [Hash] the empty registry
77
+ def clear!
78
+ registry.clear
79
+ end
80
+
81
+ # Get all registered model names.
82
+ #
83
+ # @return [Array<String>] array of model names as strings
84
+ # @example
85
+ # ParamsRegistry.registered_models # => ["user", "post", "comment"]
86
+ def registered_models
87
+ registry.keys.map(&:to_s)
88
+ end
89
+
90
+ private
91
+
92
+ # @return [Hash<Symbol, Class>] the internal registry hash
93
+ def registry
94
+ @registry ||= {}
95
+ end
96
+
97
+ # Normalize a key by underscoring and symbolizing it.
98
+ #
99
+ # @param key [String, Symbol] the key to normalize
100
+ # @return [Symbol] the normalized key
101
+ # @example
102
+ # normalize_key('BlogPost') # => :blog_post
103
+ # normalize_key(:user) # => :user
104
+ def normalize_key(key)
105
+ key.to_s.gsub(/([A-Z])/) { "_#{$1.downcase}" }.gsub(/^_/, '').to_sym
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Core framework-agnostic strong parameters implementation
4
+ #
5
+ # The core module provides all essential functionality without any framework dependencies:
6
+ # - Parameters: Hash-based parameter filtering and whitelisting
7
+ # - ApplicationParams: Declarative DSL for defining parameter permissions
8
+ # - ParamsRegistry: Centralized registry for params classes
9
+ # - ForbiddenAttributesProtection: Mass assignment protection mixin
10
+ # - Configuration: Centralized configuration management
11
+ require 'durable_parameters/core/configuration'
12
+ require 'durable_parameters/core/parameters'
13
+ require 'durable_parameters/core/application_params'
14
+ require 'durable_parameters/core/params_registry'
15
+ require 'durable_parameters/core/forbidden_attributes_protection'
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/log_subscriber'
4
+ require 'active_support/notifications'
5
+
6
+ module StrongParameters
7
+ # Log subscriber for unpermitted parameters notifications.
8
+ #
9
+ # This subscriber listens for unpermitted parameter events and logs them
10
+ # using the configured logger. This is helpful for development and testing
11
+ # to identify parameters that need to be explicitly permitted.
12
+ class LogSubscriber < ActiveSupport::LogSubscriber
13
+ # Handle unpermitted_parameters notification event.
14
+ #
15
+ # @param event [ActiveSupport::Notifications::Event] the notification event
16
+ # @return [void]
17
+ def unpermitted_parameters(event)
18
+ unpermitted_keys = event.payload[:keys]
19
+ debug("Unpermitted parameters: #{unpermitted_keys.join(', ')}")
20
+ end
21
+
22
+ # Returns the logger for this subscriber.
23
+ #
24
+ # @return [Logger] the Action Controller logger
25
+ def logger
26
+ ActionController::Base.logger
27
+ end
28
+ end
29
+ end
30
+
31
+ # Only attach if ActionController is loaded (Rails environment)
32
+ if defined?(ActionController)
33
+ StrongParameters::LogSubscriber.attach_to :action_controller
34
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+ require 'durable_parameters/adapters/rails'
5
+
6
+ module StrongParameters
7
+ # Rails integration for Strong Parameters.
8
+ #
9
+ # This railtie configures Strong Parameters within Rails applications,
10
+ # setting up generators, autoload paths, and parameter logging.
11
+ class Railtie < ::Rails::Railtie
12
+ # Setup Rails adapter
13
+ config.before_initialize do
14
+ StrongParameters::Adapters::Rails.setup!
15
+ end
16
+
17
+ # Configure scaffold generator to use strong_parameters controller template
18
+ if config.respond_to?(:app_generators)
19
+ config.app_generators.scaffold_controller = :strong_parameters_controller
20
+ else
21
+ config.generators.scaffold_controller = :strong_parameters_controller
22
+ end
23
+
24
+ # Configure action on unpermitted parameters (log in dev/test, silent in production)
25
+ initializer 'strong_parameters.config', before: 'action_controller.set_configs' do |app|
26
+ StrongParameters::Adapters::Rails::Parameters.action_on_unpermitted_parameters =
27
+ app.config.action_controller.delete(:action_on_unpermitted_parameters) do
28
+ (Rails.env.test? || Rails.env.development?) ? :log : false
29
+ end
30
+ end
31
+
32
+ # Add app/params directory to autoload paths for params classes
33
+ initializer 'strong_parameters.autoload_params' do |app|
34
+ params_path = app.root.join('app', 'params')
35
+
36
+ # Add to autoload paths if directory exists
37
+ if params_path.directory?
38
+ ActiveSupport::Dependencies.autoload_paths << params_path.to_s
39
+ end
40
+ end
41
+
42
+ # Automatically load and register all params classes after Rails initialization
43
+ config.after_initialize do |app|
44
+ params_path = app.root.join('app', 'params')
45
+
46
+ next unless params_path.directory?
47
+
48
+ # Load all params class files
49
+ Dir[params_path.join('**', '*_params.rb')].each do |file|
50
+ require_dependency file
51
+ end
52
+
53
+ # Register all ApplicationParams subclasses with the registry
54
+ next unless defined?(StrongParameters::Core::ApplicationParams)
55
+
56
+ StrongParameters::Core::ApplicationParams.descendants.each do |params_class|
57
+ # Extract model name from class name (e.g., UserParams -> User)
58
+ next unless params_class.name =~ /(.+)Params$/
59
+
60
+ model_name = ::Regexp.last_match(1)
61
+ StrongParameters::Core::ParamsRegistry.register(model_name, params_class)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Durable Parameters version information.
4
+ module DurableParameters
5
+ # Current version of the durable_parameters gem
6
+ VERSION = '0.2.3'
7
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Strong Parameters provides a whitelist-based approach to mass assignment protection.
4
+ #
5
+ # This gem provides a framework-agnostic approach to parameter filtering and mass
6
+ # assignment protection, with adapters for various Ruby web frameworks including
7
+ # Rails, Sinatra, Hanami, and Rage.
8
+ #
9
+ # @see StrongParameters::Core::Parameters
10
+ # @see StrongParameters::Core::ApplicationParams
11
+ # @see StrongParameters::Core::ParamsRegistry
12
+
13
+ require 'durable_parameters/version'
14
+ require 'durable_parameters/core'
15
+
16
+ # Auto-detect and load framework adapter
17
+ if defined?(Rails)
18
+ # Rails is loaded - use Rails adapter
19
+ require 'durable_parameters/railtie'
20
+ require 'durable_parameters/log_subscriber'
21
+ elsif defined?(Sinatra)
22
+ # Sinatra is loaded - auto-setup Sinatra adapter
23
+ require 'durable_parameters/adapters/sinatra'
24
+ elsif defined?(Hanami)
25
+ # Hanami is loaded - auto-setup Hanami adapter
26
+ require 'durable_parameters/adapters/hanami'
27
+ StrongParameters::Adapters::Hanami.setup!
28
+ elsif defined?(Rage) || defined?(RageController)
29
+ # Rage is loaded - auto-setup Rage adapter
30
+ require 'durable_parameters/adapters/rage'
31
+ StrongParameters::Adapters::Rage.setup!
32
+ end
33
+
34
+ # Provide top-level convenience aliases
35
+ module StrongParameters
36
+ # Convenience aliases for core classes
37
+ Parameters = Core::Parameters unless defined?(Parameters)
38
+ ApplicationParams = Core::ApplicationParams unless defined?(ApplicationParams)
39
+ ParamsRegistry = Core::ParamsRegistry unless defined?(ParamsRegistry)
40
+ ForbiddenAttributesProtection = Core::ForbiddenAttributesProtection unless defined?(ForbiddenAttributesProtection)
41
+ end
@@ -0,0 +1,12 @@
1
+ Description:
2
+ Stubs out a scaffolded controller and its views. Different from rails
3
+ scaffold_controller, it uses strong_parameters to whitelist permissible
4
+ attributes in a private method.
5
+ Pass the model name, either CamelCased or under_scored. The controller
6
+ name is retrieved as a pluralized version of the model name.
7
+
8
+ To create a controller within a module, specify the model name as a
9
+ path like 'parent_module/controller_name'.
10
+
11
+ This generates a controller class in app/controllers and invokes helper,
12
+ template engine and test framework generators.
@@ -0,0 +1,17 @@
1
+ require 'rails/version'
2
+ require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator'
3
+
4
+ module Rails
5
+ module Generators
6
+ class StrongParametersControllerGenerator < ScaffoldControllerGenerator
7
+ argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ if ::Rails::VERSION::STRING < '3.1'
11
+ def module_namespacing
12
+ yield if block_given?
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,94 @@
1
+ <% module_namespacing do -%>
2
+ class <%= controller_class_name %>Controller < ApplicationController
3
+ # GET <%= route_url %>
4
+ # GET <%= route_url %>.json
5
+ def index
6
+ @<%= plural_table_name %> = <%= orm_class.all(class_name) %>
7
+
8
+ respond_to do |format|
9
+ format.html # index.html.erb
10
+ format.json { render json: <%= "@#{plural_table_name}" %> }
11
+ end
12
+ end
13
+
14
+ # GET <%= route_url %>/1
15
+ # GET <%= route_url %>/1.json
16
+ def show
17
+ @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
18
+
19
+ respond_to do |format|
20
+ format.html # show.html.erb
21
+ format.json { render json: <%= "@#{singular_table_name}" %> }
22
+ end
23
+ end
24
+
25
+ # GET <%= route_url %>/new
26
+ # GET <%= route_url %>/new.json
27
+ def new
28
+ @<%= singular_table_name %> = <%= orm_class.build(class_name) %>
29
+
30
+ respond_to do |format|
31
+ format.html # new.html.erb
32
+ format.json { render json: <%= "@#{singular_table_name}" %> }
33
+ end
34
+ end
35
+
36
+ # GET <%= route_url %>/1/edit
37
+ def edit
38
+ @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
39
+ end
40
+
41
+ # POST <%= route_url %>
42
+ # POST <%= route_url %>.json
43
+ def create
44
+ @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
45
+
46
+ respond_to do |format|
47
+ if @<%= orm_instance.save %>
48
+ format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %> }
49
+ format.json { render json: <%= "@#{singular_table_name}" %>, status: :created, location: <%= "@#{singular_table_name}" %> }
50
+ else
51
+ format.html { render action: "new" }
52
+ format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity }
53
+ end
54
+ end
55
+ end
56
+
57
+ # PATCH/PUT <%= route_url %>/1
58
+ # PATCH/PUT <%= route_url %>/1.json
59
+ def update
60
+ @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
61
+
62
+ respond_to do |format|
63
+ if @<%= orm_instance.update("#{singular_table_name}_params") %>
64
+ format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %> }
65
+ format.json { head :no_content }
66
+ else
67
+ format.html { render action: "edit" }
68
+ format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity }
69
+ end
70
+ end
71
+ end
72
+
73
+ # DELETE <%= route_url %>/1
74
+ # DELETE <%= route_url %>/1.json
75
+ def destroy
76
+ @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
77
+ @<%= orm_instance.destroy %>
78
+
79
+ respond_to do |format|
80
+ format.html { redirect_to <%= index_helper %>_url }
81
+ format.json { head :no_content }
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ # Use this method to whitelist the permissible parameters. Example:
88
+ # params.require(:person).permit(:name, :age)
89
+ # Also, you can specialize this method with per-user checking of permissible attributes.
90
+ def <%= "#{singular_table_name}_params" %>
91
+ params.require(<%= ":#{singular_table_name}" %>).permit(<%= attributes.map {|a| ":#{a.name}" }.sort.join(', ') %>)
92
+ end
93
+ end
94
+ <% end -%>
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module ActionController
6
+ # Base class for declarative parameter permission definitions.
7
+ #
8
+ # ApplicationParams provides a DSL for defining which attributes are allowed
9
+ # or denied for mass assignment in controllers. This enables a centralized,
10
+ # declarative approach to parameter filtering that's more maintainable than
11
+ # inline permit() calls.
12
+ #
13
+ # @example Basic usage
14
+ # class UserParams < ApplicationParams
15
+ # allow :name
16
+ # allow :email
17
+ # deny :is_admin
18
+ # end
19
+ #
20
+ # @example With action-specific permissions
21
+ # class PostParams < ApplicationParams
22
+ # allow :title
23
+ # allow :body
24
+ # allow :published, only: :create
25
+ # allow :view_count, except: :create
26
+ # end
27
+ #
28
+ # @example With metadata declaration
29
+ # class AccountParams < ApplicationParams
30
+ # allow :name
31
+ # metadata :ip_address, :role
32
+ # end
33
+ class ApplicationParams
34
+ class << self
35
+ # Returns the list of allowed attributes.
36
+ #
37
+ # @return [Array<Symbol>] array of allowed attribute names
38
+ def allowed_attributes
39
+ @allowed_attributes ||= []
40
+ end
41
+
42
+ # Returns the list of denied attributes.
43
+ #
44
+ # @return [Array<Symbol>] array of denied attribute names
45
+ def denied_attributes
46
+ @denied_attributes ||= []
47
+ end
48
+
49
+ # Returns the flags hash.
50
+ #
51
+ # @return [Hash<Symbol, Object>] hash of flag names to values
52
+ def flags
53
+ @flags ||= {}
54
+ end
55
+
56
+ # Returns the set of allowed metadata keys.
57
+ #
58
+ # @return [Set<Symbol>] set of allowed metadata key names
59
+ # @note :current_user is always implicitly allowed
60
+ def allowed_metadata
61
+ @allowed_metadata ||= Set.new
62
+ end
63
+
64
+ # DSL method to allow an attribute
65
+ # @param attribute [Symbol, String, Hash] the attribute name to allow, or a hash for arrays
66
+ # @param options [Hash] additional options
67
+ # - :only - only allow this attribute for these actions
68
+ # - :except - allow this attribute except for these actions
69
+ # - :array - if true, permit an array of scalar values
70
+ # Examples:
71
+ # allow :name # permits scalar name
72
+ # allow :tags, array: true # permits array of scalars
73
+ # allow :tags, only: :create # only for create action
74
+ def allow(attribute, options = {})
75
+ attribute = attribute.to_sym
76
+ allowed_attributes << attribute unless allowed_attributes.include?(attribute)
77
+
78
+ # Store any additional options for this attribute
79
+ if options.any?
80
+ @attribute_options ||= {}
81
+ # Normalize :only and :except to arrays for consistency
82
+ normalized_options = options.dup
83
+ [:only, :except].each do |key|
84
+ if normalized_options[key] && !normalized_options[key].is_a?(Array)
85
+ normalized_options[key] = [normalized_options[key]]
86
+ end
87
+ end
88
+ @attribute_options[attribute] = normalized_options
89
+ end
90
+ end
91
+
92
+ # DSL method to deny an attribute
93
+ # @param attribute [Symbol, String] the attribute name to deny
94
+ def deny(attribute)
95
+ attribute = attribute.to_sym
96
+ denied_attributes << attribute unless denied_attributes.include?(attribute)
97
+ end
98
+
99
+ # DSL method to set a flag
100
+ # @param name [Symbol, String] the flag name
101
+ # @param value [Boolean, Object] the flag value
102
+ def flag(name, value = true)
103
+ flags[name.to_sym] = value
104
+ end
105
+
106
+ # DSL method to declare allowed metadata keys
107
+ # @param key [Symbol, String] the metadata key to allow
108
+ # Note: :current_user is always allowed and doesn't need to be declared
109
+ def metadata(*keys)
110
+ keys.each do |key|
111
+ allowed_metadata << key.to_sym
112
+ end
113
+ end
114
+
115
+ # Check if an attribute is allowed.
116
+ #
117
+ # An attribute is allowed if it's in the allowed list and not in the denied list.
118
+ # Uses memoization for better performance on repeated checks.
119
+ #
120
+ # @param attribute [Symbol, String] the attribute name
121
+ # @return [Boolean] true if allowed, false otherwise
122
+ def allowed?(attribute)
123
+ attribute = attribute.to_sym
124
+ return false if denied_attributes.include?(attribute)
125
+
126
+ allowed_attributes.include?(attribute)
127
+ end
128
+
129
+ # Check if an attribute is denied.
130
+ #
131
+ # @param attribute [Symbol, String] the attribute name
132
+ # @return [Boolean] true if denied, false otherwise
133
+ def denied?(attribute)
134
+ denied_attributes.include?(attribute.to_sym)
135
+ end
136
+
137
+ # Check if a flag is set
138
+ # @param name [Symbol, String] the flag name
139
+ # @return [Boolean, Object] the flag value
140
+ def flag?(name)
141
+ flags[name.to_sym]
142
+ end
143
+
144
+ # Check if a metadata key is allowed
145
+ # @param key [Symbol, String] the metadata key
146
+ # @return [Boolean] true if allowed, false otherwise
147
+ # Note: :current_user is always allowed
148
+ def metadata_allowed?(key)
149
+ key = key.to_sym
150
+ key == :current_user || allowed_metadata.include?(key)
151
+ end
152
+
153
+ # Get options for an attribute
154
+ # @param attribute [Symbol, String] the attribute name
155
+ # @return [Hash] the options hash
156
+ def attribute_options(attribute)
157
+ @attribute_options ||= {}
158
+ @attribute_options[attribute.to_sym] || {}
159
+ end
160
+
161
+ # Generate a permit array suitable for strong_parameters.
162
+ #
163
+ # Returns an array of permitted attributes, optionally filtered by action.
164
+ # Results are cached per action for better performance.
165
+ #
166
+ # @param action [Symbol, String, nil] optional action name to filter by
167
+ # @return [Array<Symbol, Hash>] array of permitted attributes
168
+ # Returns symbols for scalar attributes, and {attr: []} for array attributes
169
+ # @example
170
+ # permitted_attributes # => [:name, :email]
171
+ # permitted_attributes(action: :create) # => [:name, :email, :published]
172
+ def permitted_attributes(action: nil)
173
+ # Use cache for performance on repeated calls
174
+ @permitted_cache ||= {}
175
+ cache_key = action || :__no_action__
176
+
177
+ return @permitted_cache[cache_key] if @permitted_cache.key?(cache_key)
178
+
179
+ attrs = allowed_attributes.dup
180
+
181
+ # Remove denied attributes
182
+ attrs.reject! { |attr| denied_attributes.include?(attr) }
183
+
184
+ # Filter by action-specific flags if provided
185
+ if action
186
+ action = action.to_sym
187
+ attrs.select! do |attr|
188
+ opts = attribute_options(attr)
189
+ if opts[:only]
190
+ Array(opts[:only]).include?(action)
191
+ elsif opts[:except]
192
+ !Array(opts[:except]).include?(action)
193
+ else
194
+ true
195
+ end
196
+ end
197
+ end
198
+
199
+ # Convert to proper permit format
200
+ # For array attributes, return {attr: []}, otherwise just the symbol
201
+ result = attrs.map do |attr|
202
+ opts = attribute_options(attr)
203
+ if opts[:array]
204
+ { attr => [] }
205
+ else
206
+ attr
207
+ end
208
+ end.freeze
209
+
210
+ @permitted_cache[cache_key] = result
211
+ end
212
+
213
+ # Inherit attributes from parent class.
214
+ #
215
+ # When a subclass is created, it inherits all configuration from the parent
216
+ # including allowed/denied attributes, flags, metadata, and options. This
217
+ # enables building specialized params classes on top of base ones.
218
+ #
219
+ # @param subclass [Class] the inheriting subclass
220
+ # @return [void]
221
+ def inherited(subclass)
222
+ super
223
+ # Copy parent's configuration to subclass
224
+ subclass.instance_variable_set(:@allowed_attributes, allowed_attributes.dup)
225
+ subclass.instance_variable_set(:@denied_attributes, denied_attributes.dup)
226
+ subclass.instance_variable_set(:@flags, flags.dup)
227
+ subclass.instance_variable_set(:@allowed_metadata, allowed_metadata.dup)
228
+ if instance_variable_defined?(:@attribute_options)
229
+ subclass.instance_variable_set(:@attribute_options, @attribute_options.dup)
230
+ end
231
+ # Don't copy the cache - let subclass build its own
232
+ end
233
+ end
234
+ end
235
+ end