structured_params 0.9.2 → 0.9.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99bd6bd346b7e55ee390e747bb9ff232aa84af52e0198b48d162b42e7caed5f8
4
- data.tar.gz: b4746d75c8201e0751e1d413155697a2f23da1d0da667491749838db0a6c0c19
3
+ metadata.gz: 60e548d315d109b5010333626042b9b011e867ce533c38de971201e2c9d2d87a
4
+ data.tar.gz: d2606fcd8176f4dc916af2bf879b48545508032c4752343caf9db7f0789bb7c5
5
5
  SHA512:
6
- metadata.gz: 2aef79c11cbb97312962a39869726aa7ce95ef1e1b3f29974d3d8bc1d1637487ef7cb85dbe47742866db1db9572e83999bb52095fe634247c1170cafd0ff5433
7
- data.tar.gz: b2a6bce45a08f4521979084f2ecd6b20385b7aeec3794ae2cfb8bd57d6888e87ffdae4dae10a4887b07e47417afc80d256235e25bc8ce5c9c96a3316391f81ab
6
+ metadata.gz: e9c3970818c47a021c522d12ca409b56730d580961fb79dc6cc311da9756bdec5b971fcc4e7cc9d8a2dd611b363aa5977356e074fdf1f7bc162bfddc04287692
7
+ data.tar.gz: a7930657984c4554dc7b172a7d9982a6c40cfe128053efbd4afaae2a41e8839211e063b6be3b1b5022390bf70825251633c3feaf7e955363329a8f32b1e6722d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.4] - 2026-05-02
4
+
5
+ ## What's Changed
6
+ * Enhance array index handling and update RBS configuration by @Syati in https://github.com/Syati/structured_params/pull/16
7
+
8
+
9
+ **Full Changelog**: https://github.com/Syati/structured_params/compare/v0.9.3...v0.9.4
10
+
11
+ ## [0.9.3] - 2026-04-09
12
+
13
+ ## What's Changed
14
+ * Enhance i18n support for nested attributes and update documentation by @Syati in https://github.com/Syati/structured_params/pull/15
15
+
16
+
17
+ **Full Changelog**: https://github.com/Syati/structured_params/compare/v0.9.2...v0.9.3
18
+
3
19
  ## [0.9.2] - 2026-04-02
4
20
 
5
21
  ## What's Changed
