Skip to content

Refactor swagger documentation modules into GrapeSwagger namespace with routing fixes#980

Merged
numbata merged 36 commits into
ruby-grape:masterfrom
numbata:refactor/grape-swagger-namespace-modules
Jun 26, 2026
Merged

Refactor swagger documentation modules into GrapeSwagger namespace with routing fixes#980
numbata merged 36 commits into
ruby-grape:masterfrom
numbata:refactor/grape-swagger-namespace-modules

Conversation

@numbata

@numbata numbata commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR builds on top of #976 by @moskvin, who deserves primary credit for the work here. The core refactor — extracting SwaggerRouting and SwaggerDocumentationAdder out of the top-level namespace into GrapeSwagger::, the sibling-namespace prefix boundary fix, the regex injection hardening, and the path-param deep-merge rewrite — was all done by @moskvin. This PR carries that work forward with additional spec coverage, edge-case fixes, and cleanup that emerged from review.

Specific additions on top of #976: remove the conditional guard that could leave SwaggerDocumentationAdder unaliased if SwaggerRouting was pre-defined elsewhere; fix empty-string prefix generating //-prefixed route patterns; add regression specs pinning the delete_prefix colon-stripping contract, the symbol-key lookup chain, two-level-deep hash merge, and intentional array-replace behavior in merge_path_param_options; remove redundant inline comments that narrated obvious code.

Changes

  • SwaggerRouting and SwaggerDocumentationAdder extracted to lib/grape-swagger/swagger_routing.rb and lib/grape-swagger/swagger_documentation_adder.rb under the GrapeSwagger:: namespace; top-level constants kept as deprecated aliases for backward compatibility
  • Regexp.escape applied to route.prefix and doc_klass.mount_path in combine_routes to close regex injection
  • Sibling-namespace prefix boundary fix: start_with?(ns_name) replaced with name == ns_name || name.start_with?("#{ns_name}/") so users is no longer a phantom child of user
  • standalone_namespaces hoisted out of the per-namespace loop (was O(n) recomputation)
  • Empty-string prefix treated as no prefix in route_path_start_with? to avoid //name patterns
  • gsub(/\/{2,}/, '/') replaces sub so all repeated slash runs in full_namespace are normalized, not just the first
  • Path-param merging in request_param_parsers/route.rb rewritten to deep-merge inherited namespace options and normalize keys to symbols
  • Compatibility aliases always assigned unconditionally (removed unless defined? guard)
  • Spec classes Spec renamed to Issue537Spec / Issue579Spec to avoid cross-file constant leaks

Related

Builds on #976 by @moskvin.

moskvin and others added 30 commits June 24, 2026 00:13
Master's commit 978 added a variant_types parameter to fulfill_params;
update the test to pass an empty hash for that argument.
The CI rows for grape 2.1.3 and 2.2.0 were dropped when the minimum was
raised to 2.4.0, but 2.1.x and 2.2.x users should not be broken by this
release. Revert the gemspec floor to >= 2.1 and restore the CI entries so
those combinations continue to be verified.

Update the README compatibility table to match.
SwaggerRouting and SwaggerDocumentationAdder have been namespaced under
GrapeSwagger::, but downstream code that references the old top-level
constants by name would get a NameError without any migration path.

Assign the old constant names as aliases pointing to the new namespaced
modules and document the change in UPGRADING.md so consumers know to
migrate to GrapeSwagger::SwaggerRouting / GrapeSwagger::SwaggerDocumentationAdder.
build_path_params walks the StackableValues chain from innermost to
outermost and was calling merge! at each step, which let the outer (less
specific) scope overwrite the inner one for params with the same name.

Reverse the merge so the accumulated inner result takes precedence over
each successive outer level, and add a test that asserts the innermost
:id definition wins when two namespace levels both declare options for it.
…rays

- Escape route.prefix before interpolating it into the split regex.
  A prefix like 'v1.0' made the dot a wildcard, silently mis-bucketing
  routes; a prefix with '(' raised RegexpError and crashed doc generation.

- Hoist standalone_namespaces computation above the each_key loop in
  combine_namespace_routes. The result never changes between iterations,
  so recomputing it O(N) times was wasted work.

- Replace Enumerator.new yielder blocks with plain array literals in
  route_namespace_equals? and route_path_start_with?. The two forms are
  semantically identical for a fixed two-element set; arrays are clearer.
