grape 3.2.1 → 3.3.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/README.md +116 -43
  4. data/UPGRADING.md +336 -1
  5. data/grape.gemspec +5 -5
  6. data/lib/grape/api/instance.rb +7 -7
  7. data/lib/grape/api.rb +22 -25
  8. data/lib/grape/cookies.rb +2 -6
  9. data/lib/grape/declared_params_handler.rb +48 -50
  10. data/lib/grape/dsl/callbacks.rb +9 -3
  11. data/lib/grape/dsl/desc.rb +8 -2
  12. data/lib/grape/dsl/entity.rb +88 -0
  13. data/lib/grape/dsl/helpers.rb +27 -7
  14. data/lib/grape/dsl/inside_route.rb +38 -129
  15. data/lib/grape/dsl/logger.rb +3 -5
  16. data/lib/grape/dsl/parameters.rb +32 -38
  17. data/lib/grape/dsl/request_response.rb +53 -48
  18. data/lib/grape/dsl/rescue_options.rb +24 -0
  19. data/lib/grape/dsl/routing.rb +51 -35
  20. data/lib/grape/dsl/settings.rb +14 -8
  21. data/lib/grape/dsl/version_options.rb +23 -0
  22. data/lib/grape/endpoint/options.rb +19 -0
  23. data/lib/grape/endpoint.rb +96 -68
  24. data/lib/grape/env.rb +1 -3
  25. data/lib/grape/error_formatter/base.rb +23 -20
  26. data/lib/grape/error_formatter/json.rb +8 -4
  27. data/lib/grape/error_formatter/txt.rb +10 -10
  28. data/lib/grape/exceptions/base.rb +3 -1
  29. data/lib/grape/exceptions/error_response.rb +45 -0
  30. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  31. data/lib/grape/exceptions/validation.rb +14 -0
  32. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  33. data/lib/grape/exceptions/validation_errors.rb +12 -20
  34. data/lib/grape/formatter/serializable_hash.rb +5 -9
  35. data/lib/grape/json.rb +38 -2
  36. data/lib/grape/locale/en.yml +2 -0
  37. data/lib/grape/middleware/auth/base.rb +2 -3
  38. data/lib/grape/middleware/auth/dsl.rb +23 -8
  39. data/lib/grape/middleware/base.rb +22 -33
  40. data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
  41. data/lib/grape/middleware/error.rb +152 -62
  42. data/lib/grape/middleware/formatter.rb +66 -50
  43. data/lib/grape/middleware/precomputed_content_types.rb +46 -0
  44. data/lib/grape/middleware/stack.rb +5 -6
  45. data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
  46. data/lib/grape/middleware/versioner/base.rb +34 -38
  47. data/lib/grape/middleware/versioner/header.rb +3 -5
  48. data/lib/grape/middleware/versioner/path.rb +8 -3
  49. data/lib/grape/namespace.rb +3 -3
  50. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  51. data/lib/grape/parser/json.rb +1 -1
  52. data/lib/grape/path.rb +14 -17
  53. data/lib/grape/request.rb +15 -8
  54. data/lib/grape/router/mustermann_pattern.rb +44 -0
  55. data/lib/grape/router/pattern.rb +6 -10
  56. data/lib/grape/router.rb +28 -42
  57. data/lib/grape/serve_stream/file_body.rb +1 -0
  58. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  59. data/lib/grape/serve_stream/stream_response.rb +1 -0
  60. data/lib/grape/testing.rb +33 -0
  61. data/lib/grape/util/base_inheritable.rb +13 -16
  62. data/lib/grape/util/inheritable_setting.rb +44 -27
  63. data/lib/grape/util/inheritable_values.rb +7 -3
  64. data/lib/grape/util/lazy/base.rb +16 -0
  65. data/lib/grape/util/lazy/block.rb +2 -9
  66. data/lib/grape/util/lazy/value.rb +2 -9
  67. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  68. data/lib/grape/util/media_type.rb +1 -4
  69. data/lib/grape/util/path_normalizer.rb +34 -0
  70. data/lib/grape/util/registry.rb +1 -1
  71. data/lib/grape/util/stackable_values.rb +11 -8
  72. data/lib/grape/validations/attributes_iterator.rb +13 -13
  73. data/lib/grape/validations/coerce_options.rb +21 -0
  74. data/lib/grape/validations/oneof_collector.rb +39 -0
  75. data/lib/grape/validations/param_scope_tracker.rb +14 -9
  76. data/lib/grape/validations/params_documentation.rb +25 -23
  77. data/lib/grape/validations/params_scope.rb +54 -172
  78. data/lib/grape/validations/shared_options.rb +19 -0
  79. data/lib/grape/validations/types/array_coercer.rb +2 -2
  80. data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
  81. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  82. data/lib/grape/validations/types/dry_type_coercer.rb +3 -3
  83. data/lib/grape/validations/types/primitive_coercer.rb +10 -5
  84. data/lib/grape/validations/types/set_coercer.rb +1 -1
  85. data/lib/grape/validations/types/variant_collection_coercer.rb +8 -0
  86. data/lib/grape/validations/types.rb +23 -30
  87. data/lib/grape/validations/validations_spec.rb +149 -0
  88. data/lib/grape/validations/validators/all_or_none_of_validator.rb +1 -1
  89. data/lib/grape/validations/validators/at_least_one_of_validator.rb +1 -1
  90. data/lib/grape/validations/validators/base.rb +39 -22
  91. data/lib/grape/validations/validators/coerce_validator.rb +5 -3
  92. data/lib/grape/validations/validators/default_validator.rb +7 -8
  93. data/lib/grape/validations/validators/except_values_validator.rb +3 -2
  94. data/lib/grape/validations/validators/length_validator.rb +1 -1
  95. data/lib/grape/validations/validators/multiple_params_base.rb +10 -7
  96. data/lib/grape/validations/validators/oneof_validator.rb +49 -0
  97. data/lib/grape/validations/validators/values_validator.rb +5 -5
  98. data/lib/grape/version.rb +1 -1
  99. data/lib/grape/xml.rb +8 -1
  100. data/lib/grape.rb +6 -6
  101. metadata +34 -18
  102. data/lib/grape/middleware/globals.rb +0 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81a1d1ef86854a9cd7fe4d60a1207fba921bdee9f6fd53e2f9e83c648e85c2e0
