Class: StructuredParams::Params

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::Attributes, ActiveModel::Model, AttributeMethods, I18n, Validations
Defined in:
lib/structured_params/params.rb,
sig/structured_params/params.rbs

Overview

Parameter model that supports structured objects and arrays

This class can be used in two ways:

  1. Strong Parameters validation (API requests)
  2. Form objects (View integration with form_with/form_for)

Strong Parameters example (API):

class UserParams < StructuredParams::Params
attribute :name, :string
attribute :age, :integer
attribute :address, :object, value_class: AddressParams
attribute :hobbies, :array, value_class: HobbyParams
attribute :tags, :array, value_type: :string

# Validate raw input before type casting (e.g., "12x" for integer fields)
validates_raw :age, format: { with: /\A\d+\z/, message: 'must be numeric string' }
end

# In controller:
user_params = UserParams.new(params)
if user_params.valid?
User.create!(user_params.attributes)
else
render json: { errors: user_params.errors }
end

Form object example (View integration):

class UserRegistrationForm < StructuredParams::Params
attribute :name, :string
attribute :email, :string
validates :name, presence: true
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
end

# In controller:
@form = UserRegistrationForm.new(params)
if @form.valid?
User.create!(@form.attributes)
redirect_to user_path
else
render :new
end

# In view:
<%= form_with model: @form, url: users_path do |f| %>
<%= f.text_field :name %>
<%= f.text_field :email %>
<% end %>

rubocop:disable Metrics/ClassLength

Class Method Summary collapse

Instance Method Summary collapse

Methods included from I18n

#attr_segments, #build_nested_label, #human_attribute_name, #resolve_nested_human_attribute_name

Methods included from Validations

#validates_raw_remap_errors

Methods included from AttributeMethods

attribute

Constructor Details

#initialize(params) ⇒ Params

: (Hash[untyped, untyped]|::ActionController::Parameters) -> void

Parameters:

  • (Hash[untyped, untyped], ::ActionController::Parameters)


155
156
157
158
# File 'lib/structured_params/params.rb', line 155

def initialize(params)
  processed_params = process_input_parameters(params)
  super(**processed_params)
end

Class Method Details

.form_class?Boolean

: () -> bool

Returns:

  • (Boolean)


137
138
139
# File 'lib/structured_params/params.rb', line 137

def form_class?
  name&.end_with?('Form') || false
end

.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"

: () -> ::ActiveModel::Name

Returns:

  • (::ActiveModel::Name)


77
78
79
80
81
82
83
84
# File 'lib/structured_params/params.rb', line 77

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)
    name_without_suffix = name.sub(/(Parameters?|Form)$/, '')
    ActiveModel::Name.new(self, namespace, name_without_suffix)
  end
end

.permit(params, require: true) ⇒ ActionController::Parameters

Permit parameters with optional require

For Form Objects (explicit manual permit):

UserRegistrationForm.permit(params)
# equivalent to:
params.require(:user_registration).permit(*UserRegistrationForm.permit_attribute_names)

For API requests (without require):

UserParams.permit(params, require: false)
# equivalent to:
params.permit(*UserParams.permit_attribute_names)

: (ActionController::Parameters params, ?require: bool) -> ActionController::Parameters

Parameters:

  • params (ActionController::Parameters)
  • require: (Boolean) (defaults to: true)

Returns:

  • (ActionController::Parameters)


113
114
115
116
117
118
119
120
# File 'lib/structured_params/params.rb', line 113

def permit(params, require: true)
  if require
    key = model_name.param_key.to_sym
    params.require(key).permit(*permit_attribute_names)
  else
    params.permit(*permit_attribute_names)
  end
end

.permit_attribute_namesArray[untyped]

Generate permitted parameter structure for Strong Parameters : () -> Array

Returns:

  • (Array[untyped])


88
89
90
91
92
93
94
95
96
97
98
# File 'lib/structured_params/params.rb', line 88

def permit_attribute_names
  attribute_types.map do |name, type|
    name = name.to_sym

    if type.is_a?(Type::Object) || type.is_a?(Type::Array)
      { name => type.permit_attribute_names }
    else
      name
    end
  end
