Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ Metrics/ClassLength:
Max: 150

Metrics/MethodLength:
Max: 20
Max: 25
1 change: 1 addition & 0 deletions .rubocop_rbs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Style/RbsInline/MissingTypeAnnotation:
Style/RbsInline/UntypedInstanceVariable:
Exclude:
- 'lib/structured_params/params.rb'
- 'lib/structured_params.rb'

Style/RbsInline/RequireRbsInlineComment:
Exclude:
Expand Down
5 changes: 5 additions & 0 deletions Steepfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
Syati marked this conversation as resolved.
end
end
24 changes: 24 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
78 changes: 68 additions & 10 deletions lib/structured_params.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,75 @@

# 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)
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
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 self.@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
21 changes: 15 additions & 6 deletions lib/structured_params/i18n.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 → "<parent> <index> <child>" (e.g. "Hobbies 0 Name")
# object → "<parent> <child>" (e.g. "Address Postal code")
#
# With array_index_base: 1 (human-friendly):
# array → "Hobbies 1 Name"
module I18n
extend ActiveSupport::Concern

Expand All @@ -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 = {})
Expand Down Expand Up @@ -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.
#
Expand All @@ -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
Expand Down
40 changes: 22 additions & 18 deletions rbs_collection.lock.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,39 @@ 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
version: '6.0'
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
version: '7.1'
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
version: '7.0'
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
version: '2.4'
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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion rbs_collection.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,3 @@ gems:
ignore: true
- name: diff-lcs
ignore: true

41 changes: 41 additions & 0 deletions sig/structured_params.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -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

self.@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
Expand Down
Loading