structured_params 0.9.3 → 0.9.5

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: b571c05cd70b8d5f5d3cbf40de3d06158b8b1ddfa9d0a7bd1edb4a158cd05813
4
- data.tar.gz: 4eeb5375b4e0d1a80feb1c0185084858e2932591c61cef9fef5ee10a1acbad42
3
+ metadata.gz: 786740a05f9ea27ee5e8b4c5b05aec0c5807fbac8b5995911e94a35f385c4bf4
4
+ data.tar.gz: 13647825c70a9bdfa94f5dcc14a1ae4c6c175bfa75c14e4e89183d89ad422175
5
5
  SHA512:
6
- metadata.gz: fe8a1dee53c763112c935211fd941d746871cf28ef2a16499cabc71625e9a8219279fb354887d9d7f8bc80d13537cd123eb19a29feb6424351ffa720207b36b0
7
- data.tar.gz: b03d0e6cd37482e1ddc578788e3d46e191a73a53e5e091a34ee80ccf88bd1258aa2f29f3b5544fa9252309042f6060c9f630ab820087f29b29d9a7d786dca1fa
6
+ metadata.gz: 0a4bb67dbe0b7429b07a94c926fc5a1975642992aaee50efdb58e8d9df6c9dd4cca8e7f19e35f2bd0a91c20a23c28940cf30eea27bcfcf062ed9f44b58d35ff6
7
+ data.tar.gz: 5b22220f7d90edf6b47ccadfe27aa0d1f5724388c7ca0976093dd9546936410539102c7336b9e74b7c224ed57816798b3fb5b8c6f2286da0bedc2511e3a367d6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.5] - 2026-07-12
4
+
5
+ ## What's Changed
6
+ * Refine README and installation copy by @Syati in https://github.com/Syati/structured_params/pull/18
7
+ * Simplify form object params initialization by @Syati in https://github.com/Syati/structured_params/pull/19
8
+
9
+
10
+ **Full Changelog**: https://github.com/Syati/structured_params/compare/v0.9.4...v0.9.5
11
+
12
+ ## [0.9.4] - 2026-05-02
13
+
14
+ ## What's Changed
15
+ * Enhance array index handling and update RBS configuration by @Syati in https://github.com/Syati/structured_params/pull/16
16
+
17
+
18
+ **Full Changelog**: https://github.com/Syati/structured_params/compare/v0.9.3...v0.9.4
19
+
3
20
  ## [0.9.3] - 2026-04-09
4
21
 
5
22
  ## What's Changed
@@ -22,9 +22,12 @@ module StructuredParams
22
22
  # array: "%{parent} %{index} 番目の%{child}"
23
23
  # object: "%{parent}の%{child}"
24
24
  #
25
- # Without these keys the defaults are:
25
+ # Without these keys the defaults are (with array_index_base: 0):
26
26
  # array → "<parent> <index> <child>" (e.g. "Hobbies 0 Name")
27
27
  # object → "<parent> <child>" (e.g. "Address Postal code")
28
+ #
29
+ # With array_index_base: 1 (human-friendly):
30
+ # array → "Hobbies 1 Name"
28
31
  module I18n
29
32
  extend ActiveSupport::Concern
30
33
 
@@ -34,11 +37,11 @@ module StructuredParams
34
37
  # Flat attributes (no dot) are delegated to the default ActiveModel
35
38
  # behaviour unchanged.
36
39
  #
37
- # Example (en default):
40
+ # Example (en default, array_index_base: 0):
38
41
  # human_attribute_name(:'hobbies.0.name') # => "Hobbies 0 Name"
39
42
  #
40
- # Example with i18n (ja):
41
- # human_attribute_name(:'hobbies.0.name') # => "趣味 0 番目の名前"
43
+ # Example with i18n (ja) and array_index_base: 1:
44
+ # human_attribute_name(:'hobbies.0.name') # => "趣味 1 番目の名前"
42
45
  #
43
46
  #: (Symbol | String, ?Hash[untyped, untyped]) -> String
44
47
  def human_attribute_name(attribute, options = {})
@@ -99,6 +102,11 @@ module StructuredParams
99
102
  # activemodel.errors.nested_attribute.array (parent, index, child)
100
103
  # activemodel.errors.nested_attribute.object (parent, child)
101
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
+ #
102
110
  # The +locale:+ key from +options+ is forwarded to ::I18n.t so that an
103
111
  # explicit locale passed to human_attribute_name is honoured.
104
112
  #
@@ -109,12 +117,13 @@ module StructuredParams
109
117
  i18n_opts = options.slice(:locale)
110
118
 
111
119
  if index
