From a81f09517dbb8c913c4fb152aa5068814485ca05 Mon Sep 17 00:00:00 2001 From: mizuki-y Date: Sun, 12 Jul 2026 15:12:32 +0900 Subject: [PATCH 1/7] Extract form object behavior --- lib/structured_params.rb | 2 + lib/structured_params/form_object.rb | 65 ++++++++++++++++++++++ lib/structured_params/params.rb | 80 ++------------------------- sig/structured_params/form_object.rbs | 40 ++++++++++++++ sig/structured_params/params.rbs | 53 ++---------------- 5 files changed, 115 insertions(+), 125 deletions(-) create mode 100644 lib/structured_params/form_object.rb create mode 100644 sig/structured_params/form_object.rbs diff --git a/lib/structured_params.rb b/lib/structured_params.rb index 622d85b..0463285 100644 --- a/lib/structured_params.rb +++ b/lib/structured_params.rb @@ -3,6 +3,7 @@ require 'active_model' require 'active_model/type' +require 'active_support/concern' require 'action_controller/metal/strong_parameters' # version @@ -13,6 +14,7 @@ require_relative 'structured_params/attribute_methods' require_relative 'structured_params/validations' require_relative 'structured_params/i18n' +require_relative 'structured_params/form_object' # types (load first for module definition) require_relative 'structured_params/type/object' diff --git a/lib/structured_params/form_object.rb b/lib/structured_params/form_object.rb new file mode 100644 index 0000000..5895ed5 --- /dev/null +++ b/lib/structured_params/form_object.rb @@ -0,0 +1,65 @@ +# rbs_inline: enabled +# frozen_string_literal: true + +module StructuredParams + # Form object behavior layered onto Params subclasses. + module FormObject + extend ActiveSupport::Concern + + # Form object support for Rails helpers. + #: () -> bool + def persisted? + false + end + + #: () -> nil + def to_key + nil + end + + #: () -> self + def to_model + self + end + + private + + # Whether to call params.require(param_key) before permitting. + # + # Only ever true for Form-suffixed classes (form_class?); non-Form + # Params/Parameters subclasses always permit the top level directly. + #: (ActionController::Parameters) -> bool + def require_nested_parameters?(params) + return false unless self.class.form_class? + return true if matches_model_name?(params) + return false if params.permitted? + return false if flat_parameters?(params) + + true + end + + #: (ActionController::Parameters) -> bool + def matches_model_name?(params) + key = self.class.model_name.param_key + return false unless params.key?(key) + + params[key].is_a?(ActionController::Parameters) + end + + # Only treat params as already-flat attributes when none of the top-level + # values are themselves nested. A nested value under some other key means + # the request shape is ambiguous, so we fall through to raising + # ParameterMissing instead of silently guessing which keys belong here. + #: (ActionController::Parameters) -> bool + def flat_parameters?(params) + return false if params.values.any?(ActionController::Parameters) + + attribute_keys_present?(params) + end + + #: (ActionController::Parameters) -> bool + def attribute_keys_present?(params) + params.keys.any? { |key| self.class.attribute_types.key?(key.to_s) } + end + end +end diff --git a/lib/structured_params/params.rb b/lib/structured_params/params.rb index 02ab255..b0e5c3a 100644 --- a/lib/structured_params/params.rb +++ b/lib/structured_params/params.rb @@ -50,13 +50,13 @@ module StructuredParams # <%= f.text_field :name %> # <%= f.text_field :email %> # <% end %> - # rubocop:disable Metrics/ClassLength class Params include ActiveModel::Model include ActiveModel::Attributes include AttributeMethods include Validations include I18n + include FormObject # @rbs @errors: ::StructuredParams::Errors? @@ -64,20 +64,12 @@ class << self # @rbs self.@structured_attributes: Hash[Symbol, singleton(::StructuredParams::Params)]? # @rbs self.@model_name: ::ActiveModel::Name? - # Override model_name for form helpers - # By default, removes "Parameters", "Parameter", or "Form" suffix from class name - # This allows the class to work seamlessly with Rails form helpers - # - # Example: - # UserRegistrationForm.model_name.name # => "UserRegistration" - # UserRegistrationForm.model_name.param_key # => "user_registration" - # UserParameters.model_name.name # => "User" - # Admin::UserForm.model_name.name # => "Admin::User" + # Override model_name for Rails integrations. + # By default, removes "Parameters", "Parameter", or "Form" suffix from class name. #: () -> ::ActiveModel::Name def model_name @model_name ||= begin - namespace = module_parents.detect { |n| n.respond_to?(:use_relative_model_naming?) } - # Remove suffix from the full class name (preserving namespace) + namespace = module_parents.detect { |name| name.respond_to?(:use_relative_model_naming?) } name_without_suffix = name.sub(/(Parameters?|Form)$/, '') ActiveModel::Name.new(self, namespace, name_without_suffix) end @@ -162,22 +154,6 @@ def errors @errors ||= Errors.new(self) end - # Form object support for Rails helpers. - #: () -> bool - def persisted? - false - end - - #: () -> nil - def to_key - nil - end - - #: () -> self - def to_model - self - end - # Convert structured objects to Hash and get attributes #: (?symbolize: false, ?compact_mode: :none | :nil_only | :all_blank) -> Hash[String, untyped] #: (?symbolize: true, ?compact_mode: :none | :nil_only | :all_blank) -> Hash[Symbol, untyped] @@ -222,53 +198,6 @@ def process_action_controller_parameters(params) self.class.permit(params, require: require_nested_parameters?(params)).to_h end - # Whether to call params.require(param_key) before permitting. - # - # Only ever true for Form-suffixed classes (form_class?); non-Form - # Params/Parameters subclasses always permit the top level directly. - # - # - A value nested under the model's param_key (e.g. params[:user_registration]) - # always wins, even if params were already permitted higher up, since that - # inner key still needs to be require()'d out. - # - Otherwise, params that are already permitted, or whose shape looks flat - # (see flat_parameters?), are used as-is without requiring. - # - Anything else falls through to true, so params.require raises - # ActionController::ParameterMissing instead of guessing which keys - # belong to this form. - #: (ActionController::Parameters) -> bool - def require_nested_parameters?(params) - return false unless self.class.form_class? - return true if matches_model_name?(params) - return false if params.permitted? - return false if flat_parameters?(params) - - true - end - - #: (ActionController::Parameters) -> bool - def matches_model_name?(params) - key = self.class.model_name.param_key - return false unless params.key?(key) - - params[key].is_a?(ActionController::Parameters) - end - - # Only treat params as already-flat attributes when none of the top-level - # values are themselves nested. A nested value under some other key means - # the request shape is ambiguous, so we fall through to raising - # ParameterMissing instead of silently guessing which keys belong here. - #: (ActionController::Parameters) -> bool - def flat_parameters?(params) - return false if params.values.any?(ActionController::Parameters) - - attribute_keys_present?(params) - end - - #: (ActionController::Parameters) -> bool - def attribute_keys_present?(params) - params.keys.any? { |key| self.class.attribute_types.key?(key.to_s) } - end - # Execute structured parameter validation #: () -> void def validate_structured_parameters @@ -347,5 +276,4 @@ def import_structured_errors(structured_errors, prefix) end end end - # rubocop:enable Metrics/ClassLength end diff --git a/sig/structured_params/form_object.rbs b/sig/structured_params/form_object.rbs new file mode 100644 index 0000000..a146fdf --- /dev/null +++ b/sig/structured_params/form_object.rbs @@ -0,0 +1,40 @@ +# Generated from lib/structured_params/form_object.rb with RBS::Inline + +module StructuredParams + # Form object behavior layered onto Params subclasses. + module FormObject + extend ActiveSupport::Concern + + # Form object support for Rails helpers. + # : () -> bool + def persisted?: () -> bool + + # : () -> nil + def to_key: () -> nil + + # : () -> self + def to_model: () -> self + + private + + # Whether to call params.require(param_key) before permitting. + # + # Only ever true for Form-suffixed classes (form_class?); non-Form + # Params/Parameters subclasses always permit the top level directly. + # : (ActionController::Parameters) -> bool + def require_nested_parameters?: (ActionController::Parameters) -> bool + + # : (ActionController::Parameters) -> bool + def matches_model_name?: (ActionController::Parameters) -> bool + + # Only treat params as already-flat attributes when none of the top-level + # values are themselves nested. A nested value under some other key means + # the request shape is ambiguous, so we fall through to raising + # ParameterMissing instead of silently guessing which keys belong here. + # : (ActionController::Parameters) -> bool + def flat_parameters?: (ActionController::Parameters) -> bool + + # : (ActionController::Parameters) -> bool + def attribute_keys_present?: (ActionController::Parameters) -> bool + end +end diff --git a/sig/structured_params/params.rbs b/sig/structured_params/params.rbs index 55aa1e7..ed7873d 100644 --- a/sig/structured_params/params.rbs +++ b/sig/structured_params/params.rbs @@ -49,7 +49,6 @@ module StructuredParams # <%= f.text_field :name %> # <%= f.text_field :email %> # <% end %> - # rubocop:disable Metrics/ClassLength class Params include ActiveModel::Model @@ -61,21 +60,16 @@ module StructuredParams include I18n + include FormObject + @errors: ::StructuredParams::Errors? self.@structured_attributes: Hash[Symbol, singleton(::StructuredParams::Params)]? self.@model_name: ::ActiveModel::Name? - # Override model_name for form helpers - # By default, removes "Parameters", "Parameter", or "Form" suffix from class name - # This allows the class to work seamlessly with Rails form helpers - # - # Example: - # UserRegistrationForm.model_name.name # => "UserRegistration" - # UserRegistrationForm.model_name.param_key # => "user_registration" - # UserParameters.model_name.name # => "User" - # Admin::UserForm.model_name.name # => "Admin::User" + # Override model_name for Rails integrations. + # By default, removes "Parameters", "Parameter", or "Form" suffix from class name. # : () -> ::ActiveModel::Name def self.model_name: () -> ::ActiveModel::Name @@ -115,16 +109,6 @@ module StructuredParams # : () -> ::StructuredParams::Errors def errors: () -> ::StructuredParams::Errors - # Form object support for Rails helpers. - # : () -> bool - def persisted?: () -> bool - - # : () -> nil - def to_key: () -> nil - - # : () -> self - def to_model: () -> self - # Convert structured objects to Hash and get attributes # : (?symbolize: false, ?compact_mode: :none | :nil_only | :all_blank) -> Hash[String, untyped] # : (?symbolize: true, ?compact_mode: :none | :nil_only | :all_blank) -> Hash[Symbol, untyped] @@ -140,35 +124,6 @@ module StructuredParams # : (ActionController::Parameters) -> Hash[untyped, untyped] def process_action_controller_parameters: (ActionController::Parameters) -> Hash[untyped, untyped] - # Whether to call params.require(param_key) before permitting. - # - # Only ever true for Form-suffixed classes (form_class?); non-Form - # Params/Parameters subclasses always permit the top level directly. - # - # - A value nested under the model's param_key (e.g. params[:user_registration]) - # always wins, even if params were already permitted higher up, since that - # inner key still needs to be require()'d out. - # - Otherwise, params that are already permitted, or whose shape looks flat - # (see flat_parameters?), are used as-is without requiring. - # - Anything else falls through to true, so params.require raises - # ActionController::ParameterMissing instead of guessing which keys - # belong to this form. - # : (ActionController::Parameters) -> bool - def require_nested_parameters?: (ActionController::Parameters) -> bool - - # : (ActionController::Parameters) -> bool - def matches_model_name?: (ActionController::Parameters) -> bool - - # Only treat params as already-flat attributes when none of the top-level - # values are themselves nested. A nested value under some other key means - # the request shape is ambiguous, so we fall through to raising - # ParameterMissing instead of silently guessing which keys belong here. - # : (ActionController::Parameters) -> bool - def flat_parameters?: (ActionController::Parameters) -> bool - - # : (ActionController::Parameters) -> bool - def attribute_keys_present?: (ActionController::Parameters) -> bool - # Execute structured parameter validation # : () -> void def validate_structured_parameters: () -> void From 23e81afbdd6b8aea06f5ce5b2d65c07e2c46118a Mon Sep 17 00:00:00 2001 From: mizuki-y Date: Sun, 12 Jul 2026 15:51:22 +0900 Subject: [PATCH 2/7] Scope model_name suffix stripping to Form classes only Move model_name and form_class? into the FormObject concern so the custom "Parameters"/"Parameter"/"Form" suffix-stripping naming lives alongside the other form-only logic that depends on it (require_nested_parameters?, matches_model_name?). Non-Form Params subclasses now fall back to ActiveModel's default model_name (based on the class's own name) instead of having a suffix silently removed, making param_key/i18n_key resolution predictable without needing to mentally track which suffix was stripped. This is a breaking change for non-Form Parameters/Parameter classes: their model_name, param_key, and i18n_key now reflect the full class name (e.g. UserParameter -> "user_parameter" instead of "user"). Updated specs and locale fixtures accordingly. --- lib/structured_params/form_object.rb | 27 +++++++++++++++++++++++++++ lib/structured_params/params.rb | 17 ----------------- sig/structured_params/form_object.rbs | 15 +++++++++++++++ sig/structured_params/params.rbs | 10 ---------- spec/form_object_spec.rb | 12 ++++++------ spec/i18n_spec.rb | 4 ++-- spec/params_spec.rb | 2 +- spec/permit_spec.rb | 4 ++-- spec/support/locales/ja.yml | 12 ++++++------ 9 files changed, 59 insertions(+), 44 deletions(-) diff --git a/lib/structured_params/form_object.rb b/lib/structured_params/form_object.rb index 5895ed5..ad959dd 100644 --- a/lib/structured_params/form_object.rb +++ b/lib/structured_params/form_object.rb @@ -6,6 +6,33 @@ module StructuredParams module FormObject extend ActiveSupport::Concern + class_methods do + # @rbs @model_name: ::ActiveModel::Name? + + #: () -> bool + def form_class? + name&.end_with?('Form') || false + end + + # Override model_name for Rails form integration. + # + # Only applies to Form-suffixed classes, stripping the "Form" suffix so + # form_with/url helpers and i18n resolve to the underlying model name + # (e.g. UserRegistrationForm -> "UserRegistration"). Other Params + # subclasses (Parameters/Parameter/plain) keep the default ActiveModel + # model_name based on their own class name. + #: () -> ::ActiveModel::Name + def model_name + @model_name ||= if form_class? + namespace = module_parents.detect { |mod| mod.respond_to?(:use_relative_model_naming?) } + name_without_suffix = name.sub(/Form$/, '') + ActiveModel::Name.new(self, namespace, name_without_suffix) + else + super + end + end + end + # Form object support for Rails helpers. #: () -> bool def persisted? diff --git a/lib/structured_params/params.rb b/lib/structured_params/params.rb index b0e5c3a..f086ade 100644 --- a/lib/structured_params/params.rb +++ b/lib/structured_params/params.rb @@ -62,18 +62,6 @@ class Params class << self # @rbs self.@structured_attributes: Hash[Symbol, singleton(::StructuredParams::Params)]? - # @rbs self.@model_name: ::ActiveModel::Name? - - # Override model_name for Rails integrations. - # By default, removes "Parameters", "Parameter", or "Form" suffix from class name. - #: () -> ::ActiveModel::Name - def model_name - @model_name ||= begin - namespace = module_parents.detect { |name| name.respond_to?(:use_relative_model_naming?) } - name_without_suffix = name.sub(/(Parameters?|Form)$/, '') - ActiveModel::Name.new(self, namespace, name_without_suffix) - end - end # Generate permitted parameter structure for Strong Parameters #: () -> Array[untyped] @@ -125,11 +113,6 @@ def structured_attributes end end - #: () -> bool - def form_class? - name&.end_with?('Form') || false - end - private # Determine if the specified type is a StructuredParams type diff --git a/sig/structured_params/form_object.rbs b/sig/structured_params/form_object.rbs index a146fdf..83d37bd 100644 --- a/sig/structured_params/form_object.rbs +++ b/sig/structured_params/form_object.rbs @@ -5,6 +5,21 @@ module StructuredParams module FormObject extend ActiveSupport::Concern + @model_name: ::ActiveModel::Name? + + # : () -> bool + def form_class?: () -> bool + + # Override model_name for Rails form integration. + # + # Only applies to Form-suffixed classes, stripping the "Form" suffix so + # form_with/url helpers and i18n resolve to the underlying model name + # (e.g. UserRegistrationForm -> "UserRegistration"). Other Params + # subclasses (Parameters/Parameter/plain) keep the default ActiveModel + # model_name based on their own class name. + # : () -> ::ActiveModel::Name + def model_name: () -> ::ActiveModel::Name + # Form object support for Rails helpers. # : () -> bool def persisted?: () -> bool diff --git a/sig/structured_params/params.rbs b/sig/structured_params/params.rbs index ed7873d..4ad5c70 100644 --- a/sig/structured_params/params.rbs +++ b/sig/structured_params/params.rbs @@ -66,13 +66,6 @@ module StructuredParams self.@structured_attributes: Hash[Symbol, singleton(::StructuredParams::Params)]? - self.@model_name: ::ActiveModel::Name? - - # Override model_name for Rails integrations. - # By default, removes "Parameters", "Parameter", or "Form" suffix from class name. - # : () -> ::ActiveModel::Name - def self.model_name: () -> ::ActiveModel::Name - # Generate permitted parameter structure for Strong Parameters # : () -> Array[untyped] def self.permit_attribute_names: () -> Array[untyped] @@ -96,9 +89,6 @@ module StructuredParams # : () -> Hash[Symbol, singleton(::StructuredParams::Params)] def self.structured_attributes: () -> Hash[Symbol, singleton(::StructuredParams::Params)] - # : () -> bool - def self.form_class?: () -> bool - # Determine if the specified type is a StructuredParams type # : (ActiveModel::Type::Value) -> bool private def self.structured_params_type?: (ActiveModel::Type::Value) -> bool diff --git a/spec/form_object_spec.rb b/spec/form_object_spec.rb index 4d318c7..617a88d 100644 --- a/spec/form_object_spec.rb +++ b/spec/form_object_spec.rb @@ -220,16 +220,16 @@ end describe 'class name with "Parameters" suffix' do - it 'removes "Parameters" suffix from model_name' do - expect(OrderParameters.model_name.name).to eq('Order') - expect(OrderParameters.model_name.param_key).to eq('order') + it 'keeps the default ActiveModel model_name (no suffix removal outside Form classes)' do + expect(OrderParameters.model_name.name).to eq('OrderParameters') + expect(OrderParameters.model_name.param_key).to eq('order_parameters') end end describe 'class name with "Parameter" suffix' do - it 'removes "Parameter" suffix from model_name' do - expect(PaymentParameter.model_name.name).to eq('Payment') - expect(PaymentParameter.model_name.param_key).to eq('payment') + it 'keeps the default ActiveModel model_name (no suffix removal outside Form classes)' do + expect(PaymentParameter.model_name.name).to eq('PaymentParameter') + expect(PaymentParameter.model_name.param_key).to eq('payment_parameter') end end diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index a93c902..fc585ce 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -144,8 +144,8 @@ { activemodel: { attributes: { - user: { name: 'ユーザー名' }, - hobby: { name: 'ホビー名' } + user_parameter: { name: 'ユーザー名' }, + hobby_parameter: { name: 'ホビー名' } }, errors: { nested_attribute: { diff --git a/spec/params_spec.rb b/spec/params_spec.rb index dabb34e..f0649d7 100644 --- a/spec/params_spec.rb +++ b/spec/params_spec.rb @@ -432,7 +432,7 @@ array: '%s %s 番目の%s' }, models: { - hobby: { + hobby_parameter: { attributes: { name: { blank: 'は必須です' }, years_experience: { greater_than_or_equal_to: 'は0以上にしてください' } diff --git a/spec/permit_spec.rb b/spec/permit_spec.rb index 9cc314b..3d29e65 100644 --- a/spec/permit_spec.rb +++ b/spec/permit_spec.rb @@ -37,7 +37,7 @@ context 'with nested objects' do let(:params) do ActionController::Parameters.new( - user: { + user_parameter: { name: 'John', email: 'john@example.com', age: 30, @@ -66,7 +66,7 @@ context 'with arrays' do let(:params) do ActionController::Parameters.new( - user: { + user_parameter: { name: 'John', email: 'john@example.com', age: 30, diff --git a/spec/support/locales/ja.yml b/spec/support/locales/ja.yml index dc797e9..b0cc68f 100644 --- a/spec/support/locales/ja.yml +++ b/spec/support/locales/ja.yml @@ -1,20 +1,20 @@ ja: activemodel: attributes: - user: + user_parameter: hobbies: "趣味" address: "住所" - hobby: + hobby_parameter: name: "名前" level: "レベル" years_experience: "経験年数" - address: + address_parameter: postal_code: "郵便番号" prefecture: "都道府県" - organization: + organization_parameter: team: "チーム" - team_member: + team_member_parameter: name: "名前" organization: "組織" - member_organization: + member_organization_parameter: name: "名称" From 3bbfc58c70a8daf4f47e626ddb75d4d5e501123f Mon Sep 17 00:00:00 2001 From: mizuki-y Date: Sun, 12 Jul 2026 15:54:33 +0900 Subject: [PATCH 3/7] Hoist form_class? gate to the call site process_action_controller_parameters now checks form_class? itself before deferring to FormObject#require_nested_parameters?, instead of require_nested_parameters? checking it internally. This makes the Params/FormObject boundary visible at the call site: reading process_action_controller_parameters alone tells you FormObject's logic is only consulted for Form classes, without having to jump into form_object.rb to learn that. --- lib/structured_params/form_object.rb | 4 +--- lib/structured_params/params.rb | 3 ++- sig/structured_params/form_object.rbs | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/structured_params/form_object.rb b/lib/structured_params/form_object.rb index ad959dd..4c69ac3 100644 --- a/lib/structured_params/form_object.rb +++ b/lib/structured_params/form_object.rb @@ -53,11 +53,9 @@ def to_model # Whether to call params.require(param_key) before permitting. # - # Only ever true for Form-suffixed classes (form_class?); non-Form - # Params/Parameters subclasses always permit the top level directly. + # Only called for Form-suffixed classes (see Params#process_action_controller_parameters). #: (ActionController::Parameters) -> bool def require_nested_parameters?(params) - return false unless self.class.form_class? return true if matches_model_name?(params) return false if params.permitted? return false if flat_parameters?(params) diff --git a/lib/structured_params/params.rb b/lib/structured_params/params.rb index f086ade..66805ee 100644 --- a/lib/structured_params/params.rb +++ b/lib/structured_params/params.rb @@ -178,7 +178,8 @@ def process_input_parameters(params) #: (ActionController::Parameters) -> Hash[untyped, untyped] def process_action_controller_parameters(params) - self.class.permit(params, require: require_nested_parameters?(params)).to_h + require = self.class.form_class? && require_nested_parameters?(params) + self.class.permit(params, require: require).to_h end # Execute structured parameter validation diff --git a/sig/structured_params/form_object.rbs b/sig/structured_params/form_object.rbs index 83d37bd..5f1773b 100644 --- a/sig/structured_params/form_object.rbs +++ b/sig/structured_params/form_object.rbs @@ -34,8 +34,7 @@ module StructuredParams # Whether to call params.require(param_key) before permitting. # - # Only ever true for Form-suffixed classes (form_class?); non-Form - # Params/Parameters subclasses always permit the top level directly. + # Only called for Form-suffixed classes (see Params#process_action_controller_parameters). # : (ActionController::Parameters) -> bool def require_nested_parameters?: (ActionController::Parameters) -> bool From e32dbaeba456078f6652063fbb10929b9166f8cd Mon Sep 17 00:00:00 2001 From: mizuki-y Date: Sun, 12 Jul 2026 15:56:36 +0900 Subject: [PATCH 4/7] Inline process_action_controller_parameters The method was down to two lines and had exactly one caller, so fold it into process_input_parameters's ActionController::Parameters branch instead of keeping a single-use indirection. --- lib/structured_params/form_object.rb | 2 +- lib/structured_params/params.rb | 9 ++------- sig/structured_params/form_object.rbs | 2 +- sig/structured_params/params.rbs | 3 --- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/structured_params/form_object.rb b/lib/structured_params/form_object.rb index 4c69ac3..71de71b 100644 --- a/lib/structured_params/form_object.rb +++ b/lib/structured_params/form_object.rb @@ -53,7 +53,7 @@ def to_model # Whether to call params.require(param_key) before permitting. # - # Only called for Form-suffixed classes (see Params#process_action_controller_parameters). + # Only called for Form-suffixed classes (see Params#process_input_parameters). #: (ActionController::Parameters) -> bool def require_nested_parameters?(params) return true if matches_model_name?(params) diff --git a/lib/structured_params/params.rb b/lib/structured_params/params.rb index 66805ee..74b3d83 100644 --- a/lib/structured_params/params.rb +++ b/lib/structured_params/params.rb @@ -167,7 +167,8 @@ def attributes(symbolize: false, compact_mode: :none) def process_input_parameters(params) case params when ActionController::Parameters - process_action_controller_parameters(params) + require = self.class.form_class? && require_nested_parameters?(params) + self.class.permit(params, require: require).to_h when Hash # ActiveModel::Attributes can handle both symbol and string keys params @@ -176,12 +177,6 @@ def process_input_parameters(params) end end - #: (ActionController::Parameters) -> Hash[untyped, untyped] - def process_action_controller_parameters(params) - require = self.class.form_class? && require_nested_parameters?(params) - self.class.permit(params, require: require).to_h - end - # Execute structured parameter validation #: () -> void def validate_structured_parameters diff --git a/sig/structured_params/form_object.rbs b/sig/structured_params/form_object.rbs index 5f1773b..b4512e5 100644 --- a/sig/structured_params/form_object.rbs +++ b/sig/structured_params/form_object.rbs @@ -34,7 +34,7 @@ module StructuredParams # Whether to call params.require(param_key) before permitting. # - # Only called for Form-suffixed classes (see Params#process_action_controller_parameters). + # Only called for Form-suffixed classes (see Params#process_input_parameters). # : (ActionController::Parameters) -> bool def require_nested_parameters?: (ActionController::Parameters) -> bool diff --git a/sig/structured_params/params.rbs b/sig/structured_params/params.rbs index 4ad5c70..8eb39e2 100644 --- a/sig/structured_params/params.rbs +++ b/sig/structured_params/params.rbs @@ -111,9 +111,6 @@ module StructuredParams # : (untyped) -> Hash[untyped, untyped] def process_input_parameters: (untyped) -> Hash[untyped, untyped] - # : (ActionController::Parameters) -> Hash[untyped, untyped] - def process_action_controller_parameters: (ActionController::Parameters) -> Hash[untyped, untyped] - # Execute structured parameter validation # : () -> void def validate_structured_parameters: () -> void From 4007173a7a7863e1cdf269b58e0c77b3a7e79bfa Mon Sep 17 00:00:00 2001 From: mizuki-y Date: Mon, 13 Jul 2026 06:33:54 +0900 Subject: [PATCH 5/7] Update docs for Form-only model_name suffix stripping docs/form-objects.md and docs/strong-parameters.md still documented the old behavior where Parameters/Parameter suffixes were also stripped from model_name. Update the examples and i18n locale keys to match the current Form-only scoping. --- docs/form-objects.md | 22 ++++++++++++---------- docs/strong-parameters.md | 10 +++++----- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/docs/form-objects.md b/docs/form-objects.md index 1a8c86b..9771fd5 100644 --- a/docs/form-objects.md +++ b/docs/form-objects.md @@ -211,16 +211,18 @@ end ## Class Name Conventions -`StructuredParams::Params` automatically removes the following suffixes from class names: - -- `Parameters` (plural) -- `Parameter` (singular) -- `Form` +Classes whose names end with `Form` have their `model_name` customized: the `Form` suffix is stripped so form helpers, `param_key`, and i18n keys resolve to the underlying model name. ```ruby UserRegistrationForm.model_name.name # => "UserRegistration" UserRegistrationForm.model_name.param_key # => "user_registration" -UserParameters.model_name.name # => "User" +``` + +Classes without a `Form` suffix — including ones named `...Parameters` or `...Parameter` — use ActiveModel's default `model_name`, based on the class's own name as-is: + +```ruby +UserParameters.model_name.name # => "UserParameters" +UserParameters.model_name.param_key # => "user_parameters" ``` ### Nested Modules @@ -292,12 +294,12 @@ Labels for dot-notation nested attributes (e.g. `hobbies.0.name`, `address.posta ja: activemodel: attributes: - user: + user_parameter: hobbies: "趣味" address: "住所" - hobby: + hobby_parameter: name: "名前" - address: + address_parameter: postal_code: "郵便番号" errors: nested_attribute: @@ -305,7 +307,7 @@ ja: object: "%{parent}の%{child}" ``` -Examples: +Examples (`UserParameter` has no `Form` suffix, so its i18n scope is its own class name): - `UserParameter.human_attribute_name(:'hobbies.0.name') # => "趣味 0 番目の名前"` - `UserParameter.human_attribute_name(:'address.postal_code') # => "住所の郵便番号"` diff --git a/docs/strong-parameters.md b/docs/strong-parameters.md index 9fa206b..2fc47e6 100644 --- a/docs/strong-parameters.md +++ b/docs/strong-parameters.md @@ -76,7 +76,7 @@ For fine-grained control, use `permit_attribute_names` directly. ```ruby class UsersController < ApplicationController def create - permitted_params = params.require(:user).permit(*UserParams.permit_attribute_names) + permitted_params = params.require(:user_params).permit(*UserParams.permit_attribute_names) user_params = UserParams.new(permitted_params) if user_params.valid? @@ -159,11 +159,11 @@ end ## How `permit` Determines the Parameter Key -`permit` uses `model_name.param_key` to determine which key to `require`: +`permit` uses `model_name.param_key` to determine which key to `require`. Only `Form`-suffixed classes have their suffix stripped from `model_name`; other classes use their own class name as-is: ```ruby UserParams.permit(params) -# Internally calls: params.require(:user).permit(...) +# Internally calls: params.require(:user_params).permit(...) UserRegistrationForm.permit(params) # Internally calls: params.require(:user_registration).permit(...) @@ -172,7 +172,7 @@ Admin::UserForm.permit(params) # Internally calls: params.require(:admin_user).permit(...) ``` -See [Form Objects](form-objects.md) for details on `model_name` customization. +See [Form Objects](form-objects.md#class-name-conventions) for details on `model_name` customization. ## Choosing the Right Approach @@ -207,6 +207,6 @@ user_params = UserParams.new(UserParams.permit(params, require: false)) - ✅ **Fine-grained control** — integrate with complex Strong Parameters code ```ruby -permitted = params.require(:user).permit(*UserParams.permit_attribute_names, :custom_field) +permitted = params.require(:user_params).permit(*UserParams.permit_attribute_names, :custom_field) user_params = UserParams.new(permitted) ``` From 84ed1ab941d135b1600b5c21b2c8c4b74cc1e7ea Mon Sep 17 00:00:00 2001 From: mizuki-y Date: Mon, 13 Jul 2026 07:50:24 +0900 Subject: [PATCH 6/7] Add RBS annotations for ClassMethods in FormObject module --- lib/structured_params/form_object.rb | 1 + sig/structured_params/form_object.rbs | 31 +++++++++++++++------------ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/structured_params/form_object.rb b/lib/structured_params/form_object.rb index 71de71b..6fce093 100644 --- a/lib/structured_params/form_object.rb +++ b/lib/structured_params/form_object.rb @@ -6,6 +6,7 @@ module StructuredParams module FormObject extend ActiveSupport::Concern + # @rbs module ClassMethods class_methods do # @rbs @model_name: ::ActiveModel::Name? diff --git a/sig/structured_params/form_object.rbs b/sig/structured_params/form_object.rbs index b4512e5..03b52f3 100644 --- a/sig/structured_params/form_object.rbs +++ b/sig/structured_params/form_object.rbs @@ -5,20 +5,23 @@ module StructuredParams module FormObject extend ActiveSupport::Concern - @model_name: ::ActiveModel::Name? - - # : () -> bool - def form_class?: () -> bool - - # Override model_name for Rails form integration. - # - # Only applies to Form-suffixed classes, stripping the "Form" suffix so - # form_with/url helpers and i18n resolve to the underlying model name - # (e.g. UserRegistrationForm -> "UserRegistration"). Other Params - # subclasses (Parameters/Parameter/plain) keep the default ActiveModel - # model_name based on their own class name. - # : () -> ::ActiveModel::Name - def model_name: () -> ::ActiveModel::Name + # @rbs module ClassMethods + module ClassMethods + @model_name: ::ActiveModel::Name? + + # : () -> bool + def form_class?: () -> bool + + # Override model_name for Rails form integration. + # + # Only applies to Form-suffixed classes, stripping the "Form" suffix so + # form_with/url helpers and i18n resolve to the underlying model name + # (e.g. UserRegistrationForm -> "UserRegistration"). Other Params + # subclasses (Parameters/Parameter/plain) keep the default ActiveModel + # model_name based on their own class name. + # : () -> ::ActiveModel::Name + def model_name: () -> ::ActiveModel::Name + end # Form object support for Rails helpers. # : () -> bool From e9f96d32c65bdad14d8631ab37186986188cdbd5 Mon Sep 17 00:00:00 2001 From: mizuki-y Date: Thu, 16 Jul 2026 08:11:34 +0900 Subject: [PATCH 7/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/structured_params/params.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/structured_params/params.rb b/lib/structured_params/params.rb index 74b3d83..41e6420 100644 --- a/lib/structured_params/params.rb +++ b/lib/structured_params/params.rb @@ -167,8 +167,8 @@ def attributes(symbolize: false, compact_mode: :none) def process_input_parameters(params) case params when ActionController::Parameters - require = self.class.form_class? && require_nested_parameters?(params) - self.class.permit(params, require: require).to_h + require_nested = self.class.form_class? && require_nested_parameters?(params) + self.class.permit(params, require: require_nested).to_h when Hash # ActiveModel::Attributes can handle both symbol and string keys params