Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 on lines -29 to -32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gemspec declares grape >= 1.7, but this PR replaces all matrix entries for Grape 1.8.0–2.2.0 with 2.4.0+. As the minimum requirement is being raised to 2.4.0, could you update the gemspec and README?

- { 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
164 changes: 2 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,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)
3 changes: 2 additions & 1 deletion lib/grape-swagger/request_param_parsers/route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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[param] || path_params[param.to_s] || path_params[param.to_sym] || {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

  alt = param.is_a?(Symbol) ? param.to_s : param.to_sym
  path_options = path_params[param] || path_params[alt] || {}

or normalize keys once in build_path_params so callers don't need to think about it.

value = path_options.merge(defined_options)
accum[key] = value.empty? ? DEFAULT_PARAM_TYPE : value
end
end
Expand Down
67 changes: 67 additions & 0 deletions lib/grape-swagger/swagger_documentation_adder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

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