From 30af2477e1f9c8f80736219490807cb32887b72c Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 25 May 2026 10:34:50 +0200 Subject: [PATCH] Drop Enumerable from Grape::Exceptions::ValidationErrors `Grape::Exceptions::ValidationErrors` mixed in `Enumerable` and exposed a public `#each` that yielded `(attribute, error)` pairs. That surface was undocumented (the README only covers `#errors`, `#full_messages`, `#message`, and `rescue_from`), untested, and had a single in-class consumer: `#full_messages` calling `map` on `self`. Removed `include Enumerable` and `#each`. Rewrote `#full_messages` to walk the `errors` hash directly via `flat_map`, which also documents the actual data shape (`{ params => [validations] }`) at the call site. UPGRADING entry added since this is a contract break, even if the surface was undocumented. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 1 + UPGRADING.md | 18 +++++++++++++++ lib/grape/exceptions/validation_errors.rb | 28 ++++++++--------------- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c579e464..eb5a5f0f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ * [#2733](https://github.com/ruby-grape/grape/pull/2733): Drop the dead `active_support/core_ext/hash/reverse_merge` require; call `ActiveSupport::HashWithIndifferentAccess.new(...)` directly at call sites - [@ericproulx](https://github.com/ericproulx). * [#2734](https://github.com/ruby-grape/grape/pull/2734): Extract `options_route_enabled` from the Endpoint options Hash into a dedicated `attr_accessor` - [@ericproulx](https://github.com/ericproulx). * [#2736](https://github.com/ruby-grape/grape/pull/2736): Collapse `Endpoint#run_validators` rescue branches via `ValidationErrors` flatten; `ValidationErrors#initialize` keyword renamed `errors:` → `exceptions:` - [@ericproulx](https://github.com/ericproulx). +* [#2747](https://github.com/ruby-grape/grape/pull/2747): Drop `Enumerable` from `Grape::Exceptions::ValidationErrors` and remove its public `#each`; rewrite `#full_messages` to walk `#errors` directly - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/UPGRADING.md b/UPGRADING.md index eb3ed9ed5..5d7873696 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -13,6 +13,24 @@ Grape::Exceptions::ValidationErrors.new(errors: [validation, validation_array_er # after Grape::Exceptions::ValidationErrors.new(exceptions: [validation, validation_array_errors], headers:) +#### `Grape::Exceptions::ValidationErrors` no longer mixes in `Enumerable` + +`Grape::Exceptions::ValidationErrors` no longer includes `Enumerable` and no longer defines a public `#each`. The Enumerable surface (`#each`, `#map`, `#select`, `#to_a`, etc.) was undocumented and untested; the documented accessors — `#errors`, `#full_messages`, `#message`, `#as_json` — are unchanged. + +If a `rescue_from` block iterated over the exception instance, switch to `#errors`: + +```ruby +# before +rescue_from Grape::Exceptions::ValidationErrors do |e| + e.each { |attribute, error| ... } +end + +# after +rescue_from Grape::Exceptions::ValidationErrors do |e| + e.errors.each do |attributes, errs| + errs.each { |error| ... } + end +end ``` #### `auth`, `http_basic` and `http_digest` now take keyword arguments diff --git a/lib/grape/exceptions/validation_errors.rb b/lib/grape/exceptions/validation_errors.rb index 69006201b..9fad6f94c 100644 --- a/lib/grape/exceptions/validation_errors.rb +++ b/lib/grape/exceptions/validation_errors.rb @@ -3,8 +3,6 @@ module Grape module Exceptions class ValidationErrors < Base - include Enumerable - attr_reader :errors def initialize(exceptions: [], headers: {}) @@ -12,14 +10,6 @@ def initialize(exceptions: [], headers: {}) super(message: full_messages.join(', '), status: 400, headers:) end - def each - errors.each_pair do |attribute, errors| - errors.each do |error| - yield attribute, error - end - end - end - def as_json(**_opts) errors.map do |k, v| { @@ -34,14 +24,16 @@ def to_json(*_opts) end def full_messages - messages = map do |attributes, error| - translate( - :format, - scope: 'grape.errors', - default: '%s %s', - attributes: translate_attributes(attributes), - message: error.message - ) + messages = errors.flat_map do |attributes, errs| + errs.map do |error| + translate( + :format, + scope: 'grape.errors', + default: '%s %s', + attributes: translate_attributes(attributes), + message: error.message + ) + end end messages.uniq! messages