120
+ display_index = index.to_i + StructuredParams.configuration.array_index_base
112
121
  ::I18n.t(
113
122
  'activemodel.errors.nested_attribute.array',
114
123
  parent: result,
115
- index: index,
124
+ index: display_index,
116
125
  child: attr_human,
117
- default: "#{result} #{index} #{attr_human}",
126
+ default: "#{result} #{display_index} #{attr_human}",
118
127
  **i18n_opts
119
128
  )
120
129
  else
@@ -37,7 +37,7 @@ module StructuredParams
37
37
  # end
38
38
  #
39
39
  # # In controller:
40
- # @form = UserRegistrationForm.new(UserRegistrationForm.permit(params))
40
+ # @form = UserRegistrationForm.new(params)
41
41
  # if @form.valid?
42
42
  # User.create!(@form.attributes)
43
43
  # redirect_to user_path
@@ -50,6 +50,7 @@ module StructuredParams
50
50
  # <%= f.text_field :name %>
51
51
  # <%= f.text_field :email %>
52
52
  # <% end %>
53
+ # rubocop:disable Metrics/ClassLength
53
54
  class Params
54
55
  include ActiveModel::Model
55
56
  include ActiveModel::Attributes
@@ -98,7 +99,7 @@ module StructuredParams
98
99
 
99
100
  # Permit parameters with optional require
100
101
  #
101
- # For Form Objects (with require):
102
+ # For Form Objects (explicit manual permit):
102
103
  # UserRegistrationForm.permit(params)
103
104
  # # equivalent to:
104
105
  # params.require(:user_registration).permit(*UserRegistrationForm.permit_attribute_names)
@@ -132,6 +133,11 @@ module StructuredParams
132
133
  end
133
134
  end
134
135
 
136
+ #: () -> bool
137
+ def form_class?
138
+ name&.end_with?('Form') || false
139
+ end
140
+
135
141
  private
136
142
 
137
143
  # Determine if the specified type is a StructuredParams type
@@ -156,27 +162,17 @@ module StructuredParams
156
162
  @errors ||= Errors.new(self)
157
163
  end
158
164
 
159
- # ========================================
160
- # Form object support methods
161
- # These methods enable integration with Rails form helpers (form_with, form_for)
162
- # ========================================
163
-
164
- # Indicates whether the form object has been persisted to database
165
- # Always returns false for parameter/form objects
165
+ # Form object support for Rails helpers.
166
166
  #: () -> bool
167
167
  def persisted?
168
168
  false
169
169
  end
170
170
 
171
- # Returns the primary key value for the model
172
- # Always returns nil for parameter/form objects
173
171
  #: () -> nil
174
172
  def to_key
175
173
  nil
176
174
  end
177
175
 
178
- # Returns self for form helpers
179
- # Required by Rails form helpers to get the model object
180
176
  #: () -> self
181
177
  def to_model
182
178
  self
@@ -212,7 +208,7 @@ module StructuredParams
212
208
  def process_input_parameters(params)
213
209
  case params
214
210
  when ActionController::Parameters
215
- self.class.permit(params, require: false).to_h
211
+ process_action_controller_parameters(params)
216
212
  when Hash
217
213
  # ActiveModel::Attributes can handle both symbol and string keys
218
214
  params
@@ -221,6 +217,58 @@ module StructuredParams
221
217
  end
222
218
  end
223
219
 
220
+ #: (ActionController::Parameters) -> Hash[untyped, untyped]
221
+ def process_action_controller_parameters(params)
222
+ self.class.permit(params, require: require_nested_parameters?(params)).to_h
223
+ end
224
+
225
+ # Whether to call params.require(param_key) before permitting.
226
+ #
227
+ # Only ever true for Form-suffixed classes (form_class?); non-Form
228
+ # Params/Parameters subclasses always permit the top level directly.
229
+ #
230
+ # - A value nested under the model's param_key (e.g. params[:user_registration])
231
+ # always wins, even if params were already permitted higher up, since that
232
+ # inner key still needs to be require()'d out.
233
+ # - Otherwise, params that are already permitted, or whose shape looks flat
234
+ # (see flat_parameters?), are used as-is without requiring.
235
+ # - Anything else falls through to true, so params.require raises
236
+ # ActionController::ParameterMissing instead of guessing which keys
237
+ # belong to this form.
238
+ #: (ActionController::Parameters) -> bool
239
+ def require_nested_parameters?(params)
240
+ return false unless self.class.form_class?
241
+ return true if matches_model_name?(params)
242
+ return false if params.permitted?
243
+ return false if flat_parameters?(params)
244
+
245
+ true
246
+ end
247
+
248
+ #: (ActionController::Parameters) -> bool
249
+ def matches_model_name?(params)
250
+ key = self.class.model_name.param_key
251
+ return false unless params.key?(key)
252
+
253
+ params[key].is_a?(ActionController::Parameters)
254
+ end
255
+
256
+ # Only treat params as already-flat attributes when none of the top-level
257
+ # values are themselves nested. A nested value under some other key means
258
+ # the request shape is ambiguous, so we fall through to raising
259
+ # ParameterMissing instead of silently guessing which keys belong here.
260
+ #: (ActionController::Parameters) -> bool
261
+ def flat_parameters?(params)
262
+ return false if params.values.any?(ActionController::Parameters)
263
+
264
+ attribute_keys_present?(params)
265
+ end
266
+
267
+ #: (ActionController::Parameters) -> bool
268
+ def attribute_keys_present?(params)
269
+ params.keys.any? { |key| self.class.attribute_types.key?(key.to_s) }
270
+ end
271
+
224
272
  # Execute structured parameter validation