@@ -0,0 +1,141 @@
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 (with array_index_base: 0):
26
+ # array → "<parent> <index> <child>" (e.g. "Hobbies 0 Name")
27
+ # object → "<parent> <child>" (e.g. "Address Postal code")
28
+ #
29
+ # With array_index_base: 1 (human-friendly):
30
+ # array → "Hobbies 1 Name"
31
+ module I18n
32
+ extend ActiveSupport::Concern
33
+
34
+ class_methods do # rubocop:disable Metrics/BlockLength
35
+ # Override human_attribute_name to resolve nested dot-notation paths.
36
+ #
37
+ # Flat attributes (no dot) are delegated to the default ActiveModel
38
+ # behaviour unchanged.
39
+ #
40
+ # Example (en default, array_index_base: 0):
41
+ # human_attribute_name(:'hobbies.0.name') # => "Hobbies 0 Name"
42
+ #
43
+ # Example with i18n (ja) and array_index_base: 1:
44
+ # human_attribute_name(:'hobbies.0.name') # => "趣味 1 番目の名前"
45
+ #
46
+ #: (Symbol | String, ?Hash[untyped, untyped]) -> String
47
+ def human_attribute_name(attribute, options = {})
48
+ parts = attribute.to_s.split('.')
49
+ return super if parts.length == 1
50
+ return super unless structured_attributes.key?(parts.first)
51
+
52
+ resolve_nested_human_attribute_name(parts, options)
53
+ end
54
+
55
+ private
56
+
57
+ # Walk +parts+ (e.g. ["hobbies", "0", "name"]) and build a human-readable
58
+ # label by delegating each segment to the appropriate nested class.
59
+ #
60
+ # Only +:locale+ is forwarded to inner +human_attribute_name+ calls.
61
+ # Options such as +:default+ are specific to the outer call (e.g. from
62
+ # +full_messages+) and must not bleed into individual segment lookups,
63
+ # where they would replace the segment's own translation fallback.
64
+ #
65
+ #: (Array[String], Hash[untyped, untyped]) -> String
66
+ def resolve_nested_human_attribute_name(parts, options)
67
+ label = nil
68
+ klass = self
69
+ inner_opts = options.slice(:locale)
70
+
71
+ attr_segments(parts).each do |index, attr|
72
+ human = klass&.human_attribute_name(attr, inner_opts) || attr.humanize
73
+ label = build_nested_label(label, index, human, options)
74
+ klass &&= klass.structured_attributes[attr]
75
+ end
76
+
77
+ label || parts.last.humanize
78
+ end
79
+
80
+ # Convert a parts array into (index_or_nil, attr) pairs.
81
+ #
82
+ # attr_segments(["hobbies", "0", "name"]) #=> [[nil, "hobbies"], ["0", "name"]]
83
+ # attr_segments(["address", "postal_code"]) #=> [[nil, "address"], [nil, "postal_code"]]
84
+ #
85
+ #: (Array[String]) -> Array[[String?, String]]
86
+ def attr_segments(parts)
87
+ index = nil
88
+ parts.each_with_object([]) do |part, segments|
89
+ if part.match?(/\A\d+\z/)
90
+ index = part
91
+ else
92
+ segments << [index, part]
93
+ index = nil
94
+ end
95
+ end
96
+ end
97
+
98
+ # Combine +result+ (accumulated label so far), an optional array +index+,
99
+ # and the new +attr_human+ into a single label string.
100
+ #
101
+ # Uses the i18n keys:
102
+ # activemodel.errors.nested_attribute.array (parent, index, child)
103
+ # activemodel.errors.nested_attribute.object (parent, child)
104
+ #
105
+ # The index value passed to the i18n template is adjusted by
106
+ # +StructuredParams.configuration.array_index_base+:
107
+ # * +0+ (default) – raw 0-based Ruby index (e.g. 0, 1, 2, …)
108
+ # * +1+ – human-friendly 1-based index (e.g. 1, 2, 3, …)
109
+ #
110
+ # The +locale:+ key from +options+ is forwarded to ::I18n.t so that an
111
+ # explicit locale passed to human_attribute_name is honoured.
112
+ #
113
+ #: (String?, String?, String, Hash[untyped, untyped]) -> String
114
+ def build_nested_label(result, index, attr_human, options)
115
+ return attr_human if result.nil?
116
+
117
+ i18n_opts = options.slice(:locale)
118
+
119
+ if index
120
+ display_index = index.to_i + StructuredParams.configuration.array_index_base
121
+ ::I18n.t(
122
+ 'activemodel.errors.nested_attribute.array',
123
+ parent: result,
124
+ index: display_index,
125
+ child: attr_human,
126
+ default: "#{result} #{display_index} #{attr_human}",
127
+ **i18n_opts
128
+ )
129
+ else
130
+ ::I18n.t(
131
+ 'activemodel.errors.nested_attribute.object',
132
+ parent: result,
133
+ child: attr_human,
134
+ default: "#{result} #{attr_human}",
135
+ **i18n_opts
136
+ )
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -55,6 +55,7 @@ module StructuredParams
55
55
  include ActiveModel::Attributes
56
56
  include AttributeMethods
57
57
  include Validations
58
+ include I18n
58
59
 
59
60
  # @rbs @errors: ::StructuredParams::Errors?
60
61
 
@@ -290,9 +291,11 @@ module StructuredParams
290
291
  #: (untyped, String) -> void
291
292
  def import_structured_errors(structured_errors, prefix)
292
293
  structured_errors.each do |error|
293
- # Create dotted attribute path and import normally
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.
294
297
  error_attribute = "#{prefix}.#{error.attribute}"
295
- errors.import(error, attribute: error_attribute.to_sym)
298
+ errors.import(error, attribute: error_attribute.to_sym, message: error.message)
296
299
  end
297
300
  end
298
301
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module StructuredParams
5
- VERSION = '0.9.2' #: string
5
+ VERSION = '0.9.4' #: string
6
6
  end
@@ -12,6 +12,7 @@ require_relative 'structured_params/version'
12
12
  require_relative 'structured_params/errors'
13
13
  require_relative 'structured_params/attribute_methods'
14
14
  require_relative 'structured_params/validations'
15
+ require_relative 'structured_params/i18n'
15
16
 
16
17
  # types (load first for module definition)
17
18
  require_relative 'structured_params/type/object'
@@ -22,17 +23,75 @@ require_relative 'structured_params/params'
22
23
 
23
24
  # Main module
24
25
  module StructuredParams