- Regexp.escape doc_klass.mount_path in combine_routes, mirroring the
  same fix already applied to route.prefix on the line above. A mount
  path like '/api/v1.0/docs' would otherwise treat the dot as a wildcard,
  causing the hide_documentation_path guard to suppress unintended routes
  or raise RegexpError on paths containing '(' or '+'.

- Replace app.endpoints.clone with .dup in combine_namespaces. clone
  preserves the frozen state of the original array, so if Grape ever
  freezes its endpoint list the subsequent shift would raise FrozenError.
  dup always returns a mutable copy, which is the intended semantics here.
When the same path param is declared at multiple namespace levels, a
plain Hash#merge meant the inner declaration silently dropped every key
from the outer one, even keys the inner didn't set. Replace the flat
merge with a recursive deep merge so inner values win on conflict while
outer-only metadata (e.g. description or example set at the parent
level) is preserved.

Add regression cases: one for partial override (inner type wins, outer
description preserved) and one for nested documentation hash merging.
…ion tests

Replace the triple-negation reject block with a plain select using dig,
which directly expresses the intent: keep only namespaces that opt out
of nesting via swagger: { nested: false }.

Add spec/lib/swagger_routing_spec.rb covering the two Regexp.escape
fixes: route.prefix and mount_path. Uses '+' and '(' as metacharacters
since those cause a RegexpError rather than silent mismatches, making
the test fail clearly if escaping is ever removed.
…ated

Use Ruby's built-in deprecate_constant so callers get a warning when
they reference the old top-level names, instead of silently inheriting
aliases that would otherwise become permanent. Document planned removal
in 3.0 in UPGRADING.md and add a CHANGELOG entry.
- swagger_routing.rb: tighten the standalone_namespaces parent check from
  name.start_with?(ns_name) to name == ns_name || name.start_with?("#{ns_name}/")
  so sibling namespaces with a shared prefix (e.g. 'user' and 'users') are
  not mistaken for a parent-child pair. Add a regression spec.

- grape-swagger.rb: guard the top-level compat aliases with unless defined?
  to avoid 'already initialized constant' noise in reload-ish environments.

- route.rb: replace gsub(':', '') with delete_prefix(':') to express the
  intent precisely — only a leading colon should be stripped.

- route_spec.rb: rename misleading context title from "namespace options use
  string keys" to "route.params contains only symbol-keyed params", which
  accurately reflects the fixture.
sub(/\/{2,}/, '/') only collapses the first run of consecutive slashes.
A mount_path + namespace combination that produces multiple doubled-slash
runs (e.g. //foo//bar) would produce /foo//bar after sub, leaving the
second run intact and generating a malformed namespace key.

gsub collapses every run in one pass.
- swagger_routing_spec.rb: add a test proving that multiple double-slash
  runs in a joined mount path are all collapsed (covers the gsub fix).

- swagger_routing.rb: add a one-line comment on route_path_start_with?
  explaining that Regexp.escape is intentionally absent because
  String#start_with? is a literal prefix check, not a regex match.

- swagger_documentation_adder.rb: add require_relative 'swagger_routing'
  to make the load dependency explicit rather than relying on grape-swagger.rb
  always loading it first.
numbata added 4 commits June 25, 2026 02:02
…n specs

The unless defined?(SwaggerRouting) guard only checked one of the two
top-level aliases, leaving SwaggerDocumentationAdder potentially undefined
if a downstream gem had already defined SwaggerRouting. Removing the guard
ensures both aliases are always assigned together unconditionally. Normal
require semantics already prevent double-loading.

Adds three regression specs to route_spec.rb:
- Explicit ':account_id' -> :account_id symbol-key contract, pinning the
  delete_prefix + to_sym chain end-to-end
- Multi-leading-colon edge case ('::id') confirming delete_prefix strips
  exactly one colon
- Two-level-deep nested documentation hash merge, proving merge_path_param_options
  recurses past the first frame
@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@numbata numbata self-assigned this Jun 25, 2026
@numbata

numbata commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Hey @moskvin — this PR builds directly on top of your work in #976. Would love your eyes on it since you're the original author of these changes. Let us know if anything looks off.

@numbata numbata merged commit 76dd115 into ruby-grape:master Jun 26, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants