Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
76 changes: 66 additions & 10 deletions lib/structured_params.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
Syati marked this conversation as resolved.
Outdated

@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