Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5a187d6
Update TargetRubyVersion to 3.4 in .rubocop.yml
moskvin May 11, 2026
136301d
Rename Grape::Entity classes for clarity and update documentation ref…
moskvin May 11, 2026
1d7ac40
[cop] Add SwaggerRouting and SwaggerDocumentationAdder modules
moskvin May 11, 2026
01f95c7
Update CHANGELOG for Ruby 3.4 and refactor of swagger documentation m…
moskvin May 11, 2026
7a0c5fd
Enhance route parameter handling with fallback support and add unit t…
moskvin May 11, 2026
5b4a33c
Add fallback handling for path param extraction errors in route specs
moskvin May 11, 2026
f87cb69
Add deprecation warning for additionalProperties option in parse_para…
moskvin May 11, 2026
3684fca
Enhance path parameter extraction with fallback support and add unit …
moskvin May 11, 2026
dab48e4
Add handling for ignored fallback path parameters and enhance route s…
moskvin May 11, 2026
605c128
:ambulance: Remove weird fallback path parameter handling and clean u…
moskvin May 12, 2026
f25c70c
Remove outdated Ruby and Grape version combinations from CI matrix
moskvin May 12, 2026
9c4d7fe
:sparkles: Add tests for fulfilling path parameters with symbol and s…
moskvin May 12, 2026
1c02eff
:recycle: Refactor Swagger documentation module to use GrapeSwagger n…
moskvin May 17, 2026
4f65550
:sparkles: Normalize path parameter keys to use symbols and update re…
moskvin May 17, 2026
a8031df
:recycle: Simplify route matching logic in swagger_routing.rb
moskvin May 17, 2026
e8951c6
:sparkles: Update grape dependency to require version >= 2.4.0 and ad…
moskvin May 17, 2026
e3f0a0e
:recycle: Remove unused block variable in combine_namespace_routes me…
moskvin May 17, 2026
ab83973
:sparkles: Add test for merging namespace options with symbol-keyed r…
moskvin May 17, 2026
f4b2031
Fix fulfill_params call in route_spec to pass variant_types argument
numbata Jun 23, 2026
69ea255
Keep Grape >= 2.1 support and restore CI matrix for 2.1.x and 2.2.x
numbata Jun 24, 2026
65324da
Add top-level compatibility aliases for renamed modules
numbata Jun 24, 2026
3aa5b73
Fix innermost namespace options being overwritten by outer scopes
numbata Jun 24, 2026
ada4977
Fix regex injection, hoist standalone_namespaces, simplify pattern ar…
numbata Jun 24, 2026
a2494d0
Escape mount_path in regex and use dup instead of clone for endpoints
numbata Jun 24, 2026
f2d02bc
Merge outer and inner namespace path-param options deeply
numbata Jun 24, 2026
8565b66
Simplify standalone_namespaces predicate and add regex-escape regress…
numbata Jun 24, 2026
85012ce
Mark top-level SwaggerRouting and SwaggerDocumentationAdder as deprec…
numbata Jun 24, 2026
2967310
Fix namespace boundary check, reload safety, and minor cleanups
numbata Jun 24, 2026
7f23562
Normalize all repeated slash runs in full_namespace, not just the first
numbata Jun 24, 2026
57ba533
Add slash-normalization regression spec and clarifying comment
numbata Jun 24, 2026
b8dfefe
Remove conditional guard from compatibility aliases and add regressio…
numbata Jun 25, 2026
211586f
Merge duplicate #976 CHANGELOG entry into a single Features line
numbata Jun 25, 2026
311ed9b
Add routing edge case coverage
numbata Jun 25, 2026
02cb1a1
Remove redundant inline comments from combine_namespace_routes
numbata Jun 25, 2026
1f8a015
Fix RuboCop offenses in swagger_routing: line length and redundant co…
numbata Jun 25, 2026
8ebb560
Drop ruby 3.2 + grape HEAD matrix entry: Grape HEAD requires Ruby >= 3.3
numbata Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,22 @@ jobs:
- { ruby: '3.3', grape: '2.2.0' }
- { ruby: '3.4', grape: '2.2.0' }
- { ruby: 'head', grape: '2.2.0' }
- { ruby: '3.1', grape: '2.4.0' }
- { ruby: '3.2', grape: '2.4.0' }
- { ruby: '3.3', grape: '2.4.0' }
- { ruby: '3.4', grape: '2.4.0' }
- { ruby: '3.1', grape: '3.0.1' }
- { ruby: '3.2', grape: '3.0.1' }
- { ruby: '3.3', grape: '3.0.1' }
- { ruby: '3.4', grape: '3.0.1' }
- { ruby: '3.1', grape: '3.1.1' }
- { ruby: '3.2', grape: '3.1.1' }
- { ruby: '3.3', grape: '3.1.1' }
- { ruby: '3.4', grape: '3.1.1' }
- { ruby: '3.2', grape: '3.2.1' }
- { ruby: '3.3', grape: '3.2.1' }
- { ruby: '3.4', grape: '3.2.1' }
- { ruby: 'head', grape: '3.2.1' }
- { ruby: '3.3', grape: 'HEAD' }
- { ruby: '3.4', grape: 'HEAD' }
name: test (ruby=${{ matrix.entry.ruby }}, grape=${{ matrix.entry.grape }})
Expand Down
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ AllCops:
- example/**/*
UseCache: true
NewCops: enable
TargetRubyVersion: 3.3
TargetRubyVersion: 3.4
SuggestExtensions: false

