From 435b109da5f5b60692cc23ef66ee5be2a1ca5dfe Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 22 Aug 2026 21:38:03 +0200 Subject: [PATCH] Read tags without reaching into route.options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit route.options.key?(:tags) was the last read of a route's raw options Hash. It was there to tell "no tags: given" from "tags: nil", because Grape's route.tags reader answers nil for both. That distinction turns out to be an accident. #523 added route-level tags to override path-derived grouping and read them with route.options.fetch(:tags, tag_object(route)) whose default-on-absence semantics silently gave nil a meaning of its own: suppress the tag. Nothing chose that. The README it added documents only tags: ['tag1', 'tag2'], the spec it added covers only a real list, and no commit in ten years of history mentions tags: nil — the first is #983, which restored the behaviour on the assumption it was intended. It is also inconsistent. For every other desc option nil means "not specified": deprecated_object and security_object read route.options[:x] if route.options.key?(:x), a single expression that is exactly route.options[:x] — the guard is decorative. tags was the only option where presence changed the outcome. So a blank tags: now means "not specified" here too. tags: is an override, and only a non-empty list overrides anything: #presence collapses nil and [] alike, and both fall back to the path-derived tag. That also stops an empty list being documented as "tags": [], which is valid Swagger but says nothing. Both cases are now readable from route.tags alone, so no reader on Grape's side has to expose whether an option was set, and grape-swagger no longer touches route.options at all. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019aqLx595ggku1E2djNDdxW --- CHANGELOG.md | 1 + README.md | 2 ++ UPGRADING.md | 6 ++++++ lib/grape-swagger/endpoint.rb | 2 +- .../endpoint_versioned_path_spec.rb | 21 +++++++++++++++++-- 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd35541b..54d89f15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ #### Fixes +* [#986](https://github.com/ruby-grape/grape-swagger/pull/986): Read a route's tags through `route.tags` instead of `route.options`; a blank `tags:` (`nil` or `[]`) now means "not specified" and derives the tag from the path. See [UPGRADING](UPGRADING.md) - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.2.0 (2026-08-06) diff --git a/README.md b/README.md index 1766ffff..a6a85275 100644 --- a/README.md +++ b/README.md @@ -648,6 +648,8 @@ end Tags are used for logical grouping of operations by resources or any other qualifier. To override the tags array, add `tags: ['tag1', 'tag2']` after the description. +When `tags:` is not given, or is `nil` or empty, the tag is derived from the path. + ```ruby namespace 'order' do desc 'This will be your summary', tags: ['orders'] diff --git a/UPGRADING.md b/UPGRADING.md index 0fd5d24f..f8fb7dbd 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,5 +1,11 @@ ## Upgrading Grape-swagger +### Upgrading to >= 2.3.0 + +- **A blank `desc(..., tags:)` now means "not specified".** `tags: nil` no longer suppresses the tag and `tags: []` no longer emits an empty `"tags": []`; both derive the tag from the path, as if `tags:` had been omitted. `tags:` is an override, and only a non-empty list overrides anything. + + The old `nil` behaviour was never a deliberate one: [#523](https://github.com/ruby-grape/grape-swagger/pull/523) introduced route-level tags to *override* path-derived grouping and read them with `route.options.fetch(:tags, tag_object(route))`, whose default-on-absence semantics happened to give `nil` a meaning of its own. It was never documented or tested. + ### Upgrading to >= 2.2.0 - **Minimum Grape version is now `>= 2.1`** (was `>= 1.7`). Grape 1.8.0 and 2.0.0 cannot be used on Ruby 3.3+ because of an upstream Mustermann/forwardable incompatibility; the CI rows for those combinations were already failing on `master` and have been removed. diff --git a/lib/grape-swagger/endpoint.rb b/lib/grape-swagger/endpoint.rb index 70a8a8ea..fba5fb40 100644 --- a/lib/grape-swagger/endpoint.rb +++ b/lib/grape-swagger/endpoint.rb @@ -123,7 +123,7 @@ def method_object(route, options, path) method[:parameters] = params_object(route, options, path, method[:consumes]) method[:security] = security_object(route) method[:responses] = response_object(route, options) - method[:tags] = route.options.key?(:tags) ? route.tags : tag_object(route, path) + method[:tags] = route.tags.presence || tag_object(route, path) method[:operationId] = GrapeSwagger::DocMethods::OperationId.build(route, path) method[:deprecated] = deprecated_object(route) method.delete_if { |_, value| value.nil? } diff --git a/spec/swagger_v2/endpoint_versioned_path_spec.rb b/spec/swagger_v2/endpoint_versioned_path_spec.rb index d6cc2f1b..65766f08 100644 --- a/spec/swagger_v2/endpoint_versioned_path_spec.rb +++ b/spec/swagger_v2/endpoint_versioned_path_spec.rb @@ -65,8 +65,25 @@ end end - it 'omits the tags key instead of falling back to the default tag' do - expect(subject.first['/v1/item'][:get]).not_to have_key(:tags) + it 'falls back to the default tag' do + expect(subject.first['/v1/item'][:get][:tags]).to eq ['item'] + end + end + + context 'when tags are explicitly empty' do + let(:item) do + Class.new(Grape::API) do + version 'v1', using: :path + + resource :item do + desc 'Item description', tags: [] + get '/' + end + end + end + + it 'falls back to the default tag' do + expect(subject.first['/v1/item'][:get][:tags]).to eq ['item'] end end