From 601650b79b573074579209f72dbd8b150e74141a Mon Sep 17 00:00:00 2001 From: Bogdan Gusiev Date: Tue, 2 Jun 2026 11:53:09 +0200 Subject: [PATCH] Fix documentation for :type Array notation to be different from :types Without a custom to_s, type: [Integer, String] serialised to a garbage object string in route.params[:type], making it indistinguishable from the VariantCollectionCoercer instance address. Documentation tools (grape-swagger, grape-oas) could not tell it apart from the plain-array "[Integer, String]" produced by the types: keyword. Now type: [Integer, String] produces "Array[Integer, String]" and type: Set[Integer, String] produces "Set[Integer, String]", matching the Grape DSL notation and letting tooling generate the correct schema (array with variant-member items vs oneOf at the parameter level). --- CHANGELOG.md | 1 + .../validations/types/variant_collection_coercer.rb | 8 ++++++++ spec/grape/endpoint/declared_spec.rb | 11 ++++++++++- spec/grape/validations/validations_spec_spec.rb | 9 +++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a93d89c0..80d372a44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ * [#2678](https://github.com/ruby-grape/grape/pull/2678): Update rubocop to 1.86.0 and autocorrect offenses - [@ericproulx](https://github.com/ericproulx). * [#2682](https://github.com/ruby-grape/grape/pull/2682): Fix `Style/OptionalBooleanParameter` offenses - [@ericproulx](https://github.com/ericproulx). * [#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). +* [#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). * [#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). * [#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). * [#2706](https://github.com/ruby-grape/grape/pull/2706): Fix `optional :foo, message: 'oops'` raising `UnknownValidator` - [@ericproulx](https://github.com/ericproulx). diff --git a/lib/grape/validations/types/variant_collection_coercer.rb b/lib/grape/validations/types/variant_collection_coercer.rb index 34982e133..41424018a 100644 --- a/lib/grape/validations/types/variant_collection_coercer.rb +++ b/lib/grape/validations/types/variant_collection_coercer.rb @@ -26,6 +26,14 @@ def initialize(types, method = nil) @member_coercer = MultipleTypeCoercer.new types, method end + # Returns the Grape DSL notation for this coercer, e.g. "Array[Integer, String]". + # Distinct from the plain-array string "[Integer, String]" produced by the + # +types:+ keyword, which lets documentation tools tell the two apart. + def to_s + container = @types.is_a?(Set) ? 'Set' : 'Array' + "#{container}[#{@types.join(', ')}]" + end + # Coerce the given value. # # @param value [Array] collection of values to be coerced diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb index cc31bc502..a97536a55 100644 --- a/spec/grape/endpoint/declared_spec.rb +++ b/spec/grape/endpoint/declared_spec.rb @@ -40,6 +40,7 @@ optional :empty_hash_two, type: Hash optional :empty_set, type: Set optional :empty_typed_set, type: Set[String] + optional :empty_variant_set, type: Set[Integer, String] end end @@ -108,7 +109,7 @@ end get '/declared?first=present' expect(last_response).to be_successful - expect(JSON.parse(last_response.body).keys.size).to eq(12) + expect(JSON.parse(last_response.body).keys.size).to eq(13) end it 'has a optional param with default value all the time' do @@ -205,6 +206,14 @@ expect(body['nested']['empty_typed_set']).to eq(json_empty_set) end + it 'sets objects with type=Set[Integer, String] (variant collection) to be a set' do + get '/declared?first=present' + expect(last_response).to be_successful + + body = JSON.parse(last_response.body) + expect(body['empty_variant_set']).to eq(JSON.parse(Set.new.to_json)) + end + it 'sets objects with type=Array to be an array' do get '/declared?first=present' expect(last_response).to be_successful diff --git a/spec/grape/validations/validations_spec_spec.rb b/spec/grape/validations/validations_spec_spec.rb index 8414a8d31..822d2f2fa 100644 --- a/spec/grape/validations/validations_spec_spec.rb +++ b/spec/grape/validations/validations_spec_spec.rb @@ -36,12 +36,21 @@ it 'wraps a multiple type from :type in a VariantCollectionCoercer' do multi_spec = described_class.from(type: [Integer, String]) expect(multi_spec.coerce_type).to be_a(Grape::Validations::Types::VariantCollectionCoercer) + expect(multi_spec.coerce_type.to_s).to eq('Array[Integer, String]') + expect(multi_spec.coerce_method).to be_nil + end + + it 'wraps a Set multiple type from :type in a VariantCollectionCoercer' do + multi_spec = described_class.from(type: Set[Integer, String]) + expect(multi_spec.coerce_type).to be_a(Grape::Validations::Types::VariantCollectionCoercer) + expect(multi_spec.coerce_type.to_s).to eq('Set[Integer, String]') expect(multi_spec.coerce_method).to be_nil end it 'extracts :types as a plain coerce list' do spec = described_class.from(types: [Integer, String]) expect(spec.coerce_type).to eq([Integer, String]) + expect(spec.coerce_type.to_s).to eq('[Integer, String]') end end