25
- # Helper method to register types
26
- #: () -> void
27
- def self.register_types
28
- ActiveModel::Type.register(:object, StructuredParams::Type::Object)
29
- ActiveModel::Type.register(:array, StructuredParams::Type::Array)
26
+ # Global configuration for StructuredParams.
27
+ #
28
+ # == Options
29
+ #
30
+ # +array_index_base+ (Integer, default: +0+)::
31
+ # Controls how array indices are displayed in human attribute names and
32
+ # error messages.
33
+ #
34
+ # * +0+ – 0-based (raw Ruby index): "Hobbies 0 Name"
35
+ # * +1+ – 1-based (human-friendly): "Hobbies 1 Name"
36
+ #
37
+ # This setting applies to both API param error messages and Form Object
38
+ # +full_messages+.
39
+ #
40
+ # == Example
41
+ #
42
+ # # config/initializers/structured_params.rb
43
+ # StructuredParams.configure do |config|
44
+ # config.array_index_base = 1 # show "1st" instead of "0th" to users
45
+ # end
46
+ #
47
+ class Configuration
48
+ attr_reader :array_index_base #: Integer
49
+
50
+ #: () -> void
51
+ def initialize
52
+ @array_index_base = 0
53
+ end
54
+
55
+ #: (Integer) -> void
56
+ def array_index_base=(value)
57
+ unless value.is_a?(Integer) && [0, 1].include?(value)
58
+ raise ArgumentError, "array_index_base must be 0 or 1, got: #{value.inspect}"
59
+ end
60
+
61
+ @array_index_base = value
62
+ end
30
63
  end
31
64
 
32
- # Helper method to register types with custom names
33
- #: (object_name: Symbol, array_name: Symbol) -> void
34
- def self.register_types_as(object_name:, array_name:)
35
- ActiveModel::Type.register(object_name, StructuredParams::Type::Object)
36
- ActiveModel::Type.register(array_name, StructuredParams::Type::Array)
65
+ class << self
66
+ # @rbs self.@configuration: Configuration?
67
+
68
+ #: () -> Configuration
69
+ def configuration
70
+ @configuration ||= Configuration.new
71
+ end
72
+
73
+ #: () { (Configuration) -> void } -> void
74
+ def configure
75
+ yield configuration
76
+ end
77
+
78
+ #: () -> void
79
+ def reset_configuration!
80
+ @configuration = Configuration.new
81
+ end
82
+
83
+ # Helper method to register types
84
+ #: () -> void
85
+ def register_types
86
+ ActiveModel::Type.register(:object, StructuredParams::Type::Object)
87
+ ActiveModel::Type.register(:array, StructuredParams::Type::Array)
88
+ end
89
+
90
+ # Helper method to register types with custom names
91
+ #: (object_name: Symbol, array_name: Symbol) -> void
92
+ def register_types_as(object_name:, array_name:)
93
+ ActiveModel::Type.register(object_name, StructuredParams::Type::Object)
94
+ ActiveModel::Type.register(array_name, StructuredParams::Type::Array)
95
+ end
37
96
  end
38
97
  end
@@ -0,0 +1,86 @@
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 (with array_index_base: 0):
25
+ # array → "<parent> <index> <child>" (e.g. "Hobbies 0 Name")
26
+ # object → "<parent> <child>" (e.g. "Address Postal code")
27
+ #
28
+ # With array_index_base: 1 (human-friendly):
29
+ # array → "Hobbies 1 Name"
30
+ module I18n
31
+ extend ActiveSupport::Concern
32
+
33
+ # Override human_attribute_name to resolve nested dot-notation paths.
34
+ #
35
+ # Flat attributes (no dot) are delegated to the default ActiveModel
36
+ # behaviour unchanged.
37
+ #
38
+ # Example (en default, array_index_base: 0):
39
+ # human_attribute_name(:'hobbies.0.name') # => "Hobbies 0 Name"
40
+ #
41
+ # Example with i18n (ja) and array_index_base: 1:
42
+ # human_attribute_name(:'hobbies.0.name') # => "趣味 1 番目の名前"
43
+ #
44
+ # : (Symbol | String, ?Hash[untyped, untyped]) -> String
45
+ def human_attribute_name: (Symbol | String, ?Hash[untyped, untyped]) -> String
46
+
47
+ private
48
+
49
+ # Walk +parts+ (e.g. ["hobbies", "0", "name"]) and build a human-readable
50
+ # label by delegating each segment to the appropriate nested class.
51
+ #
52
+ # Only +:locale+ is forwarded to inner +human_attribute_name+ calls.
53
+ # Options such as +:default+ are specific to the outer call (e.g. from
54
+ # +full_messages+) and must not bleed into individual segment lookups,
55
+ # where they would replace the segment's own translation fallback.
56
+ #
57
+ # : (Array[String], Hash[untyped, untyped]) -> String
58
+ def resolve_nested_human_attribute_name: (Array[String], Hash[untyped, untyped]) -> String
59
+
60
+ # Convert a parts array into (index_or_nil, attr) pairs.
61
+ #
62
+ # attr_segments(["hobbies", "0", "name"]) #=> [[nil, "hobbies"], ["0", "name"]]
63
+ # attr_segments(["address", "postal_code"]) #=> [[nil, "address"], [nil, "postal_code"]]
64
+ #
65
+ # : (Array[String]) -> Array[[String?, String]]
66
+ def attr_segments: (Array[String]) -> Array[[ String?, String ]]
67
+
68
+ # Combine +result+ (accumulated label so far), an optional array +index+,
69
+ # and the new +attr_human+ into a single label string.
70
+ #
71
+ # Uses the i18n keys:
72
+ # activemodel.errors.nested_attribute.array (parent, index, child)
73
+ # activemodel.errors.nested_attribute.object (parent, child)
74
+ #
75
+ # The index value passed to the i18n template is adjusted by
76
+ # +StructuredParams.configuration.array_index_base+:
77
+ # * +0+ (default) – raw 0-based Ruby index (e.g. 0, 1, 2, …)
78
+ # * +1+ – human-friendly 1-based index (e.g. 1, 2, 3, …)
79
+ #
80
+ # The +locale:+ key from +options+ is forwarded to ::I18n.t so that an
81
+ # explicit locale passed to human_attribute_name is honoured.
82
+ #
83
+ # : (String?, String?, String, Hash[untyped, untyped]) -> String
84
+ def build_nested_label: (String?, String?, String, Hash[untyped, untyped]) -> String
85
+ end
86
+ end
@@ -58,6 +58,8 @@ module StructuredParams
58
58
 