225
273
  #: () -> void
226
274
  def validate_structured_parameters
@@ -299,4 +347,5 @@ module StructuredParams
299
347
  end
300
348
  end
301
349
  end
350
+ # rubocop:enable Metrics/ClassLength
302
351
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module StructuredParams
5
- VERSION = '0.9.3' #: string
5
+ VERSION = '0.9.5' #: string
6
6
  end
@@ -23,17 +23,75 @@ require_relative 'structured_params/params'
23
23
 
24
24
  # Main module
25
25
  module StructuredParams
26
- # Helper method to register types
27
- #: () -> void
28
- def self.register_types
29
- ActiveModel::Type.register(:object, StructuredParams::Type::Object)
30
- 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
31
63
  end
32
64
 
33
- # Helper method to register types with custom names
34
- #: (object_name: Symbol, array_name: Symbol) -> void
35
- def self.register_types_as(object_name:, array_name:)
36
- ActiveModel::Type.register(object_name, StructuredParams::Type::Object)
37
- 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
38
96
  end
39
97
  end
@@ -21,9 +21,12 @@ module StructuredParams
21
21
  # array: "%{parent} %{index} 番目の%{child}"
22
22
  # object: "%{parent}の%{child}"
23
23
  #
24
- # Without these keys the defaults are:
24
+ # Without these keys the defaults are (with array_index_base: 0):
25
25
  # array → "<parent> <index> <child>" (e.g. "Hobbies 0 Name")
26
26
  # object → "<parent> <child>" (e.g. "Address Postal code")
27
+ #
28
+ # With array_index_base: 1 (human-friendly):
29
+ # array → "Hobbies 1 Name"
27
30
  module I18n
28
31
  extend ActiveSupport::Concern
29
32
 
@@ -32,11 +35,11 @@ module StructuredParams
32
35
  # Flat attributes (no dot) are delegated to the default ActiveModel
33
36
  # behaviour unchanged.
34
37
  #
35
- # Example (en default):
38
+ # Example (en default, array_index_base: 0):
36
39
  # human_attribute_name(:'hobbies.0.name') # => "Hobbies 0 Name"
37
40
  #
38
- # Example with i18n (ja):
39
- # human_attribute_name(:'hobbies.0.name') # => "趣味 0 番目の名前"
41
+ # Example with i18n (ja) and array_index_base: 1:
42
+ # human_attribute_name(:'hobbies.0.name') # => "趣味 1 番目の名前"
40
43
  #
41
44
  # : (Symbol | String, ?Hash[untyped, untyped]) -> String
42
45
  def human_attribute_name: (Symbol | String, ?Hash[untyped, untyped]) -> String
@@ -69,6 +72,11 @@ module StructuredParams
69
72
  # activemodel.errors.nested_attribute.array (parent, index, child)
70
73
  # activemodel.errors.nested_attribute.object (parent, child)
71
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
+ #
72
80
  # The +locale:+ key from +options+ is forwarded to ::I18n.t so that an
73
81
  # explicit locale passed to human_attribute_name is honoured.
74
82
  #
@@ -36,7 +36,7 @@ module StructuredParams
36
36
  # end
37
37
  #
38
38
  # # In controller:
39
- # @form = UserRegistrationForm.new(UserRegistrationForm.permit(params))
39
+ # @form = UserRegistrationForm.new(params)
40
40
  # if @form.valid?
41
41
  # User.create!(@form.attributes)
42
42
  # redirect_to user_path
@@ -49,6 +49,7 @@ module StructuredParams
49
49
  # <%= f.text_field :name %>
50
50
  # <%= f.text_field :email %>
51
51
  # <% end %>
52
+ # rubocop:disable Metrics/ClassLength
52
53
  class Params
53
54
  include ActiveModel::Model
54
55
 
