structured_params 0.9.1 → 0.9.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/lib/structured_params/attribute_methods.rb +29 -0
- data/lib/structured_params/i18n.rb +132 -0
- data/lib/structured_params/params.rb +11 -2
- data/lib/structured_params/validations.rb +72 -0
- data/lib/structured_params/version.rb +1 -1
- data/lib/structured_params.rb +3 -0
- data/sig/structured_params/attribute_methods.rbs +23 -0
- data/sig/structured_params/i18n.rbs +78 -0
- data/sig/structured_params/params.rbs +10 -0
- data/sig/structured_params/validations.rbs +47 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b571c05cd70b8d5f5d3cbf40de3d06158b8b1ddfa9d0a7bd1edb4a158cd05813
|
|
4
|
+
data.tar.gz: 4eeb5375b4e0d1a80feb1c0185084858e2932591c61cef9fef5ee10a1acbad42
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fe8a1dee53c763112c935211fd941d746871cf28ef2a16499cabc71625e9a8219279fb354887d9d7f8bc80d13537cd123eb19a29feb6424351ffa720207b36b0
|
|
7
|
+
data.tar.gz: b03d0e6cd37482e1ddc578788e3d46e191a73a53e5e091a34ee80ccf88bd1258aa2f29f3b5544fa9252309042f6060c9f630ab820087f29b29d9a7d786dca1fa
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.3] - 2026-04-09
|
|
4
|
+
|
|
5
|
+
## What's Changed
|
|
6
|
+
* Enhance i18n support for nested attributes and update documentation by @Syati in https://github.com/Syati/structured_params/pull/15
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
**Full Changelog**: https://github.com/Syati/structured_params/compare/v0.9.2...v0.9.3
|
|
10
|
+
|
|
11
|
+
## [0.9.2] - 2026-04-02
|
|
12
|
+
|
|
13
|
+
## What's Changed
|
|
14
|
+
* Clarify gemspec summary to describe type-safe parameter validation by @Syati in https://github.com/Syati/structured_params/pull/13
|
|
15
|
+
* Addressing PR comments by @Syati in https://github.com/Syati/structured_params/pull/14
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
**Full Changelog**: https://github.com/Syati/structured_params/compare/v0.9.1...v0.9.2
|
|
19
|
+
|
|
3
20
|
## [0.9.1] - 2026-03-18
|
|
4
21
|
|
|
5
22
|
**Full Changelog**: https://github.com/Syati/structured_params/compare/v0.9.0...v0.9.1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module StructuredParams
|
|
5
|
+
# Extends ActiveModel::Attributes to define +attr_before_type_cast+ accessors
|
|
6
|
+
# for each attribute, mirroring ActiveRecord::AttributeMethods::BeforeTypeCast.
|
|
7
|
+
#
|
|
8
|
+
# Example:
|
|
9
|
+
# class UserParams < StructuredParams::Params
|
|
10
|
+
# attribute :age, :integer
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# params = UserParams.new(age: "42abc")
|
|
14
|
+
# params.age # => 42 (type-cast)
|
|
15
|
+
# params.age_before_type_cast # => "42abc" (raw input)
|
|
16
|
+
module AttributeMethods
|
|
17
|
+
extend ActiveSupport::Concern
|
|
18
|
+
|
|
19
|
+
included do
|
|
20
|
+
# Override attribute to also define `attr_before_type_cast`
|
|
21
|
+
# via ActiveModel::Attribute#value_before_type_cast
|
|
22
|
+
#: (Symbol name, *untyped) -> void
|
|
23
|
+
def self.attribute(name, ...)
|
|
24
|
+
super
|
|
25
|
+
define_method(:"#{name}_before_type_cast") { @attributes[name.to_s].value_before_type_cast }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module StructuredParams
|
|
5
|
+
# Provides i18n-aware human_attribute_name resolution for nested dot-notation
|
|
6
|
+
# attributes (e.g. "hobbies.0.name").
|
|
7
|
+
#
|
|
8
|
+
# When included in a Params subclass, overrides +human_attribute_name+ so that
|
|
9
|
+
# each segment of the path is resolved by the corresponding nested model class,
|
|
10
|
+
# ensuring that child-model translations are respected instead of falling back
|
|
11
|
+
# to the parent model's i18n context.
|
|
12
|
+
#
|
|
13
|
+
# == i18n keys
|
|
14
|
+
#
|
|
15
|
+
# You can customize how array indices and object nesting are rendered by
|
|
16
|
+
# defining the following keys in your locale file:
|
|
17
|
+
#
|
|
18
|
+
# ja:
|
|
19
|
+
# activemodel:
|
|
20
|
+
# errors:
|
|
21
|
+
# nested_attribute:
|
|
22
|
+
# array: "%{parent} %{index} 番目の%{child}"
|
|
23
|
+
# object: "%{parent}の%{child}"
|
|
24
|
+
#
|
|
25
|
+
# Without these keys the defaults are:
|
|
26
|
+
# array → "<parent> <index> <child>" (e.g. "Hobbies 0 Name")
|
|
27
|
+
# object → "<parent> <child>" (e.g. "Address Postal code")
|
|
28
|
+
module I18n
|
|
29
|
+
extend ActiveSupport::Concern
|
|
30
|
+
|
|
31
|
+
class_methods do # rubocop:disable Metrics/BlockLength
|
|
32
|
+
# Override human_attribute_name to resolve nested dot-notation paths.
|
|
33
|
+
#
|
|
34
|
+
# Flat attributes (no dot) are delegated to the default ActiveModel
|
|
35
|
+
# behaviour unchanged.
|
|
36
|
+
#
|
|
37
|
+
# Example (en default):
|
|
38
|
+
# human_attribute_name(:'hobbies.0.name') # => "Hobbies 0 Name"
|
|
39
|
+
#
|
|
40
|
+
# Example with i18n (ja):
|
|
41
|
+
# human_attribute_name(:'hobbies.0.name') # => "趣味 0 番目の名前"
|
|
42
|
+
#
|
|
43
|
+
#: (Symbol | String, ?Hash[untyped, untyped]) -> String
|
|
44
|
+
def human_attribute_name(attribute, options = {})
|
|
45
|
+
parts = attribute.to_s.split('.')
|
|
46
|
+
return super if parts.length == 1
|
|
47
|
+
return super unless structured_attributes.key?(parts.first)
|
|
48
|
+
|
|
49
|
+
resolve_nested_human_attribute_name(parts, options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Walk +parts+ (e.g. ["hobbies", "0", "name"]) and build a human-readable
|
|
55
|
+
# label by delegating each segment to the appropriate nested class.
|
|
56
|
+
#
|
|
57
|
+
# Only +:locale+ is forwarded to inner +human_attribute_name+ calls.
|
|
58
|
+
# Options such as +:default+ are specific to the outer call (e.g. from
|
|
59
|
+
# +full_messages+) and must not bleed into individual segment lookups,
|
|
60
|
+
# where they would replace the segment's own translation fallback.
|
|
61
|
+
#
|
|
62
|
+
#: (Array[String], Hash[untyped, untyped]) -> String
|
|
63
|
+
def resolve_nested_human_attribute_name(parts, options)
|
|
64
|
+
label = nil
|
|
65
|
+
klass = self
|
|
66
|
+
inner_opts = options.slice(:locale)
|
|
67
|
+
|
|
68
|
+
attr_segments(parts).each do |index, attr|
|
|
69
|
+
human = klass&.human_attribute_name(attr, inner_opts) || attr.humanize
|
|
70
|
+
label = build_nested_label(label, index, human, options)
|
|
71
|
+
klass &&= klass.structured_attributes[attr]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
label || parts.last.humanize
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Convert a parts array into (index_or_nil, attr) pairs.
|
|
78
|
+
#
|
|
79
|
+
# attr_segments(["hobbies", "0", "name"]) #=> [[nil, "hobbies"], ["0", "name"]]
|
|
80
|
+
# attr_segments(["address", "postal_code"]) #=> [[nil, "address"], [nil, "postal_code"]]
|
|
81
|
+
#
|
|
82
|
+
#: (Array[String]) -> Array[[String?, String]]
|
|
83
|
+
def attr_segments(parts)
|
|
84
|
+
index = nil
|
|
85
|
+
parts.each_with_object([]) do |part, segments|
|
|
86
|
+
if part.match?(/\A\d+\z/)
|
|
87
|
+
index = part
|
|
88
|
+
else
|
|
89
|
+
segments << [index, part]
|
|
90
|
+
index = nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Combine +result+ (accumulated label so far), an optional array +index+,
|
|
96
|
+
# and the new +attr_human+ into a single label string.
|
|
97
|
+
#
|
|
98
|
+
# Uses the i18n keys:
|
|
99
|
+
# activemodel.errors.nested_attribute.array (parent, index, child)
|
|
100
|
+
# activemodel.errors.nested_attribute.object (parent, child)
|
|
101
|
+
#
|
|
102
|
+
# The +locale:+ key from +options+ is forwarded to ::I18n.t so that an
|
|
103
|
+
# explicit locale passed to human_attribute_name is honoured.
|
|
104
|
+
#
|
|
105
|
+
#: (String?, String?, String, Hash[untyped, untyped]) -> String
|
|
106
|
+
def build_nested_label(result, index, attr_human, options)
|
|
107
|
+
return attr_human if result.nil?
|
|
108
|
+
|
|
109
|
+
i18n_opts = options.slice(:locale)
|
|
110
|
+
|
|
111
|
+
if index
|
|
112
|
+
::I18n.t(
|
|
113
|
+
'activemodel.errors.nested_attribute.array',
|
|
114
|
+
parent: result,
|
|
115
|
+
index: index,
|
|
116
|
+
child: attr_human,
|
|
117
|
+
default: "#{result} #{index} #{attr_human}",
|
|
118
|
+
**i18n_opts
|
|
119
|
+
)
|
|
120
|
+
else
|
|
121
|
+
::I18n.t(
|
|
122
|
+
'activemodel.errors.nested_attribute.object',
|
|
123
|
+
parent: result,
|
|
124
|
+
child: attr_human,
|
|
125
|
+
default: "#{result} #{attr_human}",
|
|
126
|
+
**i18n_opts
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -11,9 +11,13 @@ module StructuredParams
|
|
|
11
11
|
# Strong Parameters example (API):
|
|
12
12
|
# class UserParams < StructuredParams::Params
|
|
13
13
|
# attribute :name, :string
|
|
14
|
+
# attribute :age, :integer
|
|
14
15
|
# attribute :address, :object, value_class: AddressParams
|
|
15
16
|
# attribute :hobbies, :array, value_class: HobbyParams
|
|
16
17
|
# attribute :tags, :array, value_type: :string
|
|
18
|
+
#
|
|
19
|
+
# # Validate raw input before type casting (e.g., "12x" for integer fields)
|
|
20
|
+
# validates_raw :age, format: { with: /\A\d+\z/, message: 'must be numeric string' }
|
|
17
21
|
# end
|
|
18
22
|
#
|
|
19
23
|
# # In controller:
|
|
@@ -49,6 +53,9 @@ module StructuredParams
|
|
|
49
53
|
class Params
|
|
50
54
|
include ActiveModel::Model
|
|
51
55
|
include ActiveModel::Attributes
|
|
56
|
+
include AttributeMethods
|
|
57
|
+
include Validations
|
|
58
|
+
include I18n
|
|
52
59
|
|
|
53
60
|
# @rbs @errors: ::StructuredParams::Errors?
|
|
54
61
|
|
|
@@ -284,9 +291,11 @@ module StructuredParams
|
|
|
284
291
|
#: (untyped, String) -> void
|
|
285
292
|
def import_structured_errors(structured_errors, prefix)
|
|
286
293
|
structured_errors.each do |error|
|
|
287
|
-
# Create dotted attribute path and import
|
|
294
|
+
# Create dotted attribute path and import with the message already resolved
|
|
295
|
+
# in the child model's i18n context, so the parent model's locale does not
|
|
296
|
+
# override the child's translations.
|
|
288
297
|
error_attribute = "#{prefix}.#{error.attribute}"
|
|
289
|
-
errors.import(error, attribute: error_attribute.to_sym)
|
|
298
|
+
errors.import(error, attribute: error_attribute.to_sym, message: error.message)
|
|
290
299
|
end
|
|
291
300
|
end
|
|
292
301
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module StructuredParams
|
|
5
|
+
# Provides +validates_raw+ which validates raw parameter values before type casting.
|
|
6
|
+
#
|
|
7
|
+
# Internally delegates to ActiveModel's +validates+ on the +_before_type_cast+
|
|
8
|
+
# attribute, then remaps errors back to the original attribute name.
|
|
9
|
+
# This means all standard ActiveModel validators (format, numericality, etc.)
|
|
10
|
+
# work as-is on the raw input value.
|
|
11
|
+
#
|
|
12
|
+
# Example:
|
|
13
|
+
# class UserParams < StructuredParams::Params
|
|
14
|
+
# attribute :age, :integer
|
|
15
|
+
# validates_raw :age, format: { with: /\A\d+\z/, message: 'must be numeric string' }
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# params = UserParams.new(age: "abc")
|
|
19
|
+
# params.valid? # => false
|
|
20
|
+
# params.errors[:age] # => ["must be numeric string"]
|
|
21
|
+
module Validations
|
|
22
|
+
extend ActiveSupport::Concern
|
|
23
|
+
|
|
24
|
+
included do
|
|
25
|
+
class_attribute :validates_raw_btc_map, instance_accessor: false, default: {}
|
|
26
|
+
class_attribute :validates_raw_remap_validator_installed, instance_accessor: false, default: false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @rbs module ClassMethods
|
|
30
|
+
class_methods do
|
|
31
|
+
# Validates raw attribute value before type casting.
|
|
32
|
+
#
|
|
33
|
+
# Accepts the same options as +validates+ (format, numericality, presence, etc.),
|
|
34
|
+
# but validates the raw input value before it is converted by ActiveModel::Attributes.
|
|
35
|
+
#
|
|
36
|
+
# Examples:
|
|
37
|
+
# validates_raw :age, format: { with: /\A\d+\z/ }
|
|
38
|
+
# validates_raw :score, numericality: { only_integer: true }
|
|
39
|
+
# validates_raw :code, format: { with: /\A[A-Z]+\z/, message: 'must be uppercase' }
|
|
40
|
+
#
|
|
41
|
+
#: (*Symbol, **untyped) -> void
|
|
42
|
+
def validates_raw(*attr_names, **options)
|
|
43
|
+
btc_map = attr_names.to_h { |attr| [attr.to_sym, :"#{attr}_before_type_cast"] }
|
|
44
|
+
validates(*btc_map.values, **options)
|
|
45
|
+
self.validates_raw_btc_map = validates_raw_btc_map.merge(btc_map)
|
|
46
|
+
validates_raw_install_remap_validator_once
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
#: () -> void
|
|
50
|
+
def validates_raw_install_remap_validator_once
|
|
51
|
+
return if validates_raw_remap_validator_installed
|
|
52
|
+
|
|
53
|
+
set_callback(:validate, :after, :validates_raw_remap_errors)
|
|
54
|
+
|
|
55
|
+
self.validates_raw_remap_validator_installed = true
|
|
56
|
+
end
|
|
57
|
+
private :validates_raw_install_remap_validator_once
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
#: () -> void
|
|
63
|
+
def validates_raw_remap_errors
|
|
64
|
+
self.class.validates_raw_btc_map.each do |attr, btc|
|
|
65
|
+
next if errors.where(btc).none?
|
|
66
|
+
|
|
67
|
+
errors.where(btc).dup.each { |e| errors.import(e, attribute: attr) }
|
|
68
|
+
errors.delete(btc)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/structured_params.rb
CHANGED
|
@@ -10,6 +10,9 @@ require_relative 'structured_params/version'
|
|
|
10
10
|
|
|
11
11
|
# errors
|
|
12
12
|
require_relative 'structured_params/errors'
|
|
13
|
+
require_relative 'structured_params/attribute_methods'
|
|
14
|
+
require_relative 'structured_params/validations'
|
|
15
|
+
require_relative 'structured_params/i18n'
|
|
13
16
|
|
|
14
17
|
# types (load first for module definition)
|
|
15
18
|
require_relative 'structured_params/type/object'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Generated from lib/structured_params/attribute_methods.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module StructuredParams
|
|
4
|
+
# Extends ActiveModel::Attributes to define +attr_before_type_cast+ accessors
|
|
5
|
+
# for each attribute, mirroring ActiveRecord::AttributeMethods::BeforeTypeCast.
|
|
6
|
+
#
|
|
7
|
+
# Example:
|
|
8
|
+
# class UserParams < StructuredParams::Params
|
|
9
|
+
# attribute :age, :integer
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# params = UserParams.new(age: "42abc")
|
|
13
|
+
# params.age # => 42 (type-cast)
|
|
14
|
+
# params.age_before_type_cast # => "42abc" (raw input)
|
|
15
|
+
module AttributeMethods
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
|
|
18
|
+
# Override attribute to also define `attr_before_type_cast`
|
|
19
|
+
# via ActiveModel::Attribute#value_before_type_cast
|
|
20
|
+
# : (Symbol name, *untyped) -> void
|
|
21
|
+
def self.attribute: (Symbol name, *untyped) -> void
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Generated from lib/structured_params/i18n.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module StructuredParams
|
|
4
|
+
# Provides i18n-aware human_attribute_name resolution for nested dot-notation
|
|
5
|
+
# attributes (e.g. "hobbies.0.name").
|
|
6
|
+
#
|
|
7
|
+
# When included in a Params subclass, overrides +human_attribute_name+ so that
|
|
8
|
+
# each segment of the path is resolved by the corresponding nested model class,
|
|
9
|
+
# ensuring that child-model translations are respected instead of falling back
|
|
10
|
+
# to the parent model's i18n context.
|
|
11
|
+
#
|
|
12
|
+
# == i18n keys
|
|
13
|
+
#
|
|
14
|
+
# You can customize how array indices and object nesting are rendered by
|
|
15
|
+
# defining the following keys in your locale file:
|
|
16
|
+
#
|
|
17
|
+
# ja:
|
|
18
|
+
# activemodel:
|
|
19
|
+
# errors:
|
|
20
|
+
# nested_attribute:
|
|
21
|
+
# array: "%{parent} %{index} 番目の%{child}"
|
|
22
|
+
# object: "%{parent}の%{child}"
|
|
23
|
+
#
|
|
24
|
+
# Without these keys the defaults are:
|
|
25
|
+
# array → "<parent> <index> <child>" (e.g. "Hobbies 0 Name")
|
|
26
|
+
# object → "<parent> <child>" (e.g. "Address Postal code")
|
|
27
|
+
module I18n
|
|
28
|
+
extend ActiveSupport::Concern
|
|
29
|
+
|
|
30
|
+
# Override human_attribute_name to resolve nested dot-notation paths.
|
|
31
|
+
#
|
|
32
|
+
# Flat attributes (no dot) are delegated to the default ActiveModel
|
|
33
|
+
# behaviour unchanged.
|
|
34
|
+
#
|
|
35
|
+
# Example (en default):
|
|
36
|
+
# human_attribute_name(:'hobbies.0.name') # => "Hobbies 0 Name"
|
|
37
|
+
#
|
|
38
|
+
# Example with i18n (ja):
|
|
39
|
+
# human_attribute_name(:'hobbies.0.name') # => "趣味 0 番目の名前"
|
|
40
|
+
#
|
|
41
|
+
# : (Symbol | String, ?Hash[untyped, untyped]) -> String
|
|
42
|
+
def human_attribute_name: (Symbol | String, ?Hash[untyped, untyped]) -> String
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Walk +parts+ (e.g. ["hobbies", "0", "name"]) and build a human-readable
|
|
47
|
+
# label by delegating each segment to the appropriate nested class.
|
|
48
|
+
#
|
|
49
|
+
# Only +:locale+ is forwarded to inner +human_attribute_name+ calls.
|
|
50
|
+
# Options such as +:default+ are specific to the outer call (e.g. from
|
|
51
|
+
# +full_messages+) and must not bleed into individual segment lookups,
|
|
52
|
+
# where they would replace the segment's own translation fallback.
|
|
53
|
+
#
|
|
54
|
+
# : (Array[String], Hash[untyped, untyped]) -> String
|
|
55
|
+
def resolve_nested_human_attribute_name: (Array[String], Hash[untyped, untyped]) -> String
|
|
56
|
+
|
|
57
|
+
# Convert a parts array into (index_or_nil, attr) pairs.
|
|
58
|
+
#
|
|
59
|
+
# attr_segments(["hobbies", "0", "name"]) #=> [[nil, "hobbies"], ["0", "name"]]
|
|
60
|
+
# attr_segments(["address", "postal_code"]) #=> [[nil, "address"], [nil, "postal_code"]]
|
|
61
|
+
#
|
|
62
|
+
# : (Array[String]) -> Array[[String?, String]]
|
|
63
|
+
def attr_segments: (Array[String]) -> Array[[ String?, String ]]
|
|
64
|
+
|
|
65
|
+
# Combine +result+ (accumulated label so far), an optional array +index+,
|
|
66
|
+
# and the new +attr_human+ into a single label string.
|
|
67
|
+
#
|
|
68
|
+
# Uses the i18n keys:
|
|
69
|
+
# activemodel.errors.nested_attribute.array (parent, index, child)
|
|
70
|
+
# activemodel.errors.nested_attribute.object (parent, child)
|
|
71
|
+
#
|
|
72
|
+
# The +locale:+ key from +options+ is forwarded to ::I18n.t so that an
|
|
73
|
+
# explicit locale passed to human_attribute_name is honoured.
|
|
74
|
+
#
|
|
75
|
+
# : (String?, String?, String, Hash[untyped, untyped]) -> String
|
|
76
|
+
def build_nested_label: (String?, String?, String, Hash[untyped, untyped]) -> String
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -10,9 +10,13 @@ module StructuredParams
|
|
|
10
10
|
# Strong Parameters example (API):
|
|
11
11
|
# class UserParams < StructuredParams::Params
|
|
12
12
|
# attribute :name, :string
|
|
13
|
+
# attribute :age, :integer
|
|
13
14
|
# attribute :address, :object, value_class: AddressParams
|
|
14
15
|
# attribute :hobbies, :array, value_class: HobbyParams
|
|
15
16
|
# attribute :tags, :array, value_type: :string
|
|
17
|
+
#
|
|
18
|
+
# # Validate raw input before type casting (e.g., "12x" for integer fields)
|
|
19
|
+
# validates_raw :age, format: { with: /\A\d+\z/, message: 'must be numeric string' }
|
|
16
20
|
# end
|
|
17
21
|
#
|
|
18
22
|
# # In controller:
|
|
@@ -50,6 +54,12 @@ module StructuredParams
|
|
|
50
54
|
|
|
51
55
|
include ActiveModel::Attributes
|
|
52
56
|
|
|
57
|
+
include AttributeMethods
|
|
58
|
+
|
|
59
|
+
include Validations
|
|
60
|
+
|
|
61
|
+
include I18n
|
|
62
|
+
|
|
53
63
|
@errors: ::StructuredParams::Errors?
|
|
54
64
|
|
|
55
65
|
self.@structured_attributes: Hash[Symbol, singleton(::StructuredParams::Params)]?
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Generated from lib/structured_params/validations.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module StructuredParams
|
|
4
|
+
# Provides +validates_raw+ which validates raw parameter values before type casting.
|
|
5
|
+
#
|
|
6
|
+
# Internally delegates to ActiveModel's +validates+ on the +_before_type_cast+
|
|
7
|
+
# attribute, then remaps errors back to the original attribute name.
|
|
8
|
+
# This means all standard ActiveModel validators (format, numericality, etc.)
|
|
9
|
+
# work as-is on the raw input value.
|
|
10
|
+
#
|
|
11
|
+
# Example:
|
|
12
|
+
# class UserParams < StructuredParams::Params
|
|
13
|
+
# attribute :age, :integer
|
|
14
|
+
# validates_raw :age, format: { with: /\A\d+\z/, message: 'must be numeric string' }
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# params = UserParams.new(age: "abc")
|
|
18
|
+
# params.valid? # => false
|
|
19
|
+
# params.errors[:age] # => ["must be numeric string"]
|
|
20
|
+
module Validations
|
|
21
|
+
extend ActiveSupport::Concern
|
|
22
|
+
|
|
23
|
+
# @rbs module ClassMethods
|
|
24
|
+
module ClassMethods
|
|
25
|
+
# Validates raw attribute value before type casting.
|
|
26
|
+
#
|
|
27
|
+
# Accepts the same options as +validates+ (format, numericality, presence, etc.),
|
|
28
|
+
# but validates the raw input value before it is converted by ActiveModel::Attributes.
|
|
29
|
+
#
|
|
30
|
+
# Examples:
|
|
31
|
+
# validates_raw :age, format: { with: /\A\d+\z/ }
|
|
32
|
+
# validates_raw :score, numericality: { only_integer: true }
|
|
33
|
+
# validates_raw :code, format: { with: /\A[A-Z]+\z/, message: 'must be uppercase' }
|
|
34
|
+
#
|
|
35
|
+
# : (*Symbol, **untyped) -> void
|
|
36
|
+
def validates_raw: (*Symbol, **untyped) -> void
|
|
37
|
+
|
|
38
|
+
# : () -> void
|
|
39
|
+
def validates_raw_install_remap_validator_once: () -> void
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# : () -> void
|
|
45
|
+
def validates_raw_remap_errors: () -> void
|
|
46
|
+
end
|
|
47
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: structured_params
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mizuki Yamamoto
|
|
@@ -59,16 +59,22 @@ files:
|
|
|
59
59
|
- CHANGELOG.md
|
|
60
60
|
- LICENSE.txt
|
|
61
61
|
- lib/structured_params.rb
|
|
62
|
+
- lib/structured_params/attribute_methods.rb
|
|
62
63
|
- lib/structured_params/errors.rb
|
|
64
|
+
- lib/structured_params/i18n.rb
|
|
63
65
|
- lib/structured_params/params.rb
|
|
64
66
|
- lib/structured_params/type/array.rb
|
|
65
67
|
- lib/structured_params/type/object.rb
|
|
68
|
+
- lib/structured_params/validations.rb
|
|
66
69
|
- lib/structured_params/version.rb
|
|
67
70
|
- sig/structured_params.rbs
|
|
71
|
+
- sig/structured_params/attribute_methods.rbs
|
|
68
72
|
- sig/structured_params/errors.rbs
|
|
73
|
+
- sig/structured_params/i18n.rbs
|
|
69
74
|
- sig/structured_params/params.rbs
|
|
70
75
|
- sig/structured_params/type/array.rbs
|
|
71
76
|
- sig/structured_params/type/object.rbs
|
|
77
|
+
- sig/structured_params/validations.rbs
|
|
72
78
|
- sig/structured_params/version.rbs
|
|
73
79
|
homepage: https://github.com/Syati/structured_params
|
|
74
80
|
licenses:
|
|
@@ -95,5 +101,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
95
101
|
requirements: []
|
|
96
102
|
rubygems_version: 3.6.9
|
|
97
103
|
specification_version: 4
|
|
98
|
-
summary:
|
|
104
|
+
summary: Type-safe parameter validation and form objects for Rails.
|
|
99
105
|
test_files: []
|