Skip to content

Commit 0942165

Browse files
committed
Address review findings: scope-aware variant keys, robust desc kwargs
VariantCollectionCoercer recovery (route.rb) - Build the variant-type map keyed by the fully-qualified param name (e.g. "group[inner]") via the validator's @scope.full_name(attr), so nested-param multi-types are restored instead of being looked up under the wrong key, and same-named outer params at a different scope are not clobbered. - Use the public validator.attrs reader, and normalise the recovered @types to an Array so an order-defined coercer Set cannot leak in. - Cover the inheritance shape of stackable[:validations] with .flatten. desc keyword-args (doc_methods.rb) - Coerce api_documentation / specific_api_documentation keys to symbols via transform_keys before splatting, so string-keyed Hashes (YAML/JSON configs) no longer raise TypeError under `**`. - Accept :description as an alias for :desc. Without this, a user-supplied :description was forwarded into Grape's desc and immediately overwritten with nil because the positional description arg was missing. Tests - Add a regression spec asserting `type: [Integer, Float]` produces "integer" (the only assertion that proves the recovery actually wires the real types through; previously [String, Integer] also "worked" via the broken fallback because String happens to be first). - Cover the nested-namespace case (`group { requires :inner, type: [...] }`) to lock in the scope-aware key. CHANGELOG - Document the >= 2.1 Grape floor bump and the `type: 'Object'` migration pattern under a Breaking changes section, and split the multi-type recovery and desc-kwargs robustness fixes into separate entries.
1 parent 54465f4 commit 0942165

4 files changed

Lines changed: 51 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@
44

55
* Your contribution here.
66

7+
#### Breaking changes
8+
9+
* Minimum required Grape version is now `>= 2.1` (was `>= 1.7`). Grape 1.8.0 and 2.0.0 do not work on Ruby 3.3+ due to a Mustermann private-method incompatibility (the existing CI rows for those versions had been failing on master). Their CI rows are removed.
10+
* On Grape 3.2+ the `params` block rejects string type names. If you used `params { optional :foo, type: 'Object' }` to declare a swagger-only documentation hint, move the type into the `documentation:` hash: `optional :foo, documentation: { type: 'Object' }`. grape-swagger picks the type up from the merged settings unchanged.
11+
712
#### Fixes
813

914
* [#977](https://github.com/ruby-grape/grape-swagger/issues/977): Pass keyword arguments to `desc` to fix deprecation warning from Grape - [@numbata](https://github.com/numbata).
15+
* Accept string-keyed `api_documentation` / `specific_api_documentation` and the `:description` key as an alias for `:desc`, matching the pre-keyword-args behaviour of `desc`.
16+
* Grape 3.3+: recover the real type list for multi-type params (`type: [A, B]`). Grape now wraps these in a `VariantCollectionCoercer` and serialises it via `#to_s` in `route.params`, which lost the original types. Swagger output now reflects the first declared type (e.g. `type: [Integer, Float]` produces `"integer"`) instead of the coercer's `#inspect` string.
1017

1118
### 2.1.4 (2026-02-02)
1219

lib/grape-swagger/doc_methods.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,14 @@ def setup(options)
8686
# for available options see #defaults
8787
target_class = options[:target_class]
8888
guard = options[:swagger_endpoint_guard]
89-
api_doc = options[:api_documentation].dup
90-
specific_api_doc = options[:specific_api_documentation].dup
89+
api_doc = options[:api_documentation].transform_keys(&:to_sym)
90+
specific_api_doc = options[:specific_api_documentation].transform_keys(&:to_sym)
9191

9292
class_variables_from(options)
9393

9494
setup_formatter(options[:format])
9595

96-
desc api_doc.delete(:desc), **api_doc
96+
desc(pop_desc(api_doc), **api_doc)
9797

9898
instance_eval(guard) unless guard.nil?
9999

@@ -105,7 +105,7 @@ def setup(options)
105105
.output_path_definitions(target_class.combined_namespace_routes, self, target_class, options)
106106
end
107107

108-
desc specific_api_doc.delete(:desc), params: specific_api_doc.delete(:params) || {}, **specific_api_doc
108+
desc(pop_desc(specific_api_doc), params: specific_api_doc.delete(:params) || {}, **specific_api_doc)
109109

110110
params do
111111
requires :name, type: String, desc: 'Resource name of mounted API'
@@ -136,5 +136,9 @@ def setup_formatter(formatter)
136136

137137
FORMATTER_METHOD.each { |method| send(method, formatter) }
138138
end
139+
140+
def pop_desc(doc)
141+
doc.delete(:desc) || doc.delete(:description)
142+
end
139143
end
140144
end

lib/grape-swagger/request_param_parsers/route.rb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,22 +52,24 @@ def fetch_inherited_params(stackable_values)
5252
# Grape's documentation pipeline serialises it via `#to_s`, so the original
5353
# type list is lost in route.params. The live coercer is still reachable
5454
# through the CoerceValidator's @converter, so we rebuild a name => types
55-
# map and restore it in fulfill_params.
55+
# map keyed by the fully-qualified param name (e.g. "group[inner]") to match
56+
# route.params keys and avoid clobbering same-named params at outer scopes.
5657
def collect_variant_types(stackable_values)
5758
variant_types = {}
5859
return variant_types unless defined?(Grape::Validations::Types::VariantCollectionCoercer) &&
5960
defined?(Grape::Validations::Validators::CoerceValidator) &&
6061
stackable_values.respond_to?(:[])
6162

62-
Array(stackable_values[:validations]).each do |validator|
63+
Array(stackable_values[:validations]).flatten.each do |validator|
6364
next unless validator.is_a?(Grape::Validations::Validators::CoerceValidator)
6465

6566
converter = validator.instance_variable_get(:@converter)
6667
next unless converter.is_a?(Grape::Validations::Types::VariantCollectionCoercer)
6768

68-
types = converter.instance_variable_get(:@types)
69-
Array(validator.instance_variable_get(:@attrs)).each do |attr|
70-
variant_types[attr.to_s] = types
69+
types = Array(converter.instance_variable_get(:@types))
70+
scope = validator.instance_variable_get(:@scope)
71+
validator.attrs.each do |attr|
72+
variant_types[scope.full_name(attr)] = types
7173
end
7274
end
7375

spec/swagger_v2/param_multi_type_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,35 @@ def app
5050
]
5151
end
5252

53+
describe 'with non-string primary type' do
54+
def app
55+
Class.new(Grape::API) do
56+
format :json
57+
58+
desc 'action' do
59+
consumes ['application/x-www-form-urlencoded']
60+
end
61+
params do
62+
requires :int_first, type: [Integer, Float]
63+
requires :nested_group, type: Hash do
64+
requires :nested_int, type: [Integer, Float]
65+
end
66+
end
67+
post :action do
68+
{ message: 'hi' }
69+
end
70+
71+
add_swagger_documentation
72+
end
73+
end
74+
75+
it 'recovers the first variant type, not a hardcoded fallback' do
76+
types = subject.to_h { |p| [p['name'], p['type']] }
77+
expect(types['int_first']).to eq('integer')
78+
expect(types['nested_group[nested_int]']).to eq('integer')
79+
end
80+
end
81+
5382
describe 'header params' do
5483
def app
5584
Class.new(Grape::API) do

0 commit comments

Comments
 (0)