grape 3.1.1 → 3.2.0

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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -1
  3. data/README.md +76 -161
  4. data/UPGRADING.md +106 -0
  5. data/grape.gemspec +2 -2
  6. data/lib/grape/api/instance.rb +1 -1
  7. data/lib/grape/api.rb +1 -1
  8. data/lib/grape/declared_params_handler.rb +3 -3
  9. data/lib/grape/dsl/declared.rb +1 -1
  10. data/lib/grape/dsl/desc.rb +1 -1
  11. data/lib/grape/dsl/inside_route.rb +9 -9
  12. data/lib/grape/dsl/parameters.rb +14 -14
  13. data/lib/grape/dsl/routing.rb +8 -8
  14. data/lib/grape/endpoint.rb +42 -49
  15. data/lib/grape/error_formatter/base.rb +2 -2
  16. data/lib/grape/exceptions/base.rb +18 -44
  17. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  18. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  19. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  20. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  21. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  22. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  23. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  24. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  25. data/lib/grape/exceptions/request_error.rb +11 -0
  26. data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
  27. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  28. data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
  29. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  30. data/lib/grape/exceptions/validation.rb +7 -4
  31. data/lib/grape/exceptions/validation_errors.rb +13 -7
  32. data/lib/grape/locale/en.yml +0 -5
  33. data/lib/grape/middleware/auth/base.rb +2 -0
  34. data/lib/grape/middleware/base.rb +2 -4
  35. data/lib/grape/middleware/error.rb +2 -2
  36. data/lib/grape/middleware/formatter.rb +1 -1
  37. data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
  38. data/lib/grape/request.rb +2 -10
  39. data/lib/grape/router/pattern.rb +1 -1
  40. data/lib/grape/router.rb +4 -2
  41. data/lib/grape/util/api_description.rb +1 -1
  42. data/lib/grape/util/deep_freeze.rb +35 -0
  43. data/lib/grape/util/inheritable_setting.rb +1 -1
  44. data/lib/grape/util/media_type.rb +1 -1
  45. data/lib/grape/util/translation.rb +42 -0
  46. data/lib/grape/validations/attributes_iterator.rb +33 -18
  47. data/lib/grape/validations/contract_scope.rb +1 -7
  48. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  49. data/lib/grape/validations/param_scope_tracker.rb +57 -0
  50. data/lib/grape/validations/params_scope.rb +111 -107
  51. data/lib/grape/validations/single_attribute_iterator.rb +2 -2
  52. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  53. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  54. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  55. data/lib/grape/validations/validators/base.rb +95 -18
  56. data/lib/grape/validations/validators/coerce_validator.rb +15 -35
  57. data/lib/grape/validations/validators/contract_scope_validator.rb +10 -8
  58. data/lib/grape/validations/validators/default_validator.rb +12 -18
  59. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  60. data/lib/grape/validations/validators/except_values_validator.rb +13 -4
  61. data/lib/grape/validations/validators/length_validator.rb +21 -22
  62. data/lib/grape/validations/validators/multiple_params_base.rb +5 -5
  63. data/lib/grape/validations/validators/mutually_exclusive_validator.rb +3 -1
  64. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  65. data/lib/grape/validations/validators/regexp_validator.rb +8 -10
  66. data/lib/grape/validations/validators/same_as_validator.rb +6 -15
  67. data/lib/grape/validations/validators/values_validator.rb +29 -21
  68. data/lib/grape/version.rb +1 -1
  69. data/lib/grape.rb +18 -1
  70. metadata +11 -13
  71. data/lib/grape/exceptions/conflicting_types.rb +0 -11
  72. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  73. data/lib/grape/exceptions/invalid_parameters.rb +0 -11
  74. data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
  75. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  76. data/lib/grape/validations/validator_factory.rb +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f03f5db37edb93b40fa1b9addead63466a5c1f49740c4f1c1335eabb3dd93ff1
4
- data.tar.gz: 0e42696b25c1ee096e15d6e808262904ba61d85d9faa7d008a052ff3cd553de9
3
+ metadata.gz: 68c3425fcc271ae7241f0531bbd9cb6c466aca9ccfed37dc37c0a6f83a4500d2
4
+ data.tar.gz: d98863b6374c5e25bb95a101c891c0c97b4f3746c9be0c6d7cc4b5b03f1f30fd
5
5
  SHA512:
