From ed0ba791b31c0937a7a7e04218a2c2653adb1531 Mon Sep 17 00:00:00 2001 From: mizuki-y Date: Fri, 1 May 2026 14:20:27 +0900 Subject: [PATCH 1/6] Enhance array index handling in human_attribute_name and add configuration for index base --- .rubocop.yml | 2 +- lib/structured_params.rb | 76 ++++++++++++++++++++++++++++----- lib/structured_params/i18n.rb | 21 +++++++--- spec/i18n_spec.rb | 79 +++++++++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 17 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 6472f10..03edc96 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -31,4 +31,4 @@ Metrics/ClassLength: Max: 150 Metrics/MethodLength: - Max: 20 + Max: 25 diff --git a/lib/structured_params.rb b/lib/structured_params.rb index 285ccab..9f5c756 100644 --- a/lib/structured_params.rb +++ b/lib/structured_params.rb @@ -23,17 +23,73 @@ # Main module module StructuredParams - # Helper method to register types - #: () -> void - def self.register_types - ActiveModel::Type.register(:object, StructuredParams::Type::Object) - ActiveModel::Type.register(:array, StructuredParams::Type::Array) + # Global configuration for StructuredParams. + # + # == Options + # + # +array_index_base+ (Integer, default: +0+):: + # Controls how array indices are displayed in human attribute names and + # error messages. + # + # * +0+ – 0-based (raw Ruby index): "Hobbies 0 Name" + # * +1+ – 1-based (human-friendly): "Hobbies 1 Name" + # + # This setting applies to both API param error messages and Form Object + # +full_messages+. + # + # == Example + # + # # config/initializers/structured_params.rb + # StructuredParams.configure do |config| + # config.array_index_base = 1 # show "1st" instead of "0th" to users + # end + # + class Configuration + attr_reader :array_index_base #: Integer + + #: () -> void + def initialize + @array_index_base = 0 + end + + #: (Integer) -> void + def array_index_base=(value) + raise ArgumentError, "array_index_base must be 0 or 1, got: #{value.inspect}" unless [0, 1].include?(value) + + @array_index_base = value + end end - # Helper method to register types with custom names - #: (object_name: Symbol, array_name: Symbol) -> void - def self.register_types_as(object_name:, array_name:) - ActiveModel::Type.register(object_name, StructuredParams::Type::Object) - ActiveModel::Type.register(array_name, StructuredParams::Type::Array) + class << self + # @rbs @configuration: Configuration? + + #: () -> Configuration + def configuration + @configuration ||= Configuration.new + end + + #: () { (Configuration) -> void } -> void + def configure + yield configuration + end + + #: () -> void + def reset_configuration! + @configuration = Configuration.new + end + + # Helper method to register types + #: () -> void + def register_types + ActiveModel::Type.register(:object, StructuredParams::Type::Object) + ActiveModel::Type.register(:array, StructuredParams::Type::Array) + end + + # Helper method to register types with custom names + #: (object_name: Symbol, array_name: Symbol) -> void + def register_types_as(object_name:, array_name:) + ActiveModel::Type.register(object_name, StructuredParams::Type::Object) + ActiveModel::Type.register(array_name, StructuredParams::Type::Array) + end end end diff --git a/lib/structured_params/i18n.rb b/lib/structured_params/i18n.rb index 7b564c4..80c699e 100644 --- a/lib/structured_params/i18n.rb +++ b/lib/structured_params/i18n.rb @@ -22,9 +22,12 @@ module StructuredParams # array: "%{parent} %{index} 番目の%{child}" # object: "%{parent}の%{child}" # - # Without these keys the defaults are: + # Without these keys the defaults are (with array_index_base: 0): # array → " " (e.g. "Hobbies 0 Name") # object → " " (e.g. "Address Postal code") + # + # With array_index_base: 1 (human-friendly): + # array → "Hobbies 1 Name" module I18n extend ActiveSupport::Concern @@ -34,11 +37,11 @@ module I18n # Flat attributes (no dot) are delegated to the default ActiveModel # behaviour unchanged. # - # Example (en default): + # Example (en default, array_index_base: 0): # human_attribute_name(:'hobbies.0.name') # => "Hobbies 0 Name" # - # Example with i18n (ja): - # human_attribute_name(:'hobbies.0.name') # => "趣味 0 番目の名前" + # Example with i18n (ja) and array_index_base: 1: + # human_attribute_name(:'hobbies.0.name') # => "趣味 1 番目の名前" # #: (Symbol | String, ?Hash[untyped, untyped]) -> String def human_attribute_name(attribute, options = {}) @@ -99,6 +102,11 @@ def attr_segments(parts) # activemodel.errors.nested_attribute.array (parent, index, child) # activemodel.errors.nested_attribute.object (parent, child) # + # The index value passed to the i18n template is adjusted by + # +StructuredParams.configuration.array_index_base+: + # * +0+ (default) – raw 0-based Ruby index (e.g. 0, 1, 2, …) + # * +1+ – human-friendly 1-based index (e.g. 1, 2, 3, …) + # # The +locale:+ key from +options+ is forwarded to ::I18n.t so that an # explicit locale passed to human_attribute_name is honoured. # @@ -109,12 +117,13 @@ def build_nested_label(result, index, attr_human, options) i18n_opts = options.slice(:locale) if index + display_index = index.to_i + StructuredParams.configuration.array_index_base ::I18n.t( 'activemodel.errors.nested_attribute.array', parent: result, - index: index, + index: display_index, child: attr_human, - default: "#{result} #{index} #{attr_human}", + default: "#{result} #{display_index} #{attr_human}", **i18n_opts ) else diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index 9a8291d..61b92ef 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -3,6 +3,9 @@ require 'spec_helper' RSpec.describe StructuredParams::I18n do + # Reset global configuration after each example to avoid test pollution + after { StructuredParams.reset_configuration! } + describe '.human_attribute_name' do describe 'flat attributes (without dot notation)' do it 'delegates to the default ActiveModel implementation' do @@ -231,4 +234,80 @@ end end end + + describe 'StructuredParams.configuration.array_index_base' do + describe 'default (array_index_base: 0, 0-based)' do + it 'displays raw 0-based indices in the default en format' do + expect(UserParameter.human_attribute_name(:'hobbies.0.name')).to eq('Hobbies 0 Name') + expect(UserParameter.human_attribute_name(:'hobbies.2.name')).to eq('Hobbies 2 Name') + end + + context 'with ja locale and array format key' do + include_context 'with ja locale' + + let(:ja_overrides) do + { activemodel: { errors: { nested_attribute: { array: '%s %s 番目の%s' } } } } + end + + it 'displays 0-based indices in the ja format' do + expect(UserParameter.human_attribute_name(:'hobbies.0.name')).to eq('趣味 0 番目の名前') + expect(UserParameter.human_attribute_name(:'hobbies.2.name')).to eq('趣味 2 番目の名前') + end + end + end + + describe 'array_index_base: 1 (1-based, human-friendly)' do + before { StructuredParams.configure { |c| c.array_index_base = 1 } } + + it 'adds 1 to the raw index in the default en format' do + # path .0. → display 1, path .2. → display 3 + expect(UserParameter.human_attribute_name(:'hobbies.0.name')).to eq('Hobbies 1 Name') + expect(UserParameter.human_attribute_name(:'hobbies.2.name')).to eq('Hobbies 3 Name') + end + + it 'applies to API param error messages as well as Form Object full_messages (same code path)' do + expect(UserParameter.human_attribute_name(:'hobbies.0.level')).to eq('Hobbies 1 Level') + end + + context 'with ja locale and array format key' do + include_context 'with ja locale' + + let(:ja_overrides) do + { activemodel: { errors: { nested_attribute: { array: '%s %s 番目の%s' } } } } + end + + it 'displays 1-based indices in the ja format' do + expect(UserParameter.human_attribute_name(:'hobbies.0.name')).to eq('趣味 1 番目の名前') + expect(UserParameter.human_attribute_name(:'hobbies.2.name')).to eq('趣味 3 番目の名前') + end + end + + context 'when locale: :ja is passed explicitly' do + include_context 'with ja locale' + + let(:ja_overrides) do + { activemodel: { errors: { nested_attribute: { array: '%s %s 番目の%s' } } } } + end + + it 'threads both locale option and 1-based index correctly' do + I18n.with_locale(:en) do + result = UserParameter.human_attribute_name(:'hobbies.0.name', locale: :ja) + expect(result).to eq('趣味 1 番目の名前') + end + end + end + end + + describe 'validation of array_index_base=' do + it 'raises ArgumentError for values other than 0 or 1' do + expect { StructuredParams.configure { |c| c.array_index_base = 2 } } + .to raise_error(ArgumentError, /array_index_base must be 0 or 1/) + end + + it 'raises ArgumentError for negative values' do + expect { StructuredParams.configure { |c| c.array_index_base = -1 } } + .to raise_error(ArgumentError, /array_index_base must be 0 or 1/) + end + end + end end From bd65ba7e6afeff7ac8a70fc244e460e80908ef99 Mon Sep 17 00:00:00 2001 From: mizuki-y Date: Fri, 1 May 2026 14:32:25 +0900 Subject: [PATCH 2/6] Enhance array index handling in human_attribute_name and add configuration for index base --- sig/structured_params.rbs | 41 ++++++++++++++++++++++++++++ sig/structured_params/i18n.rbs | 16 ++++++++--- sig/structured_params/type/array.rbs | 13 +++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/sig/structured_params.rbs b/sig/structured_params.rbs index ac67ded..fbfc38f 100644 --- a/sig/structured_params.rbs +++ b/sig/structured_params.rbs @@ -2,6 +2,47 @@ # Main module module StructuredParams + # Global configuration for StructuredParams. + # + # == Options + # + # +array_index_base+ (Integer, default: +0+):: + # Controls how array indices are displayed in human attribute names and + # error messages. + # + # * +0+ – 0-based (raw Ruby index): "Hobbies 0 Name" + # * +1+ – 1-based (human-friendly): "Hobbies 1 Name" + # + # This setting applies to both API param error messages and Form Object + # +full_messages+. + # + # == Example + # + # # config/initializers/structured_params.rb + # StructuredParams.configure do |config| + # config.array_index_base = 1 # show "1st" instead of "0th" to users + # end + class Configuration + attr_reader array_index_base: Integer + + # : () -> void + def initialize: () -> void + + # : (Integer) -> void + def array_index_base=: (Integer) -> void + end + + @configuration: Configuration? + + # : () -> Configuration + def self.configuration: () -> Configuration + + # : () { (Configuration) -> void } -> void + def self.configure: () { (Configuration) -> void } -> void + + # : () -> void + def self.reset_configuration!: () -> void + # Helper method to register types # : () -> void def self.register_types: () -> void diff --git a/sig/structured_params/i18n.rbs b/sig/structured_params/i18n.rbs index ae44376..222ffb6 100644 --- a/sig/structured_params/i18n.rbs +++ b/sig/structured_params/i18n.rbs @@ -21,9 +21,12 @@ module StructuredParams # array: "%{parent} %{index} 番目の%{child}" # object: "%{parent}の%{child}" # - # Without these keys the defaults are: + # Without these keys the defaults are (with array_index_base: 0): # array → " " (e.g. "Hobbies 0 Name") # object → " " (e.g. "Address Postal code") + # + # With array_index_base: 1 (human-friendly): + # array → "Hobbies 1 Name" module I18n extend ActiveSupport::Concern @@ -32,11 +35,11 @@ module StructuredParams # Flat attributes (no dot) are delegated to the default ActiveModel # behaviour unchanged. # - # Example (en default): + # Example (en default, array_index_base: 0): # human_attribute_name(:'hobbies.0.name') # => "Hobbies 0 Name" # - # Example with i18n (ja): - # human_attribute_name(:'hobbies.0.name') # => "趣味 0 番目の名前" + # Example with i18n (ja) and array_index_base: 1: + # human_attribute_name(:'hobbies.0.name') # => "趣味 1 番目の名前" # # : (Symbol | String, ?Hash[untyped, untyped]) -> String def human_attribute_name: (Symbol | String, ?Hash[untyped, untyped]) -> String @@ -69,6 +72,11 @@ module StructuredParams # activemodel.errors.nested_attribute.array (parent, index, child) # activemodel.errors.nested_attribute.object (parent, child) # + # The index value passed to the i18n template is adjusted by + # +StructuredParams.configuration.array_index_base+: + # * +0+ (default) – raw 0-based Ruby index (e.g. 0, 1, 2, …) + # * +1+ – human-friendly 1-based index (e.g. 1, 2, 3, …) + # # The +locale:+ key from +options+ is forwarded to ::I18n.t so that an # explicit locale passed to human_attribute_name is honoured. # diff --git a/sig/structured_params/type/array.rbs b/sig/structured_params/type/array.rbs index 76bacd3..a8a6bcd 100644 --- a/sig/structured_params/type/array.rbs +++ b/sig/structured_params/type/array.rbs @@ -29,6 +29,19 @@ module StructuredParams def serialize: (::Array[untyped]?) -> ::Array[untyped]? # Get permitted parameter names for use with Strong Parameters + # + # Returns: + # - For object arrays (value_class): nested keys from the object + # Example: HobbyParams with [:name, :level] + # → returns [:name, :level] + # → becomes { hobbies: [:name, :level] } in Params#permit_attribute_names + # + # - For primitive arrays (value_type): empty array + # Example: attribute :tags, :array, value_type: :string + # → returns [] + # → becomes { tags: [] } in Params#permit_attribute_names + # → finally used as params.permit(:name, { tags: [] }) + # # : () -> ::Array[untyped] def permit_attribute_names: () -> ::Array[untyped] From 75a8641c15d2d058aca1fcf51742ef09329c8ab1 Mon Sep 17 00:00:00 2001 From: mizuki-y Date: Fri, 1 May 2026 14:57:35 +0900 Subject: [PATCH 3/6] Update RBS configuration and structured_params for improved type handling --- .rubocop_rbs.yml | 1 + Steepfile | 5 +++++ lib/structured_params.rb | 2 +- rbs_collection.lock.yaml | 40 +++++++++++++++++++++------------------ rbs_collection.yaml | 1 - sig/structured_params.rbs | 2 +- 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/.rubocop_rbs.yml b/.rubocop_rbs.yml index 5767a86..30f1222 100644 --- a/.rubocop_rbs.yml +++ b/.rubocop_rbs.yml @@ -12,6 +12,7 @@ Style/RbsInline/MissingTypeAnnotation: Style/RbsInline/UntypedInstanceVariable: Exclude: - 'lib/structured_params/params.rb' + - 'lib/structured_params.rb' Style/RbsInline/RequireRbsInlineComment: Exclude: diff --git a/Steepfile b/Steepfile index 8eb0ac3..5cd3edb 100644 --- a/Steepfile +++ b/Steepfile @@ -6,4 +6,9 @@ target :lib do signature 'sig' check 'lib' + + # Suppress errors from broken library RBS files in gem_rbs_collection + configure_code_diagnostics do |hash| + hash[Steep::Diagnostic::Ruby::LibraryRBSError] = nil + end end diff --git a/lib/structured_params.rb b/lib/structured_params.rb index 9f5c756..13a62ac 100644 --- a/lib/structured_params.rb +++ b/lib/structured_params.rb @@ -61,7 +61,7 @@ def array_index_base=(value) end class << self - # @rbs @configuration: Configuration? + # @rbs self.@configuration: Configuration? #: () -> Configuration def configuration diff --git a/rbs_collection.lock.yaml b/rbs_collection.lock.yaml index 906d309..d5d5320 100644 --- a/rbs_collection.lock.yaml +++ b/rbs_collection.lock.yaml @@ -6,7 +6,7 @@ gems: source: type: git name: ruby/gem_rbs_collection - revision: 14112a82013860a7d2e5246b4c4f36fbb8c349dc + revision: d7090c40bcea7011ad2ebaaa46bdd7d7ac3136a7 remote: https://github.com/ruby/gem_rbs_collection.git repo_dir: gems - name: actionview @@ -14,7 +14,7 @@ gems: source: type: git name: ruby/gem_rbs_collection - revision: 14112a82013860a7d2e5246b4c4f36fbb8c349dc + revision: d7090c40bcea7011ad2ebaaa46bdd7d7ac3136a7 remote: https://github.com/ruby/gem_rbs_collection.git repo_dir: gems - name: activemodel @@ -22,7 +22,7 @@ gems: source: type: git name: ruby/gem_rbs_collection - revision: 14112a82013860a7d2e5246b4c4f36fbb8c349dc + revision: d7090c40bcea7011ad2ebaaa46bdd7d7ac3136a7 remote: https://github.com/ruby/gem_rbs_collection.git repo_dir: gems - name: activesupport @@ -30,7 +30,7 @@ gems: source: type: git name: ruby/gem_rbs_collection - revision: 14112a82013860a7d2e5246b4c4f36fbb8c349dc + revision: d7090c40bcea7011ad2ebaaa46bdd7d7ac3136a7 remote: https://github.com/ruby/gem_rbs_collection.git repo_dir: gems - name: ast @@ -38,7 +38,7 @@ gems: source: type: git name: ruby/gem_rbs_collection - revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783 + revision: d7090c40bcea7011ad2ebaaa46bdd7d7ac3136a7 remote: https://github.com/ruby/gem_rbs_collection.git repo_dir: gems - name: base64 @@ -50,19 +50,15 @@ gems: source: type: stdlib - name: bigdecimal - version: '3.1' + version: 4.1.2 source: - type: git - name: ruby/gem_rbs_collection - revision: 14112a82013860a7d2e5246b4c4f36fbb8c349dc - remote: https://github.com/ruby/gem_rbs_collection.git - repo_dir: gems + type: rubygems - name: binding_of_caller version: '1.0' source: type: git name: ruby/gem_rbs_collection - revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783 + revision: d7090c40bcea7011ad2ebaaa46bdd7d7ac3136a7 remote: https://github.com/ruby/gem_rbs_collection.git repo_dir: gems - name: date @@ -82,13 +78,17 @@ gems: source: type: git name: ruby/gem_rbs_collection - revision: 14112a82013860a7d2e5246b4c4f36fbb8c349dc + revision: d7090c40bcea7011ad2ebaaa46bdd7d7ac3136a7 remote: https://github.com/ruby/gem_rbs_collection.git repo_dir: gems - name: logger - version: '0' + version: '1.7' source: - type: stdlib + type: git + name: ruby/gem_rbs_collection + revision: d7090c40bcea7011ad2ebaaa46bdd7d7ac3136a7 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems - name: monitor version: '0' source: @@ -102,15 +102,19 @@ gems: source: type: git name: ruby/gem_rbs_collection - revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783 + revision: d7090c40bcea7011ad2ebaaa46bdd7d7ac3136a7 remote: https://github.com/ruby/gem_rbs_collection.git repo_dir: gems - name: prism version: 1.9.0 source: type: rubygems +- name: random-formatter + version: '0' + source: + type: stdlib - name: rspec-parameterized-core - version: 2.0.1 + version: 2.0.2 source: type: rubygems - name: rspec-parameterized-table_syntax @@ -142,7 +146,7 @@ gems: source: type: git name: ruby/gem_rbs_collection - revision: 14112a82013860a7d2e5246b4c4f36fbb8c349dc + revision: d7090c40bcea7011ad2ebaaa46bdd7d7ac3136a7 remote: https://github.com/ruby/gem_rbs_collection.git repo_dir: gems - name: uri diff --git a/rbs_collection.yaml b/rbs_collection.yaml index ef70eff..f4a341d 100644 --- a/rbs_collection.yaml +++ b/rbs_collection.yaml @@ -37,4 +37,3 @@ gems: ignore: true - name: diff-lcs ignore: true - diff --git a/sig/structured_params.rbs b/sig/structured_params.rbs index fbfc38f..1a9debe 100644 --- a/sig/structured_params.rbs +++ b/sig/structured_params.rbs @@ -32,7 +32,7 @@ module StructuredParams def array_index_base=: (Integer) -> void end - @configuration: Configuration? + self.@configuration: Configuration? # : () -> Configuration def self.configuration: () -> Configuration From 5984949506bc380b00d3e87c5c8877871183dfe7 Mon Sep 17 00:00:00 2001 From: mizuki-y Date: Fri, 1 May 2026 15:02:46 +0900 Subject: [PATCH 4/6] Add configuration section to installation guide for StructuredParams --- docs/installation.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/installation.md b/docs/installation.md index ca4c23c..3911f18 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,6 +6,7 @@ Steps to add StructuredParams to a Rails application. - [Installation](#installation) - [Setup](#setup) +- [Configuration](#configuration) - [Custom Type Registration](#custom-type-registration) ## Installation @@ -37,6 +38,29 @@ StructuredParams.register_types This registers the `:object` and `:array` types with ActiveModel::Type. +## Configuration + +You can configure StructuredParams in the same initializer: + +```ruby +# config/initializers/structured_params.rb +StructuredParams.register_types + +StructuredParams.configure do |config| + # Controls how array indices appear in human attribute names and full_messages. + # 0 (default) — 0-based: "Hobbies 0 Name can't be blank" + # 1 — 1-based: "Hobbies 1 Name can't be blank" + config.array_index_base = 1 +end +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `array_index_base` | `0` | Index base for array elements in error messages (`0` or `1`) | + +> **Note:** `array_index_base` affects `human_attribute_name` and therefore `full_messages`. +> For APIs returning raw error keys (the typical pattern), this setting has no visible effect. + ## Custom Type Registration To avoid naming conflicts with existing code, register the types under custom names: From df0ad50c8117112f201e3f5025617c15f7b9a188 Mon Sep 17 00:00:00 2001 From: mizuki-y Date: Fri, 1 May 2026 15:17:43 +0900 Subject: [PATCH 5/6] Update lib/structured_params.rb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/structured_params.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/structured_params.rb b/lib/structured_params.rb index 13a62ac..dd0ac25 100644 --- a/lib/structured_params.rb +++ b/lib/structured_params.rb @@ -54,7 +54,7 @@ def initialize #: (Integer) -> void def array_index_base=(value) - raise ArgumentError, "array_index_base must be 0 or 1, got: #{value.inspect}" unless [0, 1].include?(value) + raise ArgumentError, "array_index_base must be 0 or 1, got: #{value.inspect}" unless value.is_a?(Integer) && [0, 1].include?(value) @array_index_base = value end From bd79d35251d88306d7c86775a3c3daedbd71f652 Mon Sep 17 00:00:00 2001 From: mizuki-y Date: Fri, 1 May 2026 15:22:24 +0900 Subject: [PATCH 6/6] Add tests for array_index_base validation and enhance error handling --- lib/structured_params.rb | 4 +++- spec/i18n_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/structured_params.rb b/lib/structured_params.rb index dd0ac25..622d85b 100644 --- a/lib/structured_params.rb +++ b/lib/structured_params.rb @@ -54,7 +54,9 @@ def initialize #: (Integer) -> void def array_index_base=(value) - raise ArgumentError, "array_index_base must be 0 or 1, got: #{value.inspect}" unless value.is_a?(Integer) && [0, 1].include?(value) + unless value.is_a?(Integer) && [0, 1].include?(value) + raise ArgumentError, "array_index_base must be 0 or 1, got: #{value.inspect}" + end @array_index_base = value end diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index 61b92ef..a93c902 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -308,6 +308,26 @@ expect { StructuredParams.configure { |c| c.array_index_base = -1 } } .to raise_error(ArgumentError, /array_index_base must be 0 or 1/) end + + it 'raises ArgumentError for non-integer values (string)' do + expect { StructuredParams.configure { |c| c.array_index_base = '1' } } + .to raise_error(ArgumentError, /array_index_base must be 0 or 1/) + end + + it 'raises ArgumentError for non-integer values (boolean)' do + expect { StructuredParams.configure { |c| c.array_index_base = true } } + .to raise_error(ArgumentError, /array_index_base must be 0 or 1/) + end + + it 'accepts 0' do + expect { StructuredParams.configure { |c| c.array_index_base = 0 } }.not_to raise_error + expect(StructuredParams.configuration.array_index_base).to eq(0) + end + + it 'accepts 1' do + expect { StructuredParams.configure { |c| c.array_index_base = 1 } }.not_to raise_error + expect(StructuredParams.configuration.array_index_base).to eq(1) + end end end end