# Layout stuff
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
### 2.2.0 (Next)

#### Features

* [#976](https://github.com/ruby-grape/grape-swagger/pull/976): Ruby 3.4 and refactor swagger documentation modules; deprecate top-level `SwaggerRouting` and `SwaggerDocumentationAdder` aliases in favor of `GrapeSwagger::...` - [@moskvin](https://github.com/moskvin).
* Your contribution here.

#### Fixes

* [#978](https://github.com/ruby-grape/grape-swagger/pull/978): Fix Grape 3.2+ compatibility: desc kwargs, custom types, multi-type param recovery; bump Grape to `>= 2.1, < 5.0`. See [UPGRADING](UPGRADING.md) - [@numbata](https://github.com/numbata).
* Your contribution here.

Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ This screenshot is based on the [Hussars](https://github.com/LeFnord/hussars) sa
The following versions of grape, grape-entity and grape-swagger can currently be used together.

| grape-swagger | swagger spec | grape | grape-entity | representable |
| --------------------- | ------------ | ----------------------- | ------------ | ------------- |
|-----------------------|--------------|-------------------------|--------------|---------------|
| 0.10.5 | 1.2 | >= 0.10.0 ... <= 0.14.0 | < 0.5.0 | n/a |
| 0.11.0 | 1.2 | >= 0.16.2 | < 0.5.0 | n/a |
| 0.25.2 | 2.0 | >= 0.14.0 ... <= 0.18.0 | <= 0.6.0 | >= 2.4.1 |
Expand All @@ -123,7 +123,6 @@ The following versions of grape, grape-entity and grape-swagger can currently be
| 0.32.0 | 2.0 | >= 0.16.2 | >= 0.5.0 | >= 2.4.1 |
| 0.34.0 | 2.0 | >= 0.16.2 ... < 1.3.0 | >= 0.5.0 | >= 2.4.1 |
| >= 1.0.0 | 2.0 | >= 1.3.0 | >= 0.5.0 | >= 2.4.1 |
| >= 2.0.0 | 2.0 | >= 1.7.0 | >= 0.5.0 | >= 2.4.1 |
| >= 2.0.0 ... <= 2.1.2 | 2.0 | >= 1.8.0 ... < 2.3.0 | >= 0.5.0 | >= 2.4.1 |
| >= 2.1.3 ... < 2.2.0 | 2.0 | >= 1.8.0 ... < 4.0 | >= 0.5.0 | >= 2.4.1 |
| >= 2.2.0 | 2.0 | >= 2.1 ... < 5.0 | >= 0.5.0 | >= 2.4.1 |
Expand Down
1 change: 1 addition & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### 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.
- **`SwaggerRouting` and `SwaggerDocumentationAdder` are now also namespaced under `GrapeSwagger::`**. The top-level constants remain as deprecated compatibility aliases for now and are planned for removal in grape-swagger 3.0; prefer `GrapeSwagger::SwaggerRouting` and `GrapeSwagger::SwaggerDocumentationAdder` in downstream code.
- **`type: 'Object'` (and other string type names) in `params` blocks**: Grape 3.2+ rejects string type names. If you previously declared a swagger-only documentation hint via `params { optional :foo, type: 'Object' }`, move the type under `documentation:`:

```ruby
Expand Down
170 changes: 8 additions & 162 deletions lib/grape-swagger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
require 'grape-swagger/doc_methods'
require 'grape-swagger/model_parsers'
require 'grape-swagger/request_param_parser_registry'
require 'grape-swagger/swagger_routing'
require 'grape-swagger/swagger_documentation_adder'
require 'grape-swagger/token_owner_resolver'

module GrapeSwagger
Expand Down Expand Up @@ -44,166 +46,10 @@ def request_param_parsers
}.freeze
end

module SwaggerRouting
private
# Temporary compatibility aliases for downstream code that still references
# the pre-namespace constants directly.
SwaggerRouting = GrapeSwagger::SwaggerRouting
SwaggerDocumentationAdder = GrapeSwagger::SwaggerDocumentationAdder
Object.send(:deprecate_constant, :SwaggerRouting, :SwaggerDocumentationAdder)

def combine_routes(app, doc_klass)
app.routes.each_with_object({}) do |route, combined_routes|
route_path = route.path
route_match = route_path.split(/^.*?#{route.prefix}/).last
next unless route_match

# want to match emojis … ;)
# route_match = route_match
# .match('\/([\p{Alnum}p{Emoji}\-\_]*?)[\.\/\(]') || route_match.match('\/([\p{Alpha}\p{Emoji}\-\_]*)$')
route_match = route_match.match('\/([\p{Alnum}\-\_]*?)[\.\/\(]') || route_match.match('\/([\p{Alpha}\-\_]*)$')
next unless route_match

resource = route_match.captures.first
resource = '/' if resource.empty?
combined_routes[resource] ||= []
next if doc_klass.hide_documentation_path && route.path.match(/#{doc_klass.mount_path}($|\/|\(\.)/)

combined_routes[resource] << route
end
end

def determine_namespaced_routes(name, parent_route, routes)
return routes.values.flatten if parent_route.nil?

parent_route.select do |route|
route_path_start_with?(route, name) || route_namespace_equals?(route, name)
end
end

def combine_namespace_routes(namespaces, routes)
combined_namespace_routes = {}
# iterate over each single namespace
namespaces.each_key do |name, _|
# get the parent route for the namespace
parent_route_name = extract_parent_route(name)
parent_route = routes[parent_route_name]
# fetch all routes that are within the current namespace
namespace_routes = determine_namespaced_routes(name, parent_route, routes)

# default case when not explicitly specified or nested == true
standalone_namespaces = namespaces.reject do |_, ns|
!ns.options.key?(:swagger) ||
!ns.options[:swagger].key?(:nested) ||
ns.options[:swagger][:nested] != false
end

parent_standalone_namespaces = standalone_namespaces.select { |ns_name, _| name.start_with?(ns_name) }
# add only to the main route
# if the namespace is not within any other namespace appearing as standalone resource
# rubocop:disable Style/Next
if parent_standalone_namespaces.empty?
# default option, append namespace methods to parent route
combined_namespace_routes[parent_route_name] ||= []
combined_namespace_routes[parent_route_name].push(*namespace_routes)
end
# rubocop:enable Style/Next
end

combined_namespace_routes
end

def extract_parent_route(name)
route_name = name.match(%r{^/?([^/]*).*$})[1]
return route_name unless route_name.include? ':'

matches = name.match(/\/\p{Alpha}+/)
matches.nil? ? route_name : matches[0].delete('/')
end

def route_namespace_equals?(route, name)
patterns = Enumerator.new do |yielder|
yielder << "/#{name}"
yielder << "/:version/#{name}"
end

patterns.any? { |p| route.namespace == p }
end

def route_path_start_with?(route, name)
patterns = Enumerator.new do |yielder|
if route.prefix
yielder << "/#{route.prefix}/#{name}"
yielder << "/#{route.prefix}/:version/#{name}"
else
yielder << "/#{name}"
yielder << "/:version/#{name}"
end
end

patterns.any? { |p| route.path.start_with?(p) }
end
end

module SwaggerDocumentationAdder
attr_accessor :combined_namespaces, :combined_routes, :combined_namespace_routes

include SwaggerRouting

def add_swagger_documentation(options = {})
documentation_class = create_documentation_class

version_for(options)
options = { target_class: self }.merge(options)
@target_class = options[:target_class]
auth_wrapper = options[:endpoint_auth_wrapper] || Class.new

use auth_wrapper if auth_wrapper.method_defined?(:before) && !middleware.flatten.include?(auth_wrapper)

documentation_class.setup(options)
mount(documentation_class)

combined_routes = combine_routes(@target_class, documentation_class)
combined_namespaces = combine_namespaces(@target_class)
combined_namespace_routes = combine_namespace_routes(combined_namespaces, combined_routes)
exclusive_route_keys = combined_routes.keys - combined_namespaces.keys
@target_class.combined_namespace_routes = combined_namespace_routes.merge(
combined_routes.slice(*exclusive_route_keys)
)
@target_class.combined_routes = combined_routes
@target_class.combined_namespaces = combined_namespaces

documentation_class
end

private

def version_for(options)
options[:version] = version if version
end

def combine_namespaces(app)
combined_namespaces = {}
endpoints = app.endpoints.clone

while endpoints.any?
endpoint = endpoints.shift

endpoints.push(*endpoint.options[:app].endpoints) if endpoint.options[:app]
namespace_stackable = endpoint.inheritable_setting.namespace_stackable
ns = (namespace_stackable[:namespace] || []).last
next unless ns

# use the full namespace here (not the latest level only)
# and strip leading slash
mount_path = (namespace_stackable[:mount_path] || []).join('/')
full_namespace = (mount_path + endpoint.namespace).sub(/\/{2,}/, '/').sub(/^\//, '')
combined_namespaces[full_namespace] = ns
end

combined_namespaces
end

def create_documentation_class
Class.new(GrapeInstance) do
extend GrapeSwagger::DocMethods
end
end
end

GrapeInstance.extend(SwaggerDocumentationAdder)
GrapeInstance.extend(GrapeSwagger::SwaggerDocumentationAdder)
23 changes: 19 additions & 4 deletions lib/grape-swagger/request_param_parsers/route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,35 @@ def build_path_params(stackable_values)
params = {}

while stackable_values.is_a?(Grape::Util::StackableValues)
params.merge!(fetch_inherited_params(stackable_values))
params = merge_path_params(fetch_inherited_params(stackable_values), params)
stackable_values = stackable_values.inherited_values
end

params
end

def merge_path_params(outer_params, inner_params)
outer_params.merge(inner_params) do |_key, outer_options, inner_options|
merge_path_param_options(outer_options, inner_options)
end
end

def merge_path_param_options(outer_options, inner_options)
return inner_options unless outer_options.is_a?(Hash) && inner_options.is_a?(Hash)

outer_options.merge(inner_options) do |_key, outer_value, inner_value|
merge_path_param_options(outer_value, inner_value)
end
end

def fetch_inherited_params(stackable_values)
return {} unless stackable_values.new_values

namespaces = stackable_values.new_values[:namespace] || []

namespaces.each_with_object({}) do |namespace, params|
space = namespace.space.to_s.gsub(':', '')
params[space] = namespace.options || {}
space = namespace.space.to_s.delete_prefix(':')
params[space.to_sym] = namespace.options || {}
end
end

Expand Down Expand Up @@ -110,7 +124,8 @@ def fulfill_params(path_params, variant_types)

defined_options = definition.is_a?(Hash) ? definition : {}
defined_options = restore_variant_type(defined_options, param, variant_types)
value = (path_params[param] || {}).merge(defined_options)
path_options = path_params[key] || {}
value = path_options.merge(defined_options)
accum[key] = value.empty? ? DEFAULT_PARAM_TYPE : value
end
end
Expand Down
71 changes: 71 additions & 0 deletions lib/grape-swagger/swagger_documentation_adder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

require_relative 'swagger_routing'

module GrapeSwagger
module SwaggerDocumentationAdder
attr_accessor :combined_namespaces, :combined_routes, :combined_namespace_routes

include GrapeSwagger::SwaggerRouting

def add_swagger_documentation(options = {})
documentation_class = create_documentation_class

version_for(options)
options = { target_class: self }.merge(options)
@target_class = options[:target_class]
auth_wrapper = options[:endpoint_auth_wrapper] || Class.new

use auth_wrapper if auth_wrapper.method_defined?(:before) && !middleware.flatten.include?(auth_wrapper)

documentation_class.setup(options)
mount(documentation_class)

combined_routes = combine_routes(@target_class, documentation_class)
combined_namespaces = combine_namespaces(@target_class)
combined_namespace_routes = combine_namespace_routes(combined_namespaces, combined_routes)
exclusive_route_keys = combined_routes.keys - combined_namespaces.keys
@target_class.combined_namespace_routes = combined_namespace_routes.merge(
combined_routes.slice(*exclusive_route_keys)
)
@target_class.combined_routes = combined_routes
@target_class.combined_namespaces = combined_namespaces

documentation_class
end

private

def version_for(options)
options[:version] = version if version
end

def combine_namespaces(app)
combined_namespaces = {}
endpoints = app.endpoints.dup

while endpoints.any?
endpoint = endpoints.shift

endpoints.push(*endpoint.options[:app].endpoints) if endpoint.options[:app]
namespace_stackable = endpoint.inheritable_setting.namespace_stackable
ns = (namespace_stackable[:namespace] || []).last
next unless ns

# use the full namespace here (not the latest level only)
# and strip leading slash
mount_path = (namespace_stackable[:mount_path] || []).join('/')
full_namespace = (mount_path + endpoint.namespace).gsub(/\/{2,}/, '/').sub(/^\//, '')
combined_namespaces[full_namespace] = ns
end

combined_namespaces
end

def create_documentation_class
Class.new(GrapeInstance) do
extend GrapeSwagger::DocMethods
end
end
end
end
Loading
Loading