end

.structured_attributesHash[Symbol, singleton(::StructuredParams::Params)]

Get structured attributes and their classes : () -> Hash[Symbol, singleton(::StructuredParams::Params)]

Returns:



124
125
126
127
128
129
130
131
132
133
134
# File 'lib/structured_params/params.rb', line 124

def structured_attributes
  @structured_attributes ||= attribute_types.each_with_object({}) do |(name, type), hash|
    next unless structured_params_type?(type)

    hash[name] = if type.is_a?(Type::Array)
                   type.item_type.value_class
                 else
                   type.value_class
                 end
  end
end

Instance Method Details

#attribute_keys_present?(params) ⇒ Boolean

: (ActionController::Parameters) -> bool

Parameters:

  • (ActionController::Parameters)

Returns:

  • (Boolean)


268
269
270
# File 'lib/structured_params/params.rb', line 268

def attribute_keys_present?(params)
  params.keys.any? { |key| self.class.attribute_types.key?(key.to_s) }
end

#attributes(symbolize:, compact_mode:) ⇒ Hash[String, untyped] #attributes(symbolize:, compact_mode:) ⇒ Hash[Symbol, untyped]

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]

Overloads:

  • #attributes(symbolize:, compact_mode:) ⇒ Hash[String, untyped]

    Parameters:

    • symbolize: (false)
    • compact_mode: (:none, :nil_only, :all_blank)

    Returns:

    • (Hash[String, untyped])
  • #attributes(symbolize:, compact_mode:) ⇒ Hash[Symbol, untyped]

    Parameters:

    • symbolize: (true)
    • compact_mode: (:none, :nil_only, :all_blank)

    Returns:

    • (Hash[Symbol, untyped])


184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/structured_params/params.rb', line 184

def attributes(symbolize: false, compact_mode: :none)
  attrs = super()

  self.class.structured_attributes.each_key do |name|
    value = attrs[name.to_s]
    attrs[name.to_s] = serialize_structured_value(value, compact_mode: compact_mode)
  end

  result = symbolize ? attrs.deep_symbolize_keys : attrs

  case compact_mode
  when :all_blank
    result.compact_blank
  when :nil_only
    result.compact
  else
    result
  end
end

#errors::StructuredParams::Errors

: () -> ::StructuredParams::Errors



161
162
163
# File 'lib/structured_params/params.rb', line 161

def errors
  @errors ||= Errors.new(self)
end

#flat_parameters?(params) ⇒ Boolean

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

Parameters:

  • (ActionController::Parameters)

Returns:

  • (Boolean)


261
262
263
264
265
# File 'lib/structured_params/params.rb', line 261

def flat_parameters?(params)
  return false if params.values.any?(ActionController::Parameters)

  attribute_keys_present?(params)
end

#format_error_path(attr_name, index = nil) ⇒ String

Format error path using dot notation (always consistent) : (Symbol, Integer?) -> String

Parameters:

  • (Symbol)
  • (Integer, nil)

Returns:

  • (String)


310
311
312
313
314
# File 'lib/structured_params/params.rb', line 310

def format_error_path(attr_name, index = nil)
  path_parts = [attr_name]
  path_parts << index.to_s if index
  path_parts.join('.')
end

#import_structured_errors(structured_errors, prefix) ⇒ void

This method returns an undefined value.

Integrate structured parameter errors into parent errors : (untyped, String) -> void

Parameters:

  • (Object)
  • (String)


340
341
342
343
344
345
346
347
348
# File 'lib/structured_params/params.rb', line 340

def import_structured_errors(structured_errors, prefix)
  structured_errors.each do |error|
    # Create dotted attribute path and import with the message already resolved
    # in the child model's i18n context, so the parent model's locale does not
    # override the child's translations.
    error_attribute = "#{prefix}.#{error.attribute}"
    errors.import(error, attribute: error_attribute.to_sym, message: error.message)
  end
end

#matches_model_name?(params) ⇒ Boolean

: (ActionController::Parameters) -> bool

Parameters:

  • (ActionController::Parameters)

Returns:

  • (Boolean)


