Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
443245f
Update TargetRubyVersion to 3.4 in .rubocop.yml
moskvin May 11, 2026
dea80a8
Rename Grape::Entity classes for clarity and update documentation ref…
moskvin May 11, 2026
ca5fdb2
[cop] Add SwaggerRouting and SwaggerDocumentationAdder modules
moskvin May 11, 2026
30b65e2
Update CHANGELOG for Ruby 3.4 and refactor of swagger documentation m…
moskvin May 11, 2026
93c4886
Enhance route parameter handling with fallback support and add unit t…
moskvin May 11, 2026
3ea5f11
Add fallback handling for path param extraction errors in route specs
moskvin May 11, 2026
bc01086
Add deprecation warning for additionalProperties option in parse_para…
moskvin May 11, 2026
7b7a033
Enhance path parameter extraction with fallback support and add unit …
moskvin May 11, 2026
5913ba7
Add handling for ignored fallback path parameters and enhance route s…
moskvin May 11, 2026
c46756f
:ambulance: Remove weird fallback path parameter handling and clean u…
moskvin May 12, 2026
12822ca
Remove outdated Ruby and Grape version combinations from CI matrix
moskvin May 12, 2026
85509fc
Update CI matrix with new Ruby and Grape version combinations
moskvin May 12, 2026
983db7f
:sparkles: Add tests for fulfilling path parameters with symbol and s…
moskvin May 12, 2026
d1f38ec
:recycle: Refactor Swagger documentation module to use GrapeSwagger n…
moskvin May 17, 2026
03b9ec4
:sparkles: Normalize path parameter keys to use symbols and update re…
moskvin May 17, 2026
ce63b05
:recycle: Simplify route matching logic in swagger_routing.rb
moskvin May 17, 2026
8e89a25
:sparkles: Update grape dependency to require version >= 2.4.0 and ad…
moskvin May 17, 2026
13ad0e1
:recycle: Remove unused block variable in combine_namespace_routes me…
moskvin May 17, 2026
edb770f
:sparkles: Add test for merging namespace options with symbol-keyed r…
moskvin May 17, 2026
b37da5a
:sparkles: Update README to clarify grape dependency version ranges
moskvin May 17, 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
33 changes: 16 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,22 @@ jobs:
strategy:
matrix:
entry:
- { ruby: '3.1', grape: '1.8.0' }
- { ruby: '3.2', grape: '1.8.0' }
- { ruby: '3.3', grape: '1.8.0' }
- { ruby: '3.4', grape: '1.8.0' }
Comment thread
moskvin marked this conversation as resolved.
- { ruby: '3.1', grape: '2.0.0' }
- { ruby: '3.2', grape: '2.0.0' }
- { ruby: '3.3', grape: '2.0.0' }
- { ruby: '3.4', grape: '2.0.0' }
- { ruby: '3.1', grape: '2.1.3' }
- { ruby: '3.2', grape: '2.1.3' }
- { ruby: '3.3', grape: '2.1.3' }
- { ruby: '3.4', grape: '2.1.3' }
- { ruby: '3.1', grape: '2.2.0' }
- { ruby: '3.2', grape: '2.2.0' }
- { 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.2', grape: 'HEAD' }
- { ruby: '3.3', grape: 'HEAD' }
- { ruby: '3.4', grape: 'HEAD' }
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#### Features

* [#976](https://github.com/ruby-grape/grape-swagger/pull/976): Ruby 3.4 and refactor swagger documentation modules - [@moskvin](https://github.com/moskvin).
* Your contribution here.

#### Fixes
Expand Down
6 changes: 3 additions & 3 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,9 +123,9 @@ 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.2 | 2.0 | >= 1.8.0 ... < 4.0 | >= 0.5.0 | >= 2.4.1 |
| > 2.1.2 ... < 2.2.0 | 2.0 | >= 1.8.0 ... < 4.0 | >= 0.5.0 | >= 2.4.1 |
| > 2.2.0 | 2.0 | >= 2.4.0 ... < 4.0 | >= 0.5.0 | >= 2.4.1 |


## Swagger-Spec <a name="swagger-spec"></a>
Expand Down
2 changes: 1 addition & 1 deletion grape-swagger.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Gem::Specification.new do |s|
s.metadata['rubygems_mfa_required'] = 'true'

s.required_ruby_version = '>= 3.1'
s.add_dependency 'grape', '>= 1.7', '< 4.0'
s.add_dependency 'grape', '>= 2.4.0', '< 4.0'

s.files = Dir['lib/**/*', '*.md', 'LICENSE.txt', 'grape-swagger.gemspec']
s.require_paths = ['lib']
Expand Down
166 changes: 3 additions & 163 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,4 @@ def request_param_parsers
}.freeze
end

module SwaggerRouting
private

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)
5 changes: 3 additions & 2 deletions lib/grape-swagger/request_param_parsers/route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def fetch_inherited_params(stackable_values)

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

Expand All @@ -57,7 +57,8 @@ def fulfill_params(path_params)
next if param.is_a?(String) && accum.key?(key)

defined_options = definition.is_a?(Hash) ? definition : {}
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
69 changes: 69 additions & 0 deletions lib/grape-swagger/swagger_documentation_adder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

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.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
end
Loading
Loading