59
59
  include Validations
60
60
 
61
+ include I18n
62
+
61
63
  @errors: ::StructuredParams::Errors?
62
64
 
63
65
  self.@structured_attributes: Hash[Symbol, singleton(::StructuredParams::Params)]?
@@ -29,6 +29,19 @@ module StructuredParams
29
29
  def serialize: (::Array[untyped]?) -> ::Array[untyped]?
30
30
 
31
31
  # Get permitted parameter names for use with Strong Parameters
32
+ #
33
+ # Returns:
34
+ # - For object arrays (value_class): nested keys from the object
35
+ # Example: HobbyParams with [:name, :level]
36
+ # → returns [:name, :level]
37
+ # → becomes { hobbies: [:name, :level] } in Params#permit_attribute_names
38
+ #
39
+ # - For primitive arrays (value_type): empty array
40
+ # Example: attribute :tags, :array, value_type: :string
41
+ # → returns []
42
+ # → becomes { tags: [] } in Params#permit_attribute_names
43
+ # → finally used as params.permit(:name, { tags: [] })
44
+ #
32
45
  # : () -> ::Array[untyped]
33
46
  def permit_attribute_names: () -> ::Array[untyped]
34
47
 
@@ -2,6 +2,47 @@
2
2
 
3
3
  # Main module
4
4
  module StructuredParams
5
+ # Global configuration for StructuredParams.
6
+ #
7
+ # == Options
8
+ #
9
+ # +array_index_base+ (Integer, default: +0+)::
10
+ # Controls how array indices are displayed in human attribute names and
11
+ # error messages.
12
+ #
13
+ # * +0+ – 0-based (raw Ruby index): "Hobbies 0 Name"
14
+ # * +1+ – 1-based (human-friendly): "Hobbies 1 Name"
15
+ #
16
+ # This setting applies to both API param error messages and Form Object
17
+ # +full_messages+.
18
+ #
19
+ # == Example
20
+ #
21
+ # # config/initializers/structured_params.rb
22
+ # StructuredParams.configure do |config|
23
+ # config.array_index_base = 1 # show "1st" instead of "0th" to users
24
+ # end
25
+ class Configuration
26
+ attr_reader array_index_base: Integer
27
+
28
+ # : () -> void
29
+ def initialize: () -> void
30
+
31
+ # : (Integer) -> void
32
+ def array_index_base=: (Integer) -> void
33
+ end
34
+
35
+ self.@configuration: Configuration?
36
+
37
+ # : () -> Configuration
38
+ def self.configuration: () -> Configuration
39
+
40
+ # : () { (Configuration) -> void } -> void
41
+ def self.configure: () { (Configuration) -> void } -> void
42
+
43
+ # : () -> void
44
+ def self.reset_configuration!: () -> void
45
+
5
46
  # Helper method to register types
6
47
  # : () -> void
7
48
  def self.register_types: () -> void
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.2
4
+ version: 0.9.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mizuki Yamamoto
@@ -61,6 +61,7 @@ files:
61
61
  - lib/structured_params.rb
62
62
  - lib/structured_params/attribute_methods.rb
63
63
  - lib/structured_params/errors.rb
64
+ - lib/structured_params/i18n.rb
64
65
  - lib/structured_params/params.rb
65
66
  - lib/structured_params/type/array.rb
66
67
  - lib/structured_params/type/object.rb
@@ -69,6 +70,7 @@ files:
69
70
  - sig/structured_params.rbs
70
71
  - sig/structured_params/attribute_methods.rbs
71
72
  - sig/structured_params/errors.rbs
73
+ - sig/structured_params/i18n.rbs
72
74
  - sig/structured_params/params.rbs
73
75
  - sig/structured_params/type/array.rbs
74
76
  - sig/structured_params/type/object.rbs