249
250
251
252
253
254
# File 'lib/structured_params/params.rb', line 249

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

#persisted?Boolean

Form object support for Rails helpers. : () -> bool

Returns:

  • (Boolean)


167
168
169
# File 'lib/structured_params/params.rb', line 167

def persisted?
  false
end

#process_action_controller_parameters(params) ⇒ Hash[untyped, untyped]

: (ActionController::Parameters) -> Hash[untyped, untyped]

Parameters:

  • (ActionController::Parameters)

Returns:

  • (Hash[untyped, untyped])


221
222
223
# File 'lib/structured_params/params.rb', line 221

def process_action_controller_parameters(params)
  self.class.permit(params, require: require_nested_parameters?(params)).to_h
end

#process_input_parameters(params) ⇒ Hash[untyped, untyped]

Process input parameters : (untyped) -> Hash[untyped, untyped]

Parameters:

  • (Object)

Returns:

  • (Hash[untyped, untyped])


208
209
210
211
212
213
214
215
216
217
218
# File 'lib/structured_params/params.rb', line 208

def process_input_parameters(params)
  case params
  when ActionController::Parameters
    process_action_controller_parameters(params)
  when Hash
    # ActiveModel::Attributes can handle both symbol and string keys
    params
  else
    raise ArgumentError, "params must be ActionController::Parameters or Hash, got #{params.class}"
  end
end

#require_nested_parameters?(params) ⇒ Boolean

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) 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

Parameters:

  • (ActionController::Parameters)

Returns:

  • (Boolean)


239
240
241
242
243
244
245
246
# File 'lib/structured_params/params.rb', line 239

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

#serialize_structured_value(value, compact_mode: :none) ⇒ Object

Serialize structured values : (untyped, ?compact_mode: :none | :nil_only | :all_blank) -> untyped

Parameters:

  • (Object)
  • compact_mode: (:none, :nil_only, :all_blank) (defaults to: :none)

Returns:

  • (Object)


318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/structured_params/params.rb', line 318

def serialize_structured_value(value, compact_mode: :none)
  case value
  when Array
    result = value.map { |item| item.attributes(symbolize: false, compact_mode: compact_mode) }

    case compact_mode
    when :all_blank
      result.compact_blank
    when :nil_only
      result.compact
    else
      result
    end
  when StructuredParams::Params
    value.attributes(symbolize: false, compact_mode: compact_mode)
  else
    value
  end
end

#to_keynil

: () -> nil

Returns:

  • (nil)


172
173
174
# File 'lib/structured_params/params.rb', line 172

def to_key
  nil
end

#to_modelself

: () -> self

Returns:

  • (self)


177
178
179
# File 'lib/structured_params/params.rb', line 177

def to_model
  self
end

#validate_structured_array(attr_name, array_value) ⇒ void

This method returns an undefined value.

Validate structured arrays : (Symbol, Array) -> void

Parameters:

  • (Symbol)
  • (Array[untyped])


290
291
292
293
294
295
296
297
# File 'lib/structured_params/params.rb', line 290

def validate_structured_array(attr_name, array_value)
  array_value.each_with_index do |item, index|
    next if item.valid?(validation_context)

    error_path = format_error_path(attr_name, index)
    import_structured_errors(item.errors, error_path)
  end
end

#validate_structured_object(attr_name, object_value) ⇒ void

This method returns an undefined value.

Validate structured objects : (Symbol, StructuredParams::Params) -> void

Parameters:



301
302
303
304
305
306
# File 'lib/structured_params/params.rb', line 301

def validate_structured_object(attr_name, object_value)
  return if object_value.valid?(validation_context)

  error_path = format_error_path(attr_name, nil)
  import_structured_errors(object_value.errors, error_path)
end

#validate_structured_parametersvoid

This method returns an undefined value.

Execute structured parameter validation : () -> void



274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/structured_params/params.rb', line 274

def validate_structured_parameters
  self.class.structured_attributes.each_key do |name|
    value = attribute(name)
    next if value.blank?

    case value
    when Array
      validate_structured_array(name, value)
    else
      validate_structured_object(name, value)
    end
  end
end