4
- data.tar.gz: 7a19d899e17d65141c9ce69a10e518a8d7ccec01cd1239f7c2557b77a4084ad4
3
+ metadata.gz: 533a2014e54eb4dd9c7932addb910af89c6d99f5717133cb081ba7e6d44676cc
4
+ data.tar.gz: bd0195007c0d69f4b92a2ca7651ff5e861639dc7a480540dece110877713573a
5
5
  SHA512:
6
- metadata.gz: b9ffeac5636c40b3ed53323c26eb7c5f525be4bf8268b85e4e8851cbbf2d3118e8ddc7387f81f06ef1342396cfe3af9562b5920e38548b5bf1de00536c9b136c
7
- data.tar.gz: 7fed8d88f46a2d5af508fc0ab5e68333c3b2bde482b9e7b393ea12f2e38bb08b6d3fc9bd8c9f9dfe08adccc2816f2faaf89810d16138f803d1b009b1d0940f38
6
+ metadata.gz: d2967df7dd98a67aee9f0923883f4bec90a107292fd6fd6201a26d9c5aad9961fd75323cfad5ce0e3008925434a613f61590f41dadc73f5c06939cba4719a929
7
+ data.tar.gz: a3c9d88a32f75330d680fecbec0a01a8425acf742381c76831fd674fa440e5aae0591c72e58dc8b21e7bf5978fd6f1c865487eba110d5dd47410ab91ff624536
data/CHANGELOG.md CHANGED
@@ -1,3 +1,83 @@
1
+ ### 3.3.0 (2026-06-20)
2
+
3
+ #### Features
4
+
5
+ * [#2679](https://github.com/ruby-grape/grape/pull/2679): Extract entity dsl and refactor :with to keyword argument - [@ericproulx](https://github.com/ericproulx).
6
+ * [#2681](https://github.com/ruby-grape/grape/pull/2681): Extract `Grape::Endpoint.before_each` into `Grape::Testing` - [@ericproulx](https://github.com/ericproulx).
7
+ * [#2686](https://github.com/ruby-grape/grape/pull/2686): Add `Grape::Middleware::PrecomputedContentTypes` to warm content-type caches on the parent middleware instance (opt-in via `include`); short-circuit `Middleware::Base#merge_headers` when no headers were set - [@ericproulx](https://github.com/ericproulx).
8
+ * [#2683](https://github.com/ruby-grape/grape/pull/2683): Introduce `Grape::Util::Lazy::Base` for unified lazy-type dispatch - [@ericproulx](https://github.com/ericproulx).
9
+ * [#2685](https://github.com/ruby-grape/grape/pull/2685): Skip `run_filters` and `endpoint_run_filters.grape` instrumentation when the filter list is empty - [@ericproulx](https://github.com/ericproulx).
10
+ * [#2684](https://github.com/ruby-grape/grape/pull/2684): Readability refactors: case/when, guard clauses, small cleanups - [@ericproulx](https://github.com/ericproulx).
11
+ * [#2687](https://github.com/ruby-grape/grape/pull/2687): Skip backtrace capture on internal validation exceptions - [@ericproulx](https://github.com/ericproulx).
12
+ * [#2688](https://github.com/ruby-grape/grape/pull/2688): Consolidate user-registered rescue handler lookup into `Middleware::Error#registered_rescue_handler` backed by a shared `rescue_handler_from` primitive - [@ericproulx](https://github.com/ericproulx).
13
+ * [#2689](https://github.com/ruby-grape/grape/pull/2689): Avoid empty-hash merges on request hot paths - [@ericproulx](https://github.com/ericproulx).
14
+ * [#2690](https://github.com/ruby-grape/grape/pull/2690): Avoid allocating an empty array on every `StackableValues#[]` miss - [@ericproulx](https://github.com/ericproulx).
15
+ * [#2691](https://github.com/ruby-grape/grape/pull/2691): Precompute the prefix list in `Middleware::Versioner::Path` - [@ericproulx](https://github.com/ericproulx).
16
+ * [#2692](https://github.com/ruby-grape/grape/pull/2692): Replace per-request `Proc` allocation in `Router#transaction` with a `halt?` helper - [@ericproulx](https://github.com/ericproulx).
17
+ * [#2694](https://github.com/ruby-grape/grape/pull/2694): Split `Versioner::Base#available_media_types` into an `attr_reader` plus `build_available_media_types` - [@ericproulx](https://github.com/ericproulx).
18
+ * [#2695](https://github.com/ruby-grape/grape/pull/2695): Lift trailing `if/else` into guard clauses - [@ericproulx](https://github.com/ericproulx).
19
+ * [#2698](https://github.com/ruby-grape/grape/pull/2698): Collapse `DSL::RequestResponse#extract_handler` type-dispatch into a `case`/`when` - [@ericproulx](https://github.com/ericproulx).
20
+ * [#2697](https://github.com/ruby-grape/grape/pull/2697): Extract `Grape::Util::PathNormalizer` from `Grape::Router`; `Grape::Router.normalize_path` is now a deprecated alias - [@ericproulx](https://github.com/ericproulx).
21
+ * [#2696](https://github.com/ruby-grape/grape/pull/2696): Reduce per-request allocations on the request hot path; migrate middleware options to `attr_reader` and freeze `@options` post-init - [@ericproulx](https://github.com/ericproulx).
22
+ * [#2693](https://github.com/ruby-grape/grape/pull/2693): Introduce `Grape::Exceptions::ErrorResponse` value object to replace the implicit-schema Hash thrown via `throw` - [@ericproulx](https://github.com/ericproulx).
23
+ * [#2701](https://github.com/ruby-grape/grape/pull/2701): Replace `.tap` usages in `lib/` with explicit local variables - [@ericproulx](https://github.com/ericproulx).
24
+ * [#2704](https://github.com/ruby-grape/grape/pull/2704): Add `Grape::Endpoint#logger` so the API's configured logger is reachable inside route handlers, filters, and `rescue_from` blocks without a helper - [@ericproulx](https://github.com/ericproulx).
25
+ * [#2705](https://github.com/ruby-grape/grape/pull/2705): Add `Grape.config.warn_on_helper_overrides` (off by default) to emit a warning when a helper method masks a `Grape::Endpoint` instance method - [@ericproulx](https://github.com/ericproulx).
26
+ * [#2706](https://github.com/ruby-grape/grape/pull/2706): Refactor `ParamsScope#validates` and `ParamsDocumentation` around a frozen `Grape::Validations::ValidationsSpec` value object; the validations hash supplied by the DSL is no longer mutated and the helper chain becomes pure - [@ericproulx](https://github.com/ericproulx).
27
+ * [#2707](https://github.com/ruby-grape/grape/pull/2707): Tighten six guard conditions in `lib/` via De Morgan and `blank?`/`present?`/`include?` rewrites; no behaviour change - [@ericproulx](https://github.com/ericproulx).
28
+ * [#2708](https://github.com/ruby-grape/grape/pull/2708): Tighten dynamic `define_method` in `DSL::Callbacks` and `DSL::Routing` - [@ericproulx](https://github.com/ericproulx).
29
+ * [#2709](https://github.com/ruby-grape/grape/pull/2709): Lift trailing `if/else` into guard clauses; tighten `Util::Lazy::ValueEnumerable` - [@ericproulx](https://github.com/ericproulx).
30
+ * [#2702](https://github.com/ruby-grape/grape/pull/2702): Add `oneof:` option for `requires`/`optional` to accept a Hash parameter matching one of several variant schemas (resolves [#2385](https://github.com/ruby-grape/grape/issues/2385)) - [@ericproulx](https://github.com/ericproulx).
31
+ * [#2715](https://github.com/ruby-grape/grape/pull/2715): Normalize `==` / `eql?` aliasing across value-like classes - [@ericproulx](https://github.com/ericproulx).
32
+ * [#2710](https://github.com/ruby-grape/grape/pull/2710): Tidy up `Grape::DeclaredParamsHandler` - [@ericproulx](https://github.com/ericproulx).
33
+ * [#2712](https://github.com/ruby-grape/grape/pull/2712): Pass a `Grape::Exceptions::ErrorResponse` value object to `error_formatter#call` instead of separate kwargs - [@ericproulx](https://github.com/ericproulx).
34
+ * [#2714](https://github.com/ruby-grape/grape/pull/2714): Drop unused `Grape::Middleware::Globals` and its `grape.request*` env constants - [@ericproulx](https://github.com/ericproulx).
35
+ * [#2717](https://github.com/ruby-grape/grape/pull/2717): Convert `Grape::Exceptions::ErrorResponse` to a `Data` value object - [@ericproulx](https://github.com/ericproulx).
36
+ * [#2723](https://github.com/ruby-grape/grape/pull/2723): Deprecate passing a positional options Hash to `Grape::DSL::Desc#desc`; pass options as keyword arguments. Add a grape-swagger integration spec - [@ericproulx](https://github.com/ericproulx).
37
+ * [#2722](https://github.com/ruby-grape/grape/pull/2722): Introduce `Grape::Validations::CoerceOptions` value object for the internal coerce options - [@ericproulx](https://github.com/ericproulx).
38
+ * [#2721](https://github.com/ruby-grape/grape/pull/2721): Use an internal `Grape::Validations::SharedOptions` value object in `Validators::Base` (public `opts` Hash contract unchanged) - [@ericproulx](https://github.com/ericproulx).
39
+ * [#2719](https://github.com/ruby-grape/grape/pull/2719): Move content-type helpers from `Middleware::Base` into `PrecomputedContentTypes` - [@ericproulx](https://github.com/ericproulx).
40
+ * [#2716](https://github.com/ruby-grape/grape/pull/2716): Refactor `DSL::Routing#version`: guard clause, explicit kwargs in place of `**options`, and a `Grape::DSL::VersionOptions` value object stored internally - [@ericproulx](https://github.com/ericproulx).
41
+ * [#2720](https://github.com/ruby-grape/grape/pull/2720): Move declaration-coherence checks into `Grape::Validations::ValidationsSpec` - [@ericproulx](https://github.com/ericproulx).
42
+ * [#2725](https://github.com/ruby-grape/grape/pull/2725): Encapsulate `Grape::Validations::Validators::Base` state behind readers; add `required?`/`allow_blank?` predicates - [@ericproulx](https://github.com/ericproulx).
43
+ * [#2726](https://github.com/ruby-grape/grape/pull/2726): Reuse one `AttributesIterator` per validator and drop the unused `Enumerable` mixin - [@ericproulx](https://github.com/ericproulx).
44
+ * [#2728](https://github.com/ruby-grape/grape/pull/2728): Deprecate passing a positional options Hash to `auth`/`http_basic`/`http_digest`; pass keyword arguments instead - [@ericproulx](https://github.com/ericproulx).
45
+ * [#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).
46
+ * [#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).
47
+ * [#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).
48
+ * [#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).
49
+ * [#2749](https://github.com/ruby-grape/grape/pull/2749): Middleware tidy-up — dedupe `Versioner::Base#build_available_media_types` (was emitting `application/vnd.<vendor>-<version>` once per content_type) and assorted `Formatter` cleanups (guard clauses, in-place merges, drop a no-op splat) - [@ericproulx](https://github.com/ericproulx).
50
+ * [#2718](https://github.com/ruby-grape/grape/pull/2718): Generalize middleware options to per-class `Options` `Data` value objects (`Middleware::Error`, `::Formatter`, `::Versioner::Base`); expose them via a new `config` reader, keep `options` Hash for back-compat, deprecate `Options#[]` Hash-style access - [@ericproulx](https://github.com/ericproulx).
51
+ * [#2746](https://github.com/ruby-grape/grape/pull/2746): Hoist `using:` / `except:` from `**opts` to explicit kwargs on `DSL::Parameters#requires` and `#optional` - [@ericproulx](https://github.com/ericproulx).
52
+ * [#2750](https://github.com/ruby-grape/grape/pull/2750): Bump minimum required Ruby to 3.3 - [@ericproulx](https://github.com/ericproulx).
53
+ * [#2742](https://github.com/ruby-grape/grape/pull/2742): Prune unused requires in `lib/grape.rb`; narrow `active_support/inflector` to `core_ext/string/inflections` - [@ericproulx](https://github.com/ericproulx).
54
+ * [#2741](https://github.com/ruby-grape/grape/pull/2741): Readability pass: guard clauses and small extractions across `lib/` - [@ericproulx](https://github.com/ericproulx).
55
+ * [#2740](https://github.com/ruby-grape/grape/pull/2740): Lazy-allocate `@api_class` and `@point_in_time_copies` on `Grape::Util::InheritableSetting` so unused settings layers don't carry an empty Hash and empty Array each - [@ericproulx](https://github.com/ericproulx).
56
+ * [#2739](https://github.com/ruby-grape/grape/pull/2739): Lazy-allocate `@new_values` in `Grape::Util::BaseInheritable` so settings layers that only inherit never carry an empty Hash; readers in `InheritableValues`/`StackableValues` handle nil - [@ericproulx](https://github.com/ericproulx).
57
+ * [#2737](https://github.com/ruby-grape/grape/pull/2737): `rescue_from` raises `ArgumentError` when a meta selector (`:all`, `:grape_exceptions`, `:internal_grape_exceptions`) is mixed with exception classes instead of silently dropping the classes - [@ericproulx](https://github.com/ericproulx).
58
+ * [#2735](https://github.com/ruby-grape/grape/pull/2735): Normalize `Grape::Endpoint#options` into an immutable `Grape::Endpoint::Options` `Data` value object; Hash-style `[]` reads kept for back-compat - [@ericproulx](https://github.com/ericproulx).
59
+ * [#2754](https://github.com/ruby-grape/grape/pull/2754): Merge routing args in place in `Router#process_route` instead of allocating a new Hash via `merge` - [@ericproulx](https://github.com/ericproulx).
60
+ * [#2753](https://github.com/ruby-grape/grape/pull/2753): Lazy-allocate `Grape::Validations::ParamScopeTracker`'s identity-keyed hashes so validating requests that never use the index / qualifying-params trackers allocate no hash - [@ericproulx](https://github.com/ericproulx).
61
+ * [#2752](https://github.com/ruby-grape/grape/pull/2752): Skip per-request `ActiveSupport::Notifications` payload and dispatch when no subscriber is listening, via private `instrument_<event>` guards on `Endpoint`/`Middleware::Formatter` - [@ericproulx](https://github.com/ericproulx).
62
+ * [#2755](https://github.com/ruby-grape/grape/pull/2755): Inline `mustermann-grape` into `Grape::Router::MustermannPattern` and depend on `mustermann` directly - [@ericproulx](https://github.com/ericproulx).
63
+ * [#2757](https://github.com/ruby-grape/grape/pull/2757): Build the `Grape::Cookies` jar only when a cookie is read or written (via a new `Grape::Request#cookies?` predicate gating response-cookie flushing), and drop the jar's now-redundant lazy-parse `Proc` - [@ericproulx](https://github.com/ericproulx).
64
+ * [#2756](https://github.com/ruby-grape/grape/pull/2756): Tighten dependency lower bounds to their compatibility floors (`rack >= 2.2.4`, `zeitwerk >= 2.6`, `dry-configurable >= 1.0`) - [@ericproulx](https://github.com/ericproulx).
65
+ * [#2762](https://github.com/ruby-grape/grape/pull/2762): Parse request bodies with `JSON.parse` in the stdlib JSON fallback, dropping the `create_additions: false` wrapper from #2759 (`JSON.parse` never honours `json_class`) - [@ericproulx](https://github.com/ericproulx).
66
+
67
+ #### Fixes
68
+
69
+ * [#2678](https://github.com/ruby-grape/grape/pull/2678): Update rubocop to 1.86.0 and autocorrect offenses - [@ericproulx](https://github.com/ericproulx).
70
+ * [#2682](https://github.com/ruby-grape/grape/pull/2682): Fix `Style/OptionalBooleanParameter` offenses - [@ericproulx](https://github.com/ericproulx).
71
+ * [#2699](https://github.com/ruby-grape/grape/pull/2699): Fix `Grape::Validations::Types::CustomTypeCoercer` dropping symbolized hash keys for `Array`/`Set` types; refactor the class for readability - [@ericproulx](https://github.com/ericproulx).
72
+ * [#2758](https://github.com/ruby-grape/grape/pull/2758): Fix `VariantCollectionCoercer#to_s` to return `"Array[Type, ...]"` notation instead of a raw object string - [@bogdan](https://github.com/bogdan).
73
+ * [#2700](https://github.com/ruby-grape/grape/pull/2700): Fix README typos, remove obsolete Ruby 2.4 / Fixnum section, and replace incorrect `requires + values + allow_blank` note with a correct one covering `optional + values` semantics (closes #2631) - [@ericproulx](https://github.com/ericproulx).
74
+ * [#2703](https://github.com/ruby-grape/grape/pull/2703): Catch exceptions raised inside `rescue_from` blocks; new `rescue_from :internal_grape_exceptions` opt-in for unrecognised internal errors (resolves [#2482](https://github.com/ruby-grape/grape/issues/2482)) - [@ericproulx](https://github.com/ericproulx).
75
+ * [#2706](https://github.com/ruby-grape/grape/pull/2706): Fix `optional :foo, message: 'oops'` raising `UnknownValidator` - [@ericproulx](https://github.com/ericproulx).
76
+ * [#2751](https://github.com/ruby-grape/grape/pull/2751): Fix structured error messages leaking the raw i18n key for an undefined optional step such as `summary` (closes #2748) - [@ericproulx](https://github.com/ericproulx).
77
+ * [#2759](https://github.com/ruby-grape/grape/pull/2759): Use `create_additions: false` in `Grape::Json.load` to prevent object instantiation via the `json_class` key when using the stdlib JSON fallback - [@dblock](https://github.com/dblock).
78
+ * [#2765](https://github.com/ruby-grape/grape/pull/2765): Detect the `MultiXML` constant to avoid the multi_xml 0.9 `MultiXml` deprecation - [@ericproulx](https://github.com/ericproulx).
79
+ * [#2764](https://github.com/ruby-grape/grape/pull/2764): Route `Grape::Json` through the non-deprecated `MultiJSON` API - [@ericproulx](https://github.com/ericproulx).
80
+
1
81
  ### 3.2.1 (2026-04-16)
2
82
 
3
83
  #### Fixes
data/README.md CHANGED
@@ -10,7 +10,7 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com
10
10
 
11
11
  ## Stable Release
12
12
 
13
- You're reading the documentation for the stable release of Grape, 3.2.1.
13
+ You're reading the documentation for the stable release of Grape, 3.3.0.
14
14
 
15
15
  ## Project Resources
16
16
 
@@ -27,7 +27,7 @@ The maintainers of Grape are working with Tidelift to deliver commercial support
27
27
 
28
28
  ## Installation
29
29
 
30
- Ruby 3.2 or newer is required.
30
+ Ruby 3.3 or newer is required.
31
31
 
32
32
  Grape is available as a gem, to install it run:
33
33
 
@@ -311,7 +311,7 @@ mount ::Some::Api => '/some/api', with: { condition: true }
311
311
 
312
312
  You can access `configuration` on the class (to use as dynamic attributes), inside blocks (like namespace)
313
313
 
314
- If you want logic happening given on an `configuration`, you can use the helper `given`.
314
+ If you want logic happening based on a `configuration`, you can use the helper `given`.
315
315
 
316
316
  ```ruby
317
317
  class ConditionalEndpoint::API < Grape::API
@@ -462,7 +462,7 @@ vnd.vendor-and-or-resource-v1234+format
462
462
 
463
463
  Basically all tokens between the final `-` and the `+` will be interpreted as the version.
464
464
 
465
- Using this versioning strategy, clients should pass the desired version in the HTTP `Accept` head.
465
+ Using this versioning strategy, clients should pass the desired version in the HTTP `Accept` header.
466
466
 
467
467
  curl -H Accept:application/vnd.twitter-v1+json http://localhost:9292/statuses/public_timeline
468
468
 
@@ -484,7 +484,7 @@ Using this versioning strategy, clients should pass the desired version in the H
484
484
 
485
485
  curl -H "Accept-Version:v1" http://localhost:9292/statuses/public_timeline
486
486
 
487
- By default, the first matching version is used when no `Accept-Version` header is supplied. This behavior is similar to routing in Rails. To circumvent this default behavior, one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error is returned when no correct `Accept` header is supplied and the `:cascade` option is set to `false`. Otherwise a `404 Not Found` error is returned by Rack if no other route matches.
487
+ By default, the first matching version is used when no `Accept-Version` header is supplied. This behavior is similar to routing in Rails. To circumvent this default behavior, one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error is returned when no correct `Accept-Version` header is supplied and the `:cascade` option is set to `false`. Otherwise a `404 Not Found` error is returned by Rack if no other route matches.
488
488
 
489
489
  #### Param
490
490
 
@@ -524,13 +524,13 @@ Grape.config.lint = true
524
524
  ```
525
525
 
526
526
  ### Bug in Rack::ETag under Rack 3.X
527
- If you're using Rack 3.X and the `Rack::Etag` middleware (used by [Rails](https://guides.rubyonrails.org/rails_on_rack.html#inspecting-middleware-stack)), a [bug](https://github.com/rack/rack/pull/2324) related to linting has been fixed in [3.1.13](https://github.com/rack/rack/blob/v3.1.13/CHANGELOG.md#3113---2025-04-13) and [3.0.15](https://github.com/rack/rack/blob/v3.1.13/CHANGELOG.md#3015---2025-04-13) respectively.
527
+ If you're using Rack 3.X and the `Rack::ETag` middleware (used by [Rails](https://guides.rubyonrails.org/rails_on_rack.html#inspecting-middleware-stack)), a [bug](https://github.com/rack/rack/pull/2324) related to linting has been fixed in [3.1.13](https://github.com/rack/rack/blob/v3.1.13/CHANGELOG.md#3113---2025-04-13) and [3.0.15](https://github.com/rack/rack/blob/v3.1.13/CHANGELOG.md#3015---2025-04-13) respectively.
528
528
 
529
529
  ## Describing Methods
530
530
 
531
531
  You can add a description to API methods and namespaces. The description would be used by [grape-swagger][grape-swagger] to generate swagger compliant documentation.
532
532
 
533
- Note: Description block is only for documentation and won't affects API behavior.
533
+ Note: Description block is only for documentation and won't affect API behavior.
534
534
 
535
535
  ```ruby
536
536
  desc 'Returns your public timeline.' do
@@ -1182,28 +1182,6 @@ The following are all valid types, supported out of the box by Grape:
1182
1182
  * Rack::Multipart::UploadedFile (alias `File`)
1183
1183
  * JSON
1184
1184
 
1185
- ### Integer/Fixnum and Coercions
1186
-
1187
- Please be aware that the behavior differs between Ruby 2.4 and earlier versions.
1188
- In Ruby 2.4, values consisting of numbers are converted to Integer, but in earlier versions it will be treated as Fixnum.
1189
-
1190
- ```ruby
1191
- params do
1192
- requires :integers, type: Hash do
1193
- requires :int, coerce: Integer
1194
- end
1195
- end
1196
- get '/int' do
1197
- params[:integers][:int].class
1198
- end
1199
-
1200
- ...
1201
-
1202
- get '/int' integers: { int: '45' }
1203
- #=> Integer in ruby 2.4
1204
- #=> Fixnum in earlier ruby versions
1205
- ```
1206
-
1207
1185
  ### Custom Types and Coercions
1208
1186
 
1209
1187
  Aside from the default set of supported types listed above, any class can be used as a type as long as an explicit coercion method is supplied. If the type implements a class-level `parse` method, Grape will use it automatically. This method must take one string argument and return an instance of the correct type, or return an instance of `Grape::Types::InvalidValue` which optionally accepts a message to be returned in the response.
@@ -1356,6 +1334,56 @@ end
1356
1334
  client.get('/', status_codes: %w(1 two)) # => [1, "two"]
1357
1335
  ```
1358
1336
 
1337
+ ### Multiple Hash Schemas with `oneof`
1338
+
1339
+ A Hash parameter that may take one of several different shapes can be declared with the `oneof:` option. Each variant is a `Proc` that uses the normal `params` DSL — so the full validator surface (`requires`, `optional`, `regexp:`, `values:`, `allow_blank:`, nested `Hash`/`Array`, etc.) is available inside.
1340
+
1341
+ ```ruby
1342
+ params do
1343
+ requires :value, type: Hash, oneof: [
1344
+ proc { requires :fixed_price, type: Float },
1345
+ proc do
1346
+ requires :time_unit, type: String
1347
+ requires :rate, type: Float
1348
+ end
1349
+ ]
1350
+ end
1351
+ post '/pricing' do
1352
+ params[:value]
1353
+ end
1354
+ ```
1355
+
1356
+ Both of these requests succeed:
1357
+
1358
+ ```bash
1359
+ curl -d '{"value":{"fixed_price":100.0}}' -H 'Content-Type: application/json' /pricing
1360
+ curl -d '{"value":{"time_unit":"hour","rate":50.0}}' -H 'Content-Type: application/json' /pricing
1361
+ ```
1362
+
1363
+ A request that doesn't match any variant returns `400` with `value does not match any of the allowed schemas`.
1364
+
1365
+ Variants are tried in declaration order; the first variant that validates without errors wins, and any coercions it performed (e.g. `"150.5"` → `150.5`) are applied to the request params. Nested hash structures inside variants work the same as elsewhere in Grape:
1366
+
1367
+ ```ruby
1368
+ params do
1369
+ requires :options, type: Hash, oneof: [
1370
+ proc do
1371
+ requires :form, type: Hash do
1372
+ requires :colour, type: String
1373
+ optional :size, type: Integer
1374
+ end
1375
+ end,
1376
+ proc do
1377
+ requires :api, type: Hash do
1378
+ requires :authenticated, type: Grape::API::Boolean
1379
+ end
1380
+ end
1381
+ ]
1382
+ end
1383
+ ```
1384
+
1385
+ `oneof:` requires `type: Hash`. The variants array must be non-empty and each entry must be a `Proc`; violating either raises an `ArgumentError` at definition time.
1386
+
1359
1387
  ### Validation of Nested Parameters
1360
1388
 
1361
1389
  Parameters can be nested using `group` or by calling `requires` or `optional` with a block.
@@ -1442,7 +1470,7 @@ params do
1442
1470
  end
1443
1471
  ```
1444
1472
 
1445
- You can organize settings into layers using nested `with' blocks. Each layer can use, add to, or change the settings of the layer above it. This helps to keep complex parameters organized and consistent, while still allowing for specific customizations to be made.
1473
+ You can organize settings into layers using nested `with` blocks. Each layer can use, add to, or change the settings of the layer above it. This helps to keep complex parameters organized and consistent, while still allowing for specific customizations to be made.
1446
1474
 
1447
1475
  ```ruby
1448
1476
  params do
@@ -1551,14 +1579,16 @@ end
1551
1579
 
1552
1580
  While Procs are convenient for single cases, consider using [Custom Validators](#custom-validators) in cases where a validation is used more than once.
1553
1581
 
1554
- Note that [allow_blank](#allow_blank) validator applies while using `:values`. In the following example the absence of `:allow_blank` does not prevent `:state` from receiving blank values because `:allow_blank` defaults to `true`.
1582
+ When using `optional` together with `:values`, a missing key, a `nil` value, and any value that coerces to `nil` (such as `""` for `type: Symbol`) all pass validation — `optional` collapses "key may be absent" with "value may be nil". To reject blank values while still allowing the key to be absent, add `allow_blank: false`:
1555
1583
 
1556
1584
  ```ruby
1557
1585
  params do
1558
- requires :state, type: Symbol, values: [:active, :inactive]
1586
+ optional :state, type: Symbol, values: [:active, :inactive], allow_blank: false
1559
1587
  end
1560
1588
  ```
1561
1589
 
1590
+ With `requires`, blank values are already rejected: `requires` enforces presence and `:values` rejects `nil`.
1591
+
1562
1592
  #### `except_values`
1563
1593
 
1564
1594
  Parameters can be restricted from having a specific set of values with the `:except_values` option.
@@ -1919,8 +1949,8 @@ To skip all subsequent validation checks when a specific param is found invalid,
1919
1949
  The following example will not check if `:wine` is present unless it finds `:beer`.
1920
1950
  ```ruby
1921
1951
  params do
1922
- required :beer, fail_fast: true
1923
- required :wine
1952
+ requires :beer, fail_fast: true
1953
+ requires :wine
1924
1954
  end
1925
1955
  ```
1926
1956
  The result of empty params would be a single `Grape::Exceptions::ValidationErrors` error.
@@ -1928,7 +1958,7 @@ The result of empty params would be a single `Grape::Exceptions::ValidationError
1928
1958
  Similarly, no regular expression test will be performed if `:blah` is blank in the following example.
1929
1959
  ```ruby
1930
1960
  params do
1931
- required :blah, allow_blank: false, regexp: /blah/, fail_fast: true
1961
+ requires :blah, allow_blank: false, regexp: /blah/, fail_fast: true
1932
1962
  end
1933
1963
  ```
1934
1964
 
@@ -2597,7 +2627,7 @@ You can set additional headers for the response. They will be merged with header
2597
2627
  error!('Something went wrong', 500, 'X-Error-Detail' => 'Invalid token.')
2598
2628
  ```
2599
2629
 
2600
- You can present documented errors with a Grape entity using the the [grape-entity](https://github.com/ruby-grape/grape-entity) gem.
2630
+ You can present documented errors with a Grape entity using the [grape-entity](https://github.com/ruby-grape/grape-entity) gem.
2601
2631
 
2602
2632
  ```ruby
2603
2633
  module API
@@ -2708,12 +2738,12 @@ end
2708
2738
 
2709
2739
  The error format will match the request format. See "Content-Types" below.
2710
2740
 
2711
- Custom error formatters for existing and additional types can be defined with a proc.
2741
+ Custom error formatters for existing and additional types can be defined with a proc. The formatter receives a `Grape::Exceptions::ErrorResponse` value object as `error:` plus three context kwargs — `env:`, `include_backtrace:`, `include_original_exception:`. Pull just the keys you need with `**` to ignore the rest:
2712
2742
 
2713
2743
  ```ruby
2714
2744
  class Twitter::API < Grape::API
2715
- error_formatter :txt, ->(message, backtrace, options, env, original_exception) {
2716
- "error: #{message} from #{backtrace}"
2745
+ error_formatter :txt, ->(error:, **) {
2746
+ "error #{error.status}: #{error.message} from #{error.backtrace}"
2717
2747
  }
2718
2748
  end
2719
2749
  ```
@@ -2722,8 +2752,8 @@ You can also use a module or class.
2722
2752
 
2723
2753
  ```ruby
2724
2754
  module CustomFormatter
2725
- def self.call(message, backtrace, options, env, original_exception)
2726
- { message: message, backtrace: backtrace }
2755
+ def self.call(error:, **)
2756
+ { status: error.status, message: error.message, backtrace: error.backtrace }
2727
2757
  end
2728
2758
  end
2729
2759
 
@@ -2753,6 +2783,41 @@ class Twitter::API < Grape::API
2753
2783
  end
2754
2784
  ```
2755
2785
 
2786
+ #### Re-raising from inside a `rescue_from` block
2787
+
2788
+ A `rescue_from` block can re-raise an exception to invoke a different handler. This is useful for translating one exception class into another:
2789
+
2790
+ ```ruby
2791
+ class Twitter::API < Grape::API
2792
+ rescue_from Grape::Exceptions::ValidationErrors do |e|
2793
+ raise Api::Exceptions::InvalidValueError, e.full_messages
2794
+ end
2795
+
2796
+ rescue_from Api::Exceptions::InvalidValueError do |e|
2797
+ error!({ errors: e.message }, 422)
2798
+ end
2799
+ end
2800
+ ```
2801
+
2802
+ The first handler re-raises; the second handler runs against the new exception.
2803
+
2804
+ If the re-raised exception has no registered `rescue_from` and is a `Grape::Exceptions::Base` subclass, it is rendered through the default Grape error path (using its own `status` and `message`). Anything else — typos, `NoMethodError`, an unrelated `StandardError` — is treated as an internal error: it is exposed on `env['grape.exception']` for upstream Rack middleware to observe, and rendered to the API consumer as a generic `500 Internal Server Error`. This avoids leaking internal detail in the response body.
2805
+
2806
+ You can take control of the internal-error path by opting in with `rescue_from :internal_grape_exceptions`:
2807
+
2808
+ ```ruby
2809
+ class Twitter::API < Grape::API
2810
+ rescue_from :internal_grape_exceptions do |e|
2811
+ Sentry.capture_exception(e)
2812
+ error!({ message: 'Something went wrong' }, 500)
2813
+ end
2814
+ end
2815
+ ```
2816
+
2817
+ When this handler is registered the framework hands the original exception to you, and you own the response shape and any logging. The framework deliberately does not log internal errors itself — it has no way to know your preferred format or destination.
2818
+
2819
+ A second raise inside the redispatched handler is not redispatched again — it goes straight to the framework's generic 500. This bounds the chain at one redispatch and prevents loops.
2820
+
2756
2821
  You can also rescue all exceptions with a code block and handle the Rack response at the lowest level.
2757
2822
 
2758
2823
  ```ruby
@@ -3967,7 +4032,15 @@ end
3967
4032
 
3968
4033
  ### Stubbing Helpers
3969
4034
 
3970
- Because helpers are mixed in based on the context when an endpoint is defined, it can be difficult to stub or mock them for testing. The `Grape::Endpoint.before_each` method can help by allowing you to define behavior on the endpoint that will run before every request.
4035
+ Because helpers are mixed in based on the context when an endpoint is defined, it can be difficult to stub or mock them for testing. `Grape::Endpoint.before_each` allows you to define behavior on the endpoint that will run before every request.
4036
+
4037
+ This feature is provided by `Grape::Testing`, a standalone module intended for test environments only. Add it to your test helper:
4038
+
4039
+ ```ruby
4040
+ require 'grape/testing'
4041
+ ```
4042
+
4043
+ Then use it in your tests:
3971
4044
 
3972
4045
  ```ruby
3973
4046
  describe 'an endpoint that needs helpers stubbed' do
@@ -3978,7 +4051,7 @@ describe 'an endpoint that needs helpers stubbed' do
3978
4051
  end
3979
4052
 
3980
4053
  after do
3981
- Grape::Endpoint.before_each nil
4054
+ Grape::Endpoint.reset_before_each
3982
4055
  end
3983
4056
 
3984
4057
  it 'stubs the helper' do