diff --git a/CHANGELOG.md b/CHANGELOG.md index c84d381b..f55bc39f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ #### Features +* [#879](https://github.com/ruby-grape/grape-swagger/pull/879): Add support for optional path segments - [@spaceraccoon](https://github.com/spaceraccoon) * Your contribution here. #### Fixes diff --git a/lib/grape-swagger/doc_methods/path_string.rb b/lib/grape-swagger/doc_methods/path_string.rb index 1fc02aa1..152de24e 100644 --- a/lib/grape-swagger/doc_methods/path_string.rb +++ b/lib/grape-swagger/doc_methods/path_string.rb @@ -4,8 +4,7 @@ module GrapeSwagger module DocMethods class PathString class << self - def build(route, options = {}) - path = route.path.dup + def build(route, path, options = {}) # always removing format path.sub!(/\(\.\w+?\)$/, '') path.sub!('(.:format)', '') @@ -29,6 +28,23 @@ def build(route, options = {}) [item, path.start_with?('/') ? path : "/#{path}"] end + + def generate_optional_segments(path) + # always removing format + path.sub!(/\(\.\w+?\)$/, '') + path.sub!('(.:format)', '') + + paths = [] + if path.match(/\(.+\)/) + # recurse with included optional segment + paths.concat(generate_optional_segments(path.sub(/\([^\)]+\)/, ''))) + # recurse with excluded optional segment + paths.concat(generate_optional_segments(path.sub(/\(/, '').sub(/\)/, ''))) + else + paths << path + end + paths + end end end end diff --git a/lib/grape-swagger/endpoint.rb b/lib/grape-swagger/endpoint.rb index 96817317..6220749b 100644 --- a/lib/grape-swagger/endpoint.rb +++ b/lib/grape-swagger/endpoint.rb @@ -97,18 +97,17 @@ def path_item(routes, options) routes.each do |route| next if hidden?(route, options) - @item, path = GrapeSwagger::DocMethods::PathString.build(route, options) - @entity = route.entity || route.options[:success] - - verb, method_object = method_object(route, options, path) - - if @paths.key?(path.to_s) - @paths[path.to_s][verb] = method_object - else - @paths[path.to_s] = { verb => method_object } + GrapeSwagger::DocMethods::PathString.generate_optional_segments(route.path.dup).each do |path| + @item, path = GrapeSwagger::DocMethods::PathString.build(route, path, options) + @entity = route.entity || route.options[:success] + verb, method_object = method_object(route, options, path) + if @paths.key?(path.to_s) + @paths[path.to_s][verb] = method_object + else + @paths[path.to_s] = { verb => method_object } + end + GrapeSwagger::DocMethods::Extensions.add(@paths[path.to_s], @definitions, route) end - - GrapeSwagger::DocMethods::Extensions.add(@paths[path.to_s], @definitions, route) end end diff --git a/spec/issues/878_optional_path_segments.rb b/spec/issues/878_optional_path_segments.rb new file mode 100644 index 00000000..2421f064 --- /dev/null +++ b/spec/issues/878_optional_path_segments.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe '#878 handle optional path segments' do + let(:app) do + Class.new(Grape::API) do + resource :books do + get 'page(/one)(/:two)/three' do + { message: 'hello world' } + end + end + + add_swagger_documentation + end + end + let(:parameters) { subject['paths']['/books/page/{two}/three']['get']['parameters'] } + + subject do + get '/swagger_doc' + JSON.parse(last_response.body) + end + + specify do + section_param = parameters.find { |param| param['name'] == 'two' } + expect(section_param['in']).to eq 'path' + expect subject['paths'].keys.to eq ['/books/page/three', '/books/page/{two}/three', '/books/page/one/three', '/books/page/one/{two}/three'] + end +end diff --git a/spec/lib/path_string_spec.rb b/spec/lib/path_string_spec.rb index 7380bce4..4706cb78 100644 --- a/spec/lib/path_string_spec.rb +++ b/spec/lib/path_string_spec.rb @@ -12,7 +12,7 @@ specify 'The original route path is not mutated' do route = Struct.new(:version, :path).new route.path = '/foo/:dynamic/bar' - subject.build(route, add_version: true) + subject.build(route, route.path.dup, add_version: true) expect(route.path).to eq '/foo/:dynamic/bar' end @@ -23,17 +23,17 @@ specify 'The returned path includes version' do route.path = '/{version}/thing(.json)' - expect(subject.build(route, options)).to eql ['Thing', '/v1/thing'] + expect(subject.build(route, route.path.dup, options)).to eql ['Thing', '/v1/thing'] route.path = '/{version}/thing/foo(.json)' - expect(subject.build(route, options)).to eql ['Foo', '/v1/thing/foo'] + expect(subject.build(route, route.path.dup, options)).to eql ['Foo', '/v1/thing/foo'] route.path = '/{version}/thing(.:format)' - expect(subject.build(route, options)).to eql ['Thing', '/v1/thing'] + expect(subject.build(route, route.path.dup, options)).to eql ['Thing', '/v1/thing'] route.path = '/{version}/thing/foo(.:format)' - expect(subject.build(route, options)).to eql ['Foo', '/v1/thing/foo'] + expect(subject.build(route, route.path.dup, options)).to eql ['Foo', '/v1/thing/foo'] route.path = '/{version}/thing/:id' - expect(subject.build(route, options)).to eql ['Thing', '/v1/thing/{id}'] + expect(subject.build(route, route.path.dup, options)).to eql ['Thing', '/v1/thing/{id}'] route.path = '/{version}/thing/foo/:id' - expect(subject.build(route, options)).to eql ['Foo', '/v1/thing/foo/{id}'] + expect(subject.build(route, route.path.dup, options)).to eql ['Foo', '/v1/thing/foo/{id}'] end end @@ -43,17 +43,17 @@ specify 'The returned path does not include version' do route.path = '/{version}/thing(.json)' - expect(subject.build(route, options)).to eql ['Thing', '/thing'] + expect(subject.build(route, route.path.dup, options)).to eql ['Thing', '/thing'] route.path = '/{version}/thing/foo(.json)' - expect(subject.build(route, options)).to eql ['Foo', '/thing/foo'] + expect(subject.build(route, route.path.dup, options)).to eql ['Foo', '/thing/foo'] route.path = '/{version}/thing(.:format)' - expect(subject.build(route, options)).to eql ['Thing', '/thing'] + expect(subject.build(route, route.path.dup, options)).to eql ['Thing', '/thing'] route.path = '/{version}/thing/foo(.:format)' - expect(subject.build(route, options)).to eql ['Foo', '/thing/foo'] + expect(subject.build(route, route.path.dup, options)).to eql ['Foo', '/thing/foo'] route.path = '/{version}/thing/:id' - expect(subject.build(route, options)).to eql ['Thing', '/thing/{id}'] + expect(subject.build(route, route.path.dup, options)).to eql ['Thing', '/thing/{id}'] route.path = '/{version}/thing/foo/:id' - expect(subject.build(route, options)).to eql ['Foo', '/thing/foo/{id}'] + expect(subject.build(route, route.path.dup, options)).to eql ['Foo', '/thing/foo/{id}'] end end @@ -63,17 +63,17 @@ specify 'The returned path does not include version' do route.path = '/{version}/thing(.json)' - expect(subject.build(route, options)).to eql ['Thing', '/thing'] + expect(subject.build(route, route.path.dup, options)).to eql ['Thing', '/thing'] route.path = '/{version}/thing/foo(.json)' - expect(subject.build(route, options)).to eql ['Foo', '/thing/foo'] + expect(subject.build(route, route.path.dup, options)).to eql ['Foo', '/thing/foo'] route.path = '/{version}/thing(.:format)' - expect(subject.build(route, options)).to eql ['Thing', '/thing'] + expect(subject.build(route, route.path.dup, options)).to eql ['Thing', '/thing'] route.path = '/{version}/thing/foo(.:format)' - expect(subject.build(route, options)).to eql ['Foo', '/thing/foo'] + expect(subject.build(route, route.path.dup, options)).to eql ['Foo', '/thing/foo'] route.path = '/{version}/thing/:id' - expect(subject.build(route, options)).to eql ['Thing', '/thing/{id}'] + expect(subject.build(route, route.path.dup, options)).to eql ['Thing', '/thing/{id}'] route.path = '/{version}/thing/foo/:id' - expect(subject.build(route, options)).to eql ['Foo', '/thing/foo/{id}'] + expect(subject.build(route, route.path.dup, options)).to eql ['Foo', '/thing/foo/{id}'] end end @@ -83,17 +83,17 @@ specify 'The returned path does not include version' do route.path = '/{version}/thing(.json)' - expect(subject.build(route, options)).to eql ['Thing', '/thing'] + expect(subject.build(route, route.path.dup, options)).to eql ['Thing', '/thing'] route.path = '/{version}/thing/foo(.json)' - expect(subject.build(route, options)).to eql ['Foo', '/thing/foo'] + expect(subject.build(route, route.path.dup, options)).to eql ['Foo', '/thing/foo'] route.path = '/{version}/thing(.:format)' - expect(subject.build(route, options)).to eql ['Thing', '/thing'] + expect(subject.build(route, route.path.dup, options)).to eql ['Thing', '/thing'] route.path = '/{version}/thing/foo(.:format)' - expect(subject.build(route, options)).to eql ['Foo', '/thing/foo'] + expect(subject.build(route, route.path.dup, options)).to eql ['Foo', '/thing/foo'] route.path = '/{version}/thing/:id' - expect(subject.build(route, options)).to eql ['Thing', '/thing/{id}'] + expect(subject.build(route, route.path.dup, options)).to eql ['Thing', '/thing/{id}'] route.path = '/{version}/thing/foo/:id' - expect(subject.build(route, options)).to eql ['Foo', '/thing/foo/{id}'] + expect(subject.build(route, route.path.dup, options)).to eql ['Foo', '/thing/foo/{id}'] end end end