6
- metadata.gz: d98db4f7e9cf7884f52a349bf28bad16fc62dc93a3019e808c0b7497e872334a7462353a35dd19f6cc09d2566421902984562586d2d22dfd6a6db40e37ac276a
7
- data.tar.gz: 210ce14f0ef502d9a18fd4a7b24b111ca5d69b6689fd5b0bb629c8d5c072afd9576a6834f36764e9d5e4e5a92c1372b7962aff9500d9d9f7f561879cc6bac247
6
+ metadata.gz: '059861dafdf89f21d71b23c09e8f5f618ebd42092af31600a3943e0ba9695abd4ee01e90f3cae97aedb7ae777c96e22e3963cab3d4527fcbf16b82ef19939a99'
7
+ data.tar.gz: feb02e6b1db04867d06b62d2096931c762028f7b5bf8776f13bf4a5902f4b70b538f016a7f5ada98ffa7fd3f1e22e16054fc229f33fcabf8cf3d8486a956d4c7
data/CHANGELOG.md CHANGED
@@ -1,8 +1,28 @@
1
- ### 3.1.1 (2026-01-31)
1
+ ### 3.2.0 (Next)
2
+
3
+ #### Features
4
+
5
+ * [#2662](https://github.com/ruby-grape/grape/pull/2662): Extract `Grape::Util::Translation` for shared I18n fallback logic - [@ericproulx](https://github.com/ericproulx).
6
+ * [#2656](https://github.com/ruby-grape/grape/pull/2656): Remove useless instance_variable_defined? checks - [@ericproulx](https://github.com/ericproulx).
7
+ * [#2619](https://github.com/ruby-grape/grape/pull/2619): Remove TOC from README.md and danger-toc check - [@alexanderadam](https://github.com/alexanderadam).
8
+ * [#2663](https://github.com/ruby-grape/grape/pull/2663): Refactor `ParamsScope` and `Parameters` DSL to use named kwargs - [@ericproulx](https://github.com/ericproulx).
9
+ * [#2664](https://github.com/ruby-grape/grape/pull/2664): Drop `test-prof` dependency - [@ericproulx](https://github.com/ericproulx).
10
+ * [#2665](https://github.com/ruby-grape/grape/pull/2665): Pass `attrs` directly to `AttributesIterator` instead of `validator` - [@ericproulx](https://github.com/ericproulx).
11
+ * [#2657](https://github.com/ruby-grape/grape/pull/2657): Instantiate validators at definition time - [@ericproulx](https://github.com/ericproulx).
12
+ * [#2667](https://github.com/ruby-grape/grape/pull/2667): Skip instrumentation in run_validators when no validators present - [@ericproulx](https://github.com/ericproulx).
13
+ * [#2670](https://github.com/ruby-grape/grape/pull/2670): Added support for Rack 3.2.6 and better handling to rack exceptions - [@ericproulx](https://github.com/ericproulx).
14
+ * [#2671](https://github.com/ruby-grape/grape/pull/2671): Use ruby 3.1 shorthand kwargs syntax - [@ericproulx](https://github.com/ericproulx).
15
+ * [#2672](https://github.com/ruby-grape/grape/pull/2672): Minor ruby optimizations - [@ericproulx](https://github.com/ericproulx).
16
+ * [#2675](https://github.com/ruby-grape/grape/pull/2675): Add `AGENTS.md` to please our future A.I. overlords - [@dblock](https://github.com/dblock).
2
17
 
3
18
  #### Fixes
4
19
 
20
+ * [#2670](https://github.com/ruby-grape/grape/pull/2670): Fix `UnknownAuthStrategy` raised when custom auth strategy class inherits from `Grape::Middleware::Auth::Base` - [@dblock](https://github.com/dblock).
5
21
  * [#2655](https://github.com/ruby-grape/grape/pull/2655): Fix `before_each` method to handle `nil` parameter correctly - [@ericproulx](https://github.com/ericproulx).
22
+ * [#2660](https://github.com/ruby-grape/grape/pull/2660): Fix thread safety: move mutable `ParamsScope` state (`index`, `params_meeting_dependency`) into a per-request `ParamScopeTracker` stored in `Fiber[]` - [@ericproulx](https://github.com/ericproulx).
23
+ * [#2666](https://github.com/ruby-grape/grape/pull/2666): Endpoint cleanup and minor optimizations - [@ericproulx](https://github.com/ericproulx).
24
+ * [#2676](https://github.com/ruby-grape/grape/pull/2676): Exclude ruby 3.2 for rails_edge - [@ericproulx](https://github.com/ericproulx).
25
+ * [#2677](https://github.com/ruby-grape/grape/pull/2677): Update actions/checkout to v6 - [@ericproulx](https://github.com/ericproulx).
6
26
 
7
27
  ### 3.1.0 (2026-01-25)
8
28
 
data/README.md CHANGED
@@ -4,163 +4,13 @@
4
4
  [![test](https://github.com/ruby-grape/grape/actions/workflows/test.yml/badge.svg)](https://github.com/ruby-grape/grape/actions/workflows/test.yml)
5
5
  [![Coverage Status](https://coveralls.io/repos/github/ruby-grape/grape/badge.svg?branch=master)](https://coveralls.io/github/ruby-grape/grape?branch=master)
6
6
 
7
- ## Table of Contents
8
-
9
- - [What is Grape?](#what-is-grape)
10
- - [Stable Release](#stable-release)
11
- - [Project Resources](#project-resources)
12
- - [Grape for Enterprise](#grape-for-enterprise)
13
- - [Installation](#installation)
14
- - [Basic Usage](#basic-usage)
15
- - [Rails 7.1](#rails-71)
16
- - [Mounting](#mounting)
17
- - [All](#all)
18
- - [Rack](#rack)
19
- - [Alongside Sinatra (or other frameworks)](#alongside-sinatra-or-other-frameworks)
20
- - [Rails](#rails)
21
- - [Zeitwerk](#zeitwerk)
22
- - [Modules](#modules)
23
- - [Remounting](#remounting)
24
- - [Mount Configuration](#mount-configuration)
25
- - [Versioning](#versioning)
26
- - [Strategies](#strategies)
27
- - [Path](#path)
28
- - [Header](#header)
29
- - [Accept-Version Header](#accept-version-header)
30
- - [Param](#param)
31
- - [Linting](#linting)
32
- - [Bug in Rack::ETag under Rack 3.X](#bug-in-racketag-under-rack-3x)
33
- - [Describing Methods](#describing-methods)
34
- - [Configuration](#configuration)
35
- - [Parameters](#parameters)
36
- - [Params Class](#params-class)
37
- - [Declared](#declared)
38
- - [Include Parent Namespaces](#include-parent-namespaces)
39
- - [Include Missing](#include-missing)
40
- - [Evaluate Given](#evaluate-given)
41
- - [Parameter Precedence](#parameter-precedence)
42
- - [Parameter Validation and Coercion](#parameter-validation-and-coercion)
43
- - [Supported Parameter Types](#supported-parameter-types)
44
- - [Integer/Fixnum and Coercions](#integerfixnum-and-coercions)
45
- - [Custom Types and Coercions](#custom-types-and-coercions)
46
- - [Multipart File Parameters](#multipart-file-parameters)
47
- - [First-Class JSON Types](#first-class-json-types)
48
- - [Multiple Allowed Types](#multiple-allowed-types)
49
- - [Validation of Nested Parameters](#validation-of-nested-parameters)
50
- - [Dependent Parameters](#dependent-parameters)
51
- - [Group Options](#group-options)
52
- - [Renaming](#renaming)
53
- - [Built-in Validators](#built-in-validators)
54
- - [allow_blank](#allow_blank)
55
- - [values](#values)
56
- - [except_values](#except_values)
57
- - [same_as](#same_as)
58
- - [length](#length)
59
- - [regexp](#regexp)
60
- - [mutually_exclusive](#mutually_exclusive)
61
- - [exactly_one_of](#exactly_one_of)
62
- - [at_least_one_of](#at_least_one_of)
63
- - [all_or_none_of](#all_or_none_of)
64
- - [Nested mutually_exclusive, exactly_one_of, at_least_one_of, all_or_none_of](#nested-mutually_exclusive-exactly_one_of-at_least_one_of-all_or_none_of)
65
- - [Namespace Validation and Coercion](#namespace-validation-and-coercion)
66
- - [Custom Validators](#custom-validators)
67
- - [Validation Errors](#validation-errors)
68
- - [I18n](#i18n)
69
- - [Custom Validation messages](#custom-validation-messages)
70
- - [presence, allow_blank, values, regexp](#presence-allow_blank-values-regexp)
71
- - [same_as](#same_as-1)
72
- - [length](#length-1)
73
- - [all_or_none_of](#all_or_none_of-1)
74
- - [mutually_exclusive](#mutually_exclusive-1)
75
- - [exactly_one_of](#exactly_one_of-1)
76
- - [at_least_one_of](#at_least_one_of-1)
77
- - [Coerce](#coerce)
78
- - [With Lambdas](#with-lambdas)
79
- - [Pass symbols for i18n translations](#pass-symbols-for-i18n-translations)
80
- - [Overriding Attribute Names](#overriding-attribute-names)
81
- - [With Default](#with-default)
82
- - [Using dry-validation or dry-schema](#using-dry-validation-or-dry-schema)
83
- - [Headers](#headers)
84
- - [Request](#request)
85
- - [Header Case Handling](#header-case-handling)
86
- - [Response](#response)
87
- - [Routes](#routes)
88
- - [Helpers](#helpers)
89
- - [Path Helpers](#path-helpers)
90
- - [Parameter Documentation](#parameter-documentation)
91
- - [Cookies](#cookies)
92
- - [HTTP Status Code](#http-status-code)
93
- - [Redirecting](#redirecting)
94
- - [Recognizing Path](#recognizing-path)
95
- - [Allowed Methods](#allowed-methods)
96
- - [Raising Exceptions](#raising-exceptions)
97
- - [Default Error HTTP Status Code](#default-error-http-status-code)
98
- - [Handling 404](#handling-404)
99
- - [Exception Handling](#exception-handling)
100
- - [Rescuing exceptions inside namespaces](#rescuing-exceptions-inside-namespaces)
101
- - [Unrescuable Exceptions](#unrescuable-exceptions)
102
- - [Exceptions that should be rescued explicitly](#exceptions-that-should-be-rescued-explicitly)
103
- - [Logging](#logging)
104
- - [API Formats](#api-formats)
105
- - [JSONP](#jsonp)
106
- - [CORS](#cors)
107
- - [Content-type](#content-type)
108
- - [API Data Formats](#api-data-formats)
109
- - [JSON and XML Processors](#json-and-xml-processors)
110
- - [RESTful Model Representations](#restful-model-representations)
111
- - [Grape Entities](#grape-entities)
112
- - [Hypermedia and Roar](#hypermedia-and-roar)
113
- - [Rabl](#rabl)
114
- - [Active Model Serializers](#active-model-serializers)
115
- - [Sending Raw or No Data](#sending-raw-or-no-data)
116
- - [Authentication](#authentication)
117
- - [Basic Auth](#basic-auth)
118
- - [Register custom middleware for authentication](#register-custom-middleware-for-authentication)
119
- - [Describing and Inspecting an API](#describing-and-inspecting-an-api)
120
- - [Current Route and Endpoint](#current-route-and-endpoint)
121
- - [Before, After and Finally](#before-after-and-finally)
122
- - [Anchoring](#anchoring)
123
- - [Instance Variables](#instance-variables)
124
- - [Using Custom Middleware](#using-custom-middleware)
125
- - [Grape Middleware](#grape-middleware)
126
- - [Rails Middleware](#rails-middleware)
127
- - [Remote IP](#remote-ip)
128
- - [Writing Tests](#writing-tests)
129
- - [Writing Tests with Rack](#writing-tests-with-rack)
130
- - [RSpec](#rspec)
131
- - [Airborne](#airborne)
132
- - [MiniTest](#minitest)
133
- - [Writing Tests with Rails](#writing-tests-with-rails)
134
- - [RSpec](#rspec-1)
135
- - [MiniTest](#minitest-1)
136
- - [Stubbing Helpers](#stubbing-helpers)
137
- - [Reloading API Changes in Development](#reloading-api-changes-in-development)
138
- - [Reloading in Rack Applications](#reloading-in-rack-applications)
139
- - [Reloading in Rails Applications](#reloading-in-rails-applications)
140
- - [Rails 7+ (Zeitwerk)](#rails-7-zeitwerk)
141
- - [Rails 6 and Earlier](#rails-6-and-earlier)
142
- - [Performance Monitoring](#performance-monitoring)
143
- - [Active Support Instrumentation](#active-support-instrumentation)
144
- - [Hook Points](#hook-points)
145
- - [endpoint_run.grape](#endpoint_rungrape)
146
- - [endpoint_render.grape](#endpoint_rendergrape)
147
- - [endpoint_run_filters.grape](#endpoint_run_filtersgrape)
148
- - [endpoint_run_validators.grape](#endpoint_run_validatorsgrape)
149
- - [format_response.grape](#format_responsegrape)
150
- - [Subscribe to Hooks](#subscribe-to-hooks)
151
- - [Monitoring Products](#monitoring-products)
152
- - [Contributing to Grape](#contributing-to-grape)
153
- - [Security](#security)
154
- - [License](#license)
155
- - [Copyright](#copyright)
156
-
157
7
  ## What is Grape?
158
8
 
159
9
  Grape is a REST-like API framework for Ruby. It's designed to run on Rack or complement existing web application frameworks such as Rails and Sinatra by providing a simple DSL to easily develop RESTful APIs. It has built-in support for common conventions, including multiple formats, subdomain/prefix restriction, content negotiation, versioning and much more.
160
10
 
161
11
  ## Stable Release
162
12
 
163
- You're reading the documentation for the stable release of Grape, 3.1.1.
13
+ You're reading the documentation for the stable release of Grape, 3.2.0.
164
14
 
165
15
  ## Project Resources
166
16
 
@@ -177,7 +27,7 @@ The maintainers of Grape are working with Tidelift to deliver commercial support
177
27
 
178
28
  ## Installation
179
29
 
180
- Ruby 3.1 or newer is required.
30
+ Ruby 3.2 or newer is required.
181
31
 
182
32
  Grape is available as a gem, to install it run:
183
33
 
@@ -1923,9 +1773,9 @@ end
1923
1773
  ```ruby
1924
1774
  class AlphaNumeric < Grape::Validations::Validators::Base
1925
1775
  def validate_param!(attr_name, params)
1926
- unless params[attr_name] =~ /\A[[:alnum:]]+\z/
1927
- raise Grape::Exceptions::Validation.new params: [@scope.full_name(attr_name)], message: 'must consist of alpha-numeric characters'
1928
- end
1776
+ return if params[attr_name].match?(/\A[[:alnum:]]+\z/)
1777
+
1778
+ validation_error!(attr_name, 'must consist of alpha-numeric characters')
1929
1779
  end
1930
1780
  end
1931
1781
  ```
@@ -1941,9 +1791,9 @@ You can also create custom classes that take parameters.
1941
1791
  ```ruby
1942
1792
  class Length < Grape::Validations::Validators::Base
1943
1793
  def validate_param!(attr_name, params)
1944
- unless params[attr_name].length <= @option
1945
- raise Grape::Exceptions::Validation.new params: [@scope.full_name(attr_name)], message: "must be at the most #{@option} characters long"
1946
- end
1794
+ return if params[attr_name].length <= @options
1795
+
1796
+ validation_error!(attr_name, "must be at the most #{@options} characters long")
1947
1797
  end
1948
1798
  end
1949
1799
  ```
@@ -1965,10 +1815,10 @@ class Admin < Grape::Validations::Validators::Base
1965
1815
  # @attrs being [:admin_field] and once with @attrs being [:admin_false_field]
1966
1816
  return unless request.params.key?(@attrs.first)
1967
1817
  # check if admin flag is set to true
1968
- return unless @option
1818
+ return unless @options
1969
1819
  # check if user is admin or not
1970
1820
  # as an example get a token from request and check if it's admin or not
1971
- raise Grape::Exceptions::Validation.new params: @attrs, message: 'Can not set admin-only field.' unless request.headers['X-Access-Token'] == 'admin'
1821
+ validation_error!(@attrs, 'Can not set admin-only field.') unless request.headers['X-Access-Token'] == 'admin'
1972
1822
  end
1973
1823
  end
1974
1824
  ```
@@ -1983,7 +1833,50 @@ params do
1983
1833
  end
1984
1834
  ```
1985
1835
 
1986
- Every validation will have its own instance of the validator, which means that the validator can have a state.
1836
+ Each validator is instantiated once at route definition time and frozen. Any setup (option parsing, message building) should happen in `initialize`, not in `validate_param!` or `validate`.
1837
+
1838
+ #### Available helpers
1839
+
1840
+ The following protected/private helpers are available in any `Grape::Validations::Validators::Base` subclass:
1841
+
1842
+ | Helper | Description |
1843
+ |---|---|
1844
+ | `default_message_key(key)` | Class-level macro. Declares the default I18n key for `validation_error!`. A per-option `:message` override still takes precedence. |
1845
+ | `validation_error!(attr_name_or_params, message = @exception_message)` | Raises `Grape::Exceptions::Validation`. Accepts a single attribute name or a pre-computed array of full param names. |
1846
+ | `@options` | The validator option value, deep-frozen at initialization. |
1847
+ | `@attrs` | Frozen array of attribute names this validator applies to. |
1848
+ | `@scope` | The `ParamsScope` — use `@scope.full_name(attr_name)` for the fully-qualified param name. |
1849
+ | `option_value` | Returns `@options[:value]` if present, otherwise `@options`. |
1850
+ | `options_key?(key)` | Returns true if `@options` is a hash with a non-nil `key`. |
1851
+ | `hash_like?(obj)` | Returns true if `obj` responds to `key?`. |
1852
+ | `scrub(value)` | Returns `value` with invalid byte sequences scrubbed. |
1853
+ | `translate(key, **opts)` | I18n lookup with `:en` fallback and `grape.errors.messages` scope. Called at request time to respect per-request locale. |
1854
+
1855
+ Use `default_message_key` for a fixed I18n key. The message is resolved once at route definition time via `message`, so a per-option `:message` override still wins:
1856
+
1857
+ ```ruby
1858
+ class SpecialValidator < Grape::Validations::Validators::Base
1859
+ default_message_key :special
1860
+
1861
+ def validate_param!(attr_name, params)
1862
+ return if valid?(params[attr_name])
1863
+
1864
+ validation_error!(attr_name)
1865
+ end
1866
+ end
1867
+ ```
1868
+
1869
+ For interpolated messages that must respect per-request locale, call `translate` directly inside `validate_param!`:
1870
+
1871
+ ```ruby
1872
+ class SpecialValidator < Grape::Validations::Validators::Base
1873
+ def validate_param!(attr_name, params)
1874
+ return if valid?(params[attr_name])
1875
+
1876
+ validation_error!(attr_name, translate(:special, min: 2, max: 10))
1877
+ end
1878
+ end
1879
+ ```
1987
1880
 
1988
1881
  ### Validation Errors
1989
1882
 
@@ -2045,6 +1938,28 @@ Grape supports I18n for parameter-related error messages, but will fallback to E
2045
1938
 
2046
1939
  In case your app enforces available locales only and :en is not included in your available locales, Grape cannot fall back to English and will return the translation key for the error message. To avoid this behaviour, either provide a translation for your default locale or add :en to your available locales.
2047
1940
 
1941
+ Custom validators that inherit from `Grape::Validations::Validators::Base` have access to a `translate` helper (see `Grape::Util::Translation`) and should use it instead of calling `I18n` directly. It applies the same `:en` fallback as built-in validators, defaults `scope` to `'grape.errors.messages'`, and handles interpolation without needing `format`:
1942
+
1943
+ ```ruby
1944
+ # Good — scope defaults to 'grape.errors.messages', interpolation forwarded automatically
1945
+ translate(:special, min: 2, max: 10)
1946
+
1947
+ # Bad — format is unnecessary and risks conflicting with I18n reserved keys
1948
+ format I18n.t(:special, scope: 'grape.errors.messages'), min: 2, max: 10
1949
+ ```
1950
+
1951
+ Example custom validator using an interpolated i18n message:
1952
+
1953
+ ```ruby
1954
+ class SpecialValidator < Grape::Validations::Validators::Base
1955
+ def validate_param!(attr_name, params)
1956
+ return if valid?(params[attr_name])
1957
+
1958
+ validation_error!(attr_name, translate(:special, min: 2, max: 10))
1959
+ end
1960
+ end
1961
+ ```
1962
+
2048
1963
  ### Custom Validation messages
2049
1964
 
2050
1965
  Grape supports custom validation messages for parameter-related and coerce-related error messages.
data/UPGRADING.md CHANGED
@@ -1,6 +1,112 @@
1
1
  Upgrading Grape
2
2
  ===============
3
3
 
4
+ ### Upgrading to >= 3.2
5
+
6
+ #### Rack parameter parsing errors now raise `Grape::Exceptions::RequestError`
7
+
8
+ Rack errors raised during parameter parsing (malformed multipart, parameter type conflicts, encoding issues, etc.) are now wrapped in `Grape::Exceptions::RequestError` instead of their previous specific exception classes (`Grape::Exceptions::EmptyMessageBody`, `Grape::Exceptions::TooManyMultipartFiles`, `Grape::Exceptions::TooDeepParameters`, `Grape::Exceptions::ConflictingTypes`, `Grape::Exceptions::InvalidParameters`). Those classes have been removed.
9
+
10
+ If you rescue any of these specific exceptions, update your rescue clauses to use `Grape::Exceptions::RequestError`:
11
+
12
+ ```ruby
13
+ # Before
14
+ rescue Grape::Exceptions::ConflictingTypes, Grape::Exceptions::TooDeepParameters => e
15
+ # ...
16
+
17
+ # After
18
+ rescue Grape::Exceptions::RequestError => e
19
+ # ...
20
+ ```
21
+
22
+ The error message is now forwarded directly from Rack rather than translated through Grape's locale system. On Rack 3, all Rack bad-request errors share the `Rack::BadRequest` marker module and are covered by a single rescue.
23
+
24
+ #### `endpoint_run_validators.grape` notification no longer fired when there are no validators
25
+
26
+ `ActiveSupport::Notifications` subscribers listening to `endpoint_run_validators.grape` will no longer receive an event for endpoints that have no validators. If you rely on this notification to measure every request, subscribe to `endpoint_run.grape` instead, which always fires.
27
+
28
+ #### Custom validators: use `default_message_key` and `validation_error!`
29
+
30
+ Validators are now instantiated once at definition time and frozen. Any setup should happen in `initialize`, not in `validate_param!`.
31
+
32
+ If your custom validator did work in `validate_param!` that only depends on the validator's options (not the param value), move it to `initialize`. A common case is compiling a value derived from options — for example, building a `Regexp`. Previously this may have been cached back into `@options`, which now raises `FrozenError` since `@options` and its nested values are deep-frozen by the base class:
33
+
34
+ **Before:**
35
+ ```ruby
36
+ class MyValidator < Grape::Validations::Validators::Base
37
+ def validate_param!(attr_name, params)
38
+ # raises FrozenError: @options is frozen, cannot store compiled pattern back into it
39
+ @options[:compiled] ||= Regexp.new(@options[:pattern])
40
+ validation_error!(attr_name) unless params[attr_name].match?(@options[:compiled])
41
+ end
42
+ end
43
+ ```
44
+
45
+ **After:**
46
+ ```ruby
47
+ class MyValidator < Grape::Validations::Validators::Base
48
+ def initialize(attrs, options, required, scope, opts)
49
+ super
50
+ @pattern = Regexp.new(@options[:pattern]).freeze
51
+ end
52
+
53
+ def validate_param!(attr_name, params)
54
+ validation_error!(attr_name) unless params[attr_name].match?(@pattern)
55
+ end
56
+ end
57
+ ```
58
+
59
+ Any Array or Hash derived from options and stored in an ivar should be frozen, since the validator instance is shared across requests. `@options` itself (and any nested Hash/Array/String values within it) is deep-frozen by the base class, so mutations like `@options[:values] << 'extra'` will also raise a `FrozenError`.
60
+
61
+ #### Custom validators: rename `@option` to `@options`
62
+
63
+ The instance variable holding the validator's option value has been renamed from `@option` to `@options`. `@option` remains as an alias for backwards compatibility but will be removed in the next major release. Update any custom validators to use `@options` instead.
64
+
65
+ Several new helpers are available — see [Available helpers](README.md#available-helpers) in the README for full documentation and examples.
66
+
67
+ #### `with` now uses keyword arguments
68
+
69
+ The `with` DSL method now uses `**opts` instead of a positional hash. Calls using bare keyword syntax are unaffected:
70
+
71
+ ```ruby
72
+ # still works
73
+ with(type: String, documentation: { in: 'body' }) { ... }
74
+ ```
75
+
76
+ However, passing an explicit hash literal will now raise an `ArgumentError`:
77
+
78
+ ```ruby
79
+ # raises ArgumentError
80
+ with({ type: String }) { ... }
81
+ ```
82
+
83
+ See [#2663](https://github.com/ruby-grape/grape/pull/2663) for more information.
84
+
85
+ #### Custom validators: use `translate` instead of `I18n` directly
86
+
87
+ `Grape::Util::Translation` is now included in `Grape::Validations::Validators::Base`. Custom validators that previously called `I18n.t` or `I18n.translate` directly should switch to the `translate`, which provides the same `:en` fallback logic used by all built-in validators.
88
+
89
+ Key points:
90
+ - `scope` defaults to `'grape.errors.messages'` — no need to specify it for standard error message keys.
91
+ - Interpolation variables are passed directly to I18n.
92
+ - `format` is no longer needed — `translate` returns the fully interpolated string.
93
+
94
+ ```ruby
95
+ # Before
96
+ raise Grape::Exceptions::Validation.new(
97
+ params: [@scope.full_name(attr_name)],
98
+ message: format(I18n.t(:my_key, scope: 'grape.errors.messages'), min: 2, max: 10)
99
+ )
100
+
101
+ # After
102
+ raise Grape::Exceptions::Validation.new(
103
+ params: [@scope.full_name(attr_name)],
104
+ message: translate(:my_key, min: 2, max: 10)
105
+ )
106
+ ```
107
+
108
+ See [#2662](https://github.com/ruby-grape/grape/pull/2662) for more information.
109
+
4
110
  ### Upgrading to >= 3.1
5
111
 
6
112
  #### Explicit kwargs for `namespace` and `route_param`
data/grape.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
20
20
  'rubygems_mfa_required' => 'true'
21
21
  }
22
22
 
23
- s.add_dependency 'activesupport', '>= 7.1'
23
+ s.add_dependency 'activesupport', '>= 7.2'
24
24
  s.add_dependency 'dry-configurable'
25
25
  s.add_dependency 'dry-types', '>= 1.1'
26
26
  s.add_dependency 'mustermann-grape', '~> 1.1.0'
@@ -29,5 +29,5 @@ Gem::Specification.new do |s|
29
29
 
30
30
  s.files = Dir['lib/**/*', 'CHANGELOG.md', 'CONTRIBUTING.md', 'README.md', 'grape.png', 'UPGRADING.md', 'LICENSE', 'grape.gemspec']
31
31
  s.require_paths = ['lib']
32
- s.required_ruby_version = '>= 3.1'
32
+ s.required_ruby_version = '>= 3.2'
33
33
  end
@@ -170,7 +170,7 @@ module Grape
170
170
  allow_header = namespace_inheritable[:do_not_route_options] ? allowed_methods : [Rack::OPTIONS] | allowed_methods
171
171
  last_route.app.options[:options_route_enabled] = true unless namespace_inheritable[:do_not_route_options] || allowed_methods.include?(Rack::OPTIONS)
172
172
 
173
- greedy_route = Grape::Router::GreedyRoute.new(last_route.pattern, endpoint: last_route.app, allow_header: allow_header)
173
+ greedy_route = Grape::Router::GreedyRoute.new(last_route.pattern, endpoint: last_route.app, allow_header:)
174
174
  @router.associate_routes(greedy_route)
175
175
  end
176
176
  end
data/lib/grape/api.rb CHANGED
@@ -44,7 +44,7 @@ module Grape
44
44
  def override_all_methods!
45
45
  (base_instance.methods - Class.methods - NON_OVERRIDABLE).each do |method_override|
46
46
  define_singleton_method(method_override) do |*args, **kwargs, &block|
47
- add_setup(method: method_override, args: args, kwargs: kwargs, block: block)
47
+ add_setup(method: method_override, args:, kwargs:, block:)
48
48
  end
49
49
  end
50
50
  end
@@ -12,9 +12,9 @@ module Grape
12
12
  def call(passed_params, declared_params, route_params, renamed_params)
13
13
  recursive_declared(
14
14
  passed_params,
15
- declared_params: declared_params,
16
- route_params: route_params,
17
- renamed_params: renamed_params
15
+ declared_params:,
16
+ route_params:,
17
+ renamed_params:
18
18
  )
19
19
  end
20
20
 
@@ -23,7 +23,7 @@ module Grape
23
23
  raise MethodNotYetAvailable unless before_filter_passed
24
24
 
25
25
  contract_key_map = inheritable_setting.namespace_stackable[:contract_key_map]
26
- handler = DeclaredParamsHandler.new(include_missing:, evaluate_given:, stringify:, contract_key_map: contract_key_map)
26
+ handler = DeclaredParamsHandler.new(include_missing:, evaluate_given:, stringify:, contract_key_map:)
27
27
  declared_params = include_parent_namespaces ? inheritable_setting.route[:declared_params] : (inheritable_setting.namespace_stackable[:declared_params].last || [])
28
28
  renamed_params = inheritable_setting.route[:renamed_params] || {}
29
29
  route_params = options.dig(:route_options, :params) || {} # options = endpoint's option
@@ -55,7 +55,7 @@ module Grape
55
55
  endpoint_config = defined?(configuration) ? configuration : nil
56
56
  Grape::Util::ApiDescription.new(description, endpoint_config, &config_block).settings
57
57
  else
58
- options.merge(description: description)
58
+ options.merge(description:)
59
59
  end
60
60
  inheritable_setting.namespace[:description] = settings
61
61
  inheritable_setting.route[:description] = settings
@@ -29,11 +29,11 @@ module Grape
29
29
  status = self.status(status || inheritable_setting.namespace_inheritable[:default_error_status])
30
30
  headers = additional_headers.present? ? header.merge(additional_headers) : header
31
31
  throw :error,
32
- message: message,
33
- status: status,
34
- headers: headers,
35
- backtrace: backtrace,
36
- original_exception: original_exception
32
+ message:,
33
+ status:,
34
+ headers:,
35
+ backtrace:,
36
+ original_exception:
37
37
  end
38
38
 
39
39
  # Redirect to a new url.
@@ -70,12 +70,12 @@ module Grape
70
70
  when Integer
71
71
  @status = status
72
72
  when nil
73
- return @status if instance_variable_defined?(:@status) && @status
73
+ return @status if @status
74
74
 
75
75
  if request.post?
76
76
  201
77
77
  elsif request.delete?
78
- if instance_variable_defined?(:@body) && @body.present?
78
+ if @body.present?
79
79
  200
80
80
  else
81
81
  204
@@ -114,7 +114,7 @@ module Grape
114
114
  @body = ''
115
115
  status 204
116
116
  else
117
- instance_variable_defined?(:@body) ? @body : nil
117
+ @body
118
118
  end
119
119
  end
120
120
 
@@ -272,7 +272,7 @@ module Grape
272
272
  # @return the representation of the given object as done through
273
273
  # the given entity_class.
274
274
  def entity_representation_for(entity_class, object, options)
275
- embeds = { env: env }
275
+ embeds = { env: }
276
276
  embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
277
277
  entity_class.represent(object, **embeds, **options)
278
278
  end
@@ -124,13 +124,13 @@ module Grape
124
124
  # end
125
125
  def requires(*attrs, **opts, &block)
126
126
  opts[:presence] = { value: true, message: opts[:message] }
127
- opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group
127
+ opts = @group.deep_merge(opts) if @group
128
128
 
129
129
  if opts[:using]
130
- require_required_and_optional_fields(attrs.first, opts)
130
+ require_required_and_optional_fields(attrs.first, using: opts[:using], except: opts[:except])
131
131
  else
132
- validate_attributes(attrs, opts, &block)
133
- block ? new_scope(attrs, opts, &block) : push_declared_params(attrs, opts.slice(:as))
132
+ validate_attributes(attrs, **opts, &block)
133
+ block ? new_scope(attrs.first, type: opts[:type], as: opts[:as], &block) : push_declared_params(attrs, as: opts[:as])
134
134
  end
135
135
  end
136
136
 
@@ -140,7 +140,7 @@ module Grape
140
140
  # @option (see #requires)
141
141
  def optional(*attrs, **opts, &block)
142
142
  type = opts[:type]
143
- opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group
143
+ opts = @group.deep_merge(opts) if @group
144
144
 
145
145
  # check type for optional parameter group
146
146
  if attrs && block
@@ -149,25 +149,25 @@ module Grape
149
149
  end
150
150
 
151
151
  if opts[:using]
152
- require_optional_fields(attrs.first, opts)
152
+ require_optional_fields(attrs.first, using: opts[:using], except: opts[:except])
153
153
  else
154
- validate_attributes(attrs, opts, &block)
154
+ validate_attributes(attrs, **opts, &block)
155
155
 
156
- block ? new_scope(attrs, opts, true, &block) : push_declared_params(attrs, opts.slice(:as))
156
+ block ? new_scope(attrs.first, type: opts[:type], as: opts[:as], optional: true, &block) : push_declared_params(attrs, as: opts[:as])
157
157
  end
158
158
  end
159
159
 
160
160
  # Define common settings for one or more parameters
161
161
  # @param (see #requires)
162
162
  # @option (see #requires)
163
- def with(*attrs, &)
164
- new_group_attrs = [@group, attrs.clone.first].compact.reduce(&:deep_merge)
165
- new_group_scope([new_group_attrs], &)
163
+ def with(**opts, &)
164
+ new_group_attrs = @group&.deep_merge(opts) || opts
165
+ new_group_scope(new_group_attrs, &)
166
166
  end
167
167
 
168
168
  %i[mutually_exclusive exactly_one_of at_least_one_of all_or_none_of].each do |validator|
169
169
  define_method validator do |*attrs, message: nil|
170
- validates(attrs, validator => { value: true, message: message })
170
+ validates(attrs, validator => { value: true, message: })
171
171
  end
172
172
  end
173
173
 
@@ -224,8 +224,8 @@ module Grape
224
224
  # @return hash of parameters relevant for the current scope
225
225
  # @api private
226
226
  def params(params)
227
- params = @parent.params_meeting_dependency.presence || @parent.params(params) if instance_variable_defined?(:@parent) && @parent
228
- params = map_params(params, @element) if instance_variable_defined?(:@element) && @element
227
+ params = @parent.qualifying_params.presence || @parent.params(params) if @parent
228
+ params = map_params(params, @element) if @element
229
229
  params
230
230
  end
231
231