@@ -84,7 +85,7 @@ module StructuredParams
84
85
 
85
86
  # Permit parameters with optional require
86
87
  #
87
- # For Form Objects (with require):
88
+ # For Form Objects (explicit manual permit):
88
89
  # UserRegistrationForm.permit(params)
89
90
  # # equivalent to:
90
91
  # params.require(:user_registration).permit(*UserRegistrationForm.permit_attribute_names)
@@ -101,6 +102,9 @@ module StructuredParams
101
102
  # : () -> Hash[Symbol, singleton(::StructuredParams::Params)]
102
103
  def self.structured_attributes: () -> Hash[Symbol, singleton(::StructuredParams::Params)]
103
104
 
105
+ # : () -> bool
106
+ def self.form_class?: () -> bool
107
+
104
108
  # Determine if the specified type is a StructuredParams type
105
109
  # : (ActiveModel::Type::Value) -> bool
106
110
  private def self.structured_params_type?: (ActiveModel::Type::Value) -> bool
@@ -111,18 +115,13 @@ module StructuredParams
111
115
  # : () -> ::StructuredParams::Errors
112
116
  def errors: () -> ::StructuredParams::Errors
113
117
 
114
- # Indicates whether the form object has been persisted to database
115
- # Always returns false for parameter/form objects
118
+ # Form object support for Rails helpers.
116
119
  # : () -> bool
117
120
  def persisted?: () -> bool
118
121
 
119
- # Returns the primary key value for the model
120
- # Always returns nil for parameter/form objects
121
122
  # : () -> nil
122
123
  def to_key: () -> nil
123
124
 
124
- # Returns self for form helpers
125
- # Required by Rails form helpers to get the model object
126
125
  # : () -> self
127
126
  def to_model: () -> self
128
127
 
@@ -138,6 +137,38 @@ module StructuredParams
138
137
  # : (untyped) -> Hash[untyped, untyped]
139
138
  def process_input_parameters: (untyped) -> Hash[untyped, untyped]
140
139
 
140
+ # : (ActionController::Parameters) -> Hash[untyped, untyped]
141
+ def process_action_controller_parameters: (ActionController::Parameters) -> Hash[untyped, untyped]
142
+
143
+ # Whether to call params.require(param_key) before permitting.
144
+ #
145
+ # Only ever true for Form-suffixed classes (form_class?); non-Form
146
+ # Params/Parameters subclasses always permit the top level directly.
147
+ #
148
+ # - A value nested under the model's param_key (e.g. params[:user_registration])
149
+ # always wins, even if params were already permitted higher up, since that
150
+ # inner key still needs to be require()'d out.
151
+ # - Otherwise, params that are already permitted, or whose shape looks flat
152
+ # (see flat_parameters?), are used as-is without requiring.
153
+ # - Anything else falls through to true, so params.require raises
154
+ # ActionController::ParameterMissing instead of guessing which keys
155
+ # belong to this form.
156
+ # : (ActionController::Parameters) -> bool
157
+ def require_nested_parameters?: (ActionController::Parameters) -> bool
158
+
159
+ # : (ActionController::Parameters) -> bool
160
+ def matches_model_name?: (ActionController::Parameters) -> bool
161
+
162
+ # Only treat params as already-flat attributes when none of the top-level
163
+ # values are themselves nested. A nested value under some other key means
164
+ # the request shape is ambiguous, so we fall through to raising
165
+ # ParameterMissing instead of silently guessing which keys belong here.
166
+ # : (ActionController::Parameters) -> bool
167
+ def flat_parameters?: (ActionController::Parameters) -> bool
168
+
169
+ # : (ActionController::Parameters) -> bool
170
+ def attribute_keys_present?: (ActionController::Parameters) -> bool
171
+
141
172
  # Execute structured parameter validation
142
173
  # : () -> void
143
174
  def validate_structured_parameters: () -> void
@@ -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.3
4
+ version: 0.9.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mizuki Yamamoto
@@ -49,7 +49,9 @@ dependencies:
49
49
  - - "<"
50
50
  - !ruby/object:Gem::Version
51
51
  version: '9.0'
52
- description: ''
52
+ description: StructuredParams provides typed parameter objects for Rails APIs and
53
+ forms, with nested object and array support, Strong Parameters integration, and
54
+ raw-input validation via validates_raw.
53
55
  email:
54
56
  - mizuki-y@syati.info
55
57
  executables: []
@@ -101,5 +103,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
101
103
  requirements: []
102
104
  rubygems_version: 3.6.9
103
105
  specification_version: 4
104
- summary: Type-safe parameter validation and form objects for Rails.
106
+ summary: Typed parameter objects and form objects for Rails.
105
107
  test_files: []