diff --git a/lib/grape-swagger.rb b/lib/grape-swagger.rb index bb7d5e6a..c84c5062 100644 --- a/lib/grape-swagger.rb +++ b/lib/grape-swagger.rb @@ -26,7 +26,7 @@ module SwaggerRouting def combine_routes(app, doc_klass) app.routes.each do |route| route_path = route.path - route_match = route_path.split(/^.*?#{route.prefix.to_s}/).last + route_match = route_path.split(/^.*?#{route.prefix}/).last next unless route_match route_match = route_match.match('\/([\w|-]*?)[\.\/\(]') || route_match.match('\/([\w|-]*)$') diff --git a/lib/grape-swagger/doc_methods.rb b/lib/grape-swagger/doc_methods.rb index 326a1ea1..ac3c6138 100644 --- a/lib/grape-swagger/doc_methods.rb +++ b/lib/grape-swagger/doc_methods.rb @@ -4,6 +4,7 @@ require 'grape-swagger/doc_methods/produces_consumes' require 'grape-swagger/doc_methods/data_type' require 'grape-swagger/doc_methods/extensions' +require 'grape-swagger/doc_methods/format_data' require 'grape-swagger/doc_methods/operation_id' require 'grape-swagger/doc_methods/optional_object' require 'grape-swagger/doc_methods/path_string' diff --git a/lib/grape-swagger/doc_methods/format_data.rb b/lib/grape-swagger/doc_methods/format_data.rb new file mode 100644 index 00000000..3ad2980f --- /dev/null +++ b/lib/grape-swagger/doc_methods/format_data.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module GrapeSwagger + module DocMethods + class FormatData + class << self + def to_format(parameters) + parameters.reject { |parameter| parameter[:in] == 'body' }.each do |b| + related_parameters = parameters.select do |p| + p[:name] != b[:name] && p[:name].to_s.include?(b[:name].to_s.gsub(/\[\]\z/, '') + '[') + end + parameters.reject! { |p| p[:name] == b[:name] } if move_down(b, related_parameters) + end + parameters + end + + def move_down(parameter, related_parameters) + case parameter[:type] + when 'array' + add_array(parameter, related_parameters) + unless related_parameters.blank? + add_braces(parameter, related_parameters) if parameter[:name].match?(/\A.*\[\]\z/) + return true + end + when 'object' + return true + end + false + end + + def add_braces(parameter, related_parameters) + param_name = parameter[:name].gsub(/\A(.*)\[\]\z/, '\1') + related_parameters.each { |p| p[:name] = p[:name].gsub(param_name, param_name + '[]') } + end + + def add_array(parameter, related_parameters) + related_parameters.each do |p| + p_type = p[:type] == 'array' ? 'string' : p[:type] + p[:items] = { type: p_type, format: p[:format], enum: p[:enum], is_array: p[:is_array] } + p[:items].delete_if { |_k, v| v.nil? } + p[:type] = 'array' + p[:is_array] = parameter[:is_array] + p.delete(:format) + p.delete(:enum) + p.delete_if { |_k, v| v.nil? } + end + end + end + end + end +end diff --git a/lib/grape-swagger/doc_methods/move_params.rb b/lib/grape-swagger/doc_methods/move_params.rb index a887e192..da372a69 100644 --- a/lib/grape-swagger/doc_methods/move_params.rb +++ b/lib/grape-swagger/doc_methods/move_params.rb @@ -51,22 +51,33 @@ def parent_definition_of_params(params, path, route) def move_params_to_new(definition, params) params, nested_params = params.partition { |x| !x[:name].to_s.include?('[') } - - unless params.blank? - properties, required = build_properties(params) - add_properties_to_definition(definition, properties, required) + params.each do |param| + property = param[:name] + param_properties, param_required = build_properties([param]) + add_properties_to_definition(definition, param_properties, param_required) + related_nested_params, nested_params = nested_params.partition { |x| x[:name].start_with?("#{property}[") } + prepare_nested_names(property, related_nested_params) + + next if related_nested_params.blank? + + nested_definition = if should_expose_as_array?([param]) + move_params_to_new(array_type, related_nested_params) + else + move_params_to_new(object_type, related_nested_params) + end + if definition.key?(:items) + definition[:items][:properties][property.to_sym].deep_merge!(nested_definition) + else + definition[:properties][property.to_sym].deep_merge!(nested_definition) + end end - - nested_properties = build_nested_properties(nested_params) unless nested_params.blank? - add_properties_to_definition(definition, nested_properties, []) unless nested_params.blank? + definition end def build_properties(params) properties = {} required = [] - prepare_nested_types(params) if should_expose_as_array?(params) - params.each do |param| name = param[:name].to_sym @@ -103,28 +114,6 @@ def document_as_property(param) end end - def build_nested_properties(params, properties = {}) - property = params.bsearch { |x| x[:name].include?('[') }[:name].split('[').first - - nested_params, params = params.partition { |x| x[:name].start_with?("#{property}[") } - prepare_nested_names(property, nested_params) - - recursive_call(properties, property, nested_params) unless nested_params.empty? - build_nested_properties(params, properties) unless params.empty? - - properties - end - - def recursive_call(properties, property, nested_params) - if should_expose_as_array?(nested_params) - properties[property.to_sym] = array_type - move_params_to_new(properties[property.to_sym][:items], nested_params) - else - properties[property.to_sym] = object_type - move_params_to_new(properties[property.to_sym], nested_params) - end - end - def movable_params(params) to_delete = params.each_with_object([]) { |x, memo| memo << x if deletable?(x) } delete_from(params, to_delete) @@ -177,22 +166,6 @@ def object_type { type: 'object', properties: {} } end - def prepare_nested_types(params) - params.each do |param| - next unless param[:items] - - param[:type] = if param[:items][:type] == 'array' - 'string' - elsif param[:items].key?('$ref') - param[:type] = 'object' - else - param[:items][:type] - end - param[:format] = param[:items][:format] if param[:items][:format] - param.delete(:items) if param[:type] != 'object' - end - end - def prepare_nested_names(property, params) params.each { |x| x[:name] = x[:name].sub(property, '').sub('[', '').sub(']', '') } end diff --git a/lib/grape-swagger/endpoint.rb b/lib/grape-swagger/endpoint.rb index 22631a36..be392419 100644 --- a/lib/grape-swagger/endpoint.rb +++ b/lib/grape-swagger/endpoint.rb @@ -78,8 +78,7 @@ def contact_object(infos) def path_and_definition_objects(namespace_routes, options) @paths = {} @definitions = {} - namespace_routes.each_key do |key| - routes = namespace_routes[key] + namespace_routes.each_value do |routes| path_item(routes, options) end @@ -191,6 +190,8 @@ def params_object(route, options, path) parameters = GrapeSwagger::DocMethods::MoveParams.to_definition(path, parameters, route, @definitions) end + GrapeSwagger::DocMethods::FormatData.to_format(parameters) + parameters.presence end diff --git a/lib/grape-swagger/endpoint/params_parser.rb b/lib/grape-swagger/endpoint/params_parser.rb index 7cc7614e..925ee8ee 100644 --- a/lib/grape-swagger/endpoint/params_parser.rb +++ b/lib/grape-swagger/endpoint/params_parser.rb @@ -26,19 +26,9 @@ def parse_request_params options[:is_array] = true name += '[]' if array_use_braces?(options) - else - keys = array_keys.find_all { |key| name.start_with? "#{key}[" } - if keys.any? - options[:is_array] = true - if array_use_braces?(options) - keys.sort.reverse_each do |key| - name = name.sub(key, "#{key}[]") - end - end - end end - memo[name] = options unless %w[Hash Array].include?(param_type) && !options.key?(:documentation) + memo[name] = options end end diff --git a/spec/issues/751_deeply_nested_objects_spec.rb b/spec/issues/751_deeply_nested_objects_spec.rb new file mode 100644 index 00000000..c674438f --- /dev/null +++ b/spec/issues/751_deeply_nested_objects_spec.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe '751 deeply nested objects' do + let(:app) do + Class.new(Grape::API) do + content_type :json, 'application/json; charset=UTF-8' + default_format :json + class Vrp < Grape::API + def self.vrp_request_timewindow(this) + this.optional(:start, types: [String, Float, Integer]) + this.optional(:end, types: [String, Float, Integer]) + end + + def self.vrp_request_point(this) + this.requires(:id, type: String, allow_blank: false) + this.optional(:matrix_index, type: Integer) + this.optional(:location, type: Hash) do + requires(:lat, type: Float, allow_blank: false) + requires(:lon, type: Float, allow_blank: false) + end + this.at_least_one_of :matrix_index, :location + end + + def self.vrp_request_activity(this) + this.optional(:duration, types: [String, Float, Integer]) + this.requires(:point_id, type: String, allow_blank: false) + this.optional(:timewindows, type: Array) do + Vrp.vrp_request_timewindow(self) + end + end + + def self.vrp_request_service(this) + this.requires(:id, type: String, allow_blank: false) + this.optional(:skills, type: Array[String]) + + this.optional(:activity, type: Hash) do + Vrp.vrp_request_activity(self) + end + this.optional(:activities, type: Array) do + Vrp.vrp_request_activity(self) + end + this.mutually_exclusive :activity, :activities + end + end + + namespace :vrp do + resource :submit do + desc 'Submit Problems', nickname: 'vrp' + params do + optional(:vrp, type: Hash, documentation: { param_type: 'body' }) do + optional(:points, type: Array) do + Vrp.vrp_request_point(self) + end + + optional(:services, type: Array) do + Vrp.vrp_request_service(self) + end + end + end + post do + { vrp: params[:vrp] }.to_json + end + end + end + + add_swagger_documentation format: :json + end + end + + subject do + get '/swagger_doc' + JSON.parse(last_response.body) + end + + describe 'Correctness of vrp Points' do + let(:get_points_response) { subject['definitions']['postVrpSubmit']['properties']['vrp']['properties']['points'] } + specify do + expect(get_points_response).to eql( + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'properties' => { + 'id' => { + 'type' => 'string' + }, + 'matrix_index' => { + 'type' => 'integer', + 'format' => 'int32' + }, + 'location' => { + 'type' => 'object', + 'properties' => { + 'lat' => { + 'type' => 'number', + 'format' => 'float' + }, + 'lon' => { + 'type' => 'number', + 'format' => 'float' + } + }, + 'required' => %w[lat lon] + } + }, + 'required' => ['id'] + } + ) + end + end + + describe 'Correctness of vrp Services' do + let(:get_service_response) { subject['definitions']['postVrpSubmit']['properties']['vrp']['properties']['services'] } + specify do + expect(get_service_response).to include( + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'properties' => { + 'id' => { + 'type' => 'string' + }, + 'skills' => { + 'type' => 'array', + 'items' => { + 'type' => 'string' + } + }, + 'activity' => { + 'type' => 'object', + 'properties' => { + 'duration' => { + 'type' => 'string' + }, + 'point_id' => { + 'type' => 'string' + }, + 'timewindows' => { + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'properties' => { + 'start' => { + 'type' => 'string' + }, + 'end' => { + 'type' => 'string' + } + } + } + } + }, + 'required' => ['point_id'] + }, 'activities' => { + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'properties' => { + 'duration' => { + 'type' => 'string' + }, + 'point_id' => { + 'type' => 'string' + }, + 'timewindows' => { + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'properties' => { + 'start' => { + 'type' => 'string' + }, + 'end' => { + 'type' => 'string' + } + } + } + } + }, + 'required' => ['point_id'] + } + } + }, + 'required' => ['id'] + } + ) + end + end +end diff --git a/spec/lib/endpoint/params_parser_spec.rb b/spec/lib/endpoint/params_parser_spec.rb index 539c44ef..73a2eb4b 100644 --- a/spec/lib/endpoint/params_parser_spec.rb +++ b/spec/lib/endpoint/params_parser_spec.rb @@ -46,19 +46,11 @@ context 'when param is nested in a param of array type' do let(:params) { [['param_1', { type: 'Array' }], ['param_1[param_2]', { type: 'String' }]] } - it 'skips root parameter' do - is_expected.not_to have_key 'param_1' - end - - it 'adds is_array option to the nested param' do - expect(parse_request_params['param_1[param_2]']).to eq(type: 'String', is_array: true) - end - context 'and array_use_braces setting set to true' do let(:settings) { { array_use_braces: true } } it 'adds braces to the param key' do - expect(parse_request_params.keys.first).to eq 'param_1[][param_2]' + expect(parse_request_params.keys.last).to eq 'param_1[param_2]' end end end @@ -66,19 +58,11 @@ context 'when param is nested in a param of hash type' do let(:params) { [['param_1', { type: 'Hash' }], ['param_1[param_2]', { type: 'String' }]] } - it 'skips root parameter' do - is_expected.not_to have_key 'param_1' - end - - it 'does not change options to the nested param' do - expect(parse_request_params['param_1[param_2]']).to eq(type: 'String') - end - context 'and array_use_braces setting set to true' do let(:settings) { { array_use_braces: true } } it 'does not add braces to the param key' do - expect(parse_request_params.keys.first).to eq 'param_1[param_2]' + expect(parse_request_params.keys.last).to eq 'param_1[param_2]' end end end diff --git a/spec/lib/endpoint_spec.rb b/spec/lib/endpoint_spec.rb index 6347297c..689127f8 100644 --- a/spec/lib/endpoint_spec.rb +++ b/spec/lib/endpoint_spec.rb @@ -109,7 +109,7 @@ ['id', { required: true, type: 'String' }], ['description', { required: false, type: 'String' }], ['stuffs', { required: true, type: 'Array', is_array: true }], - ['stuffs[id]', { required: true, type: 'String', is_array: true }] + ['stuffs[id]', { required: true, type: 'String' }] ] end @@ -138,8 +138,8 @@ ['stuffs', { required: true, type: 'Array', is_array: true }], ['stuffs[owners]', { required: true, type: 'Array', is_array: true }], ['stuffs[creators]', { required: true, type: 'Array', is_array: true }], - ['stuffs[owners][id]', { required: true, type: 'String', is_array: true }], - ['stuffs[creators][id]', { required: true, type: 'String', is_array: true }], + ['stuffs[owners][id]', { required: true, type: 'String' }], + ['stuffs[creators][id]', { required: true, type: 'String' }], ['stuffs_and_things', { required: true, type: 'String' }] ] end diff --git a/spec/lib/format_data_spec.rb b/spec/lib/format_data_spec.rb new file mode 100644 index 00000000..d4779726 --- /dev/null +++ b/spec/lib/format_data_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GrapeSwagger::DocMethods::FormatData do + let(:subject) { GrapeSwagger::DocMethods::FormatData } + + [true, false].each do |array_use_braces| + context 'when param is nested in a param of array type' do + let(:braces) { array_use_braces ? '[]' : '' } + let(:params) do + [ + { in: 'formData', name: "param1#{braces}", type: 'array', items: { type: 'string' } }, + { in: 'formData', name: 'param1[param2]', type: 'string' } + ] + end + + it 'skips root parameter' do + expect(subject.to_format(params).first).not_to have_key "param1#{braces}" + end + + it 'Move array type to param2' do + expect(subject.to_format(params).first).to include(name: "param1#{braces}[param2]", type: 'array') + end + end + end + + context 'when param is nested in a param of hash type' do + let(:params) { [{ in: 'formData', type: 'object', name: 'param1' }, { in: 'formData', name: 'param1[param2]', type: 'string' }] } + + it 'skips root parameter' do + expect(subject.to_format(params).first).not_to have_key 'param1' + end + + it 'Move array type to param2' do + expect(subject.to_format(params).first).to include(name: 'param1[param2]', type: 'string') + end + end + + context 'when params contain a complex array' do + let(:params) do + [ + { name: 'id', required: true, type: 'string' }, + { name: 'description', required: false, type: 'string' }, + { name: 'stuffs', required: true, type: 'array' }, + { name: 'stuffs[id]', required: true, type: 'string' } + ] + end + + let(:expected_params) do + [ + { name: 'id', required: true, type: 'string' }, + { name: 'description', required: false, type: 'string' }, + { name: 'stuffs[id]', required: true, type: 'array', items: { type: 'string' } } + ] + end + + it 'parses params correctly and adds array type all concerned elements' do + expect(subject.to_format(params)).to eq expected_params + end + + context 'when array params are not contiguous with parent array' do + let(:params) do + [ + { name: 'id', required: true, type: 'string' }, + { name: 'description', required: false, type: 'string' }, + { name: 'stuffs', required: true, type: 'array' }, + { name: 'stuffs[owners]', required: true, type: 'array' }, + { name: 'stuffs[creators]', required: true, type: 'array' }, + { name: 'stuffs[owners][id]', required: true, type: 'string' }, + { name: 'stuffs[creators][id]', required: true, type: 'string' }, + { name: 'stuffs_and_things', required: true, type: 'string' } + ] + end + + let(:expected_params) do + [ + { name: 'id', required: true, type: 'string' }, + { name: 'description', required: false, type: 'string' }, + { name: 'stuffs[owners][id]', required: true, type: 'array', items: { type: 'string' } }, + { name: 'stuffs[creators][id]', required: true, type: 'array', items: { type: 'string' } }, + { name: 'stuffs_and_things', required: true, type: 'string' } + ] + end + + it 'parses params correctly and adds array type all concerned elements' do + expect(subject.to_format(params)).to eq expected_params + end + end + end +end diff --git a/spec/lib/move_params_spec.rb b/spec/lib/move_params_spec.rb index a8b9486e..3ad753f6 100644 --- a/spec/lib/move_params_spec.rb +++ b/spec/lib/move_params_spec.rb @@ -326,268 +326,6 @@ end end - describe 'prepare_nested_types' do - before :each do - subject.send(:prepare_nested_types, params) - end - - let(:params) do - [ - { - in: 'body', - name: 'address[street_lines]', - description: 'street lines', - type: 'array', - items: { - type: 'string' - }, - required: true - } - ] - end - - context 'when params contains nothing with :items key' do - let(:params) do - [ - { - in: 'body', - name: 'phone_number', - description: 'phone number', - type: 'string', - required: true - } - ] - end - - let(:expected_params) do - [ - { - in: 'body', - name: 'phone_number', - description: 'phone number', - type: 'string', - required: true - } - ] - end - - it 'does nothing' do - expect(params).to eq expected_params - end - end - - context 'when params contains :items key with array type' do - let(:params) do - [ - { - in: 'body', - name: 'address_street_lines', - description: 'street lines', - type: 'array', - items: { - type: 'array' - }, - required: true - } - ] - end - - let(:expected_params) do - [ - { - in: 'body', - name: 'address_street_lines', - description: 'street lines', - type: 'string', - required: true - } - ] - end - - it 'sets type to string and removes :items' do - expect(params).to eq expected_params - end - end - - context 'when params contains :items key with $ref' do - let(:params) do - [ - { - in: 'body', - name: 'address_street_lines', - description: 'street lines', - type: 'array', - items: { - '$ref' => '#/definitions/StreetLine' - }, - required: true - } - ] - end - - let(:expected_params) do - [ - { - in: 'body', - name: 'address_street_lines', - description: 'street lines', - type: 'object', - items: { - '$ref' => '#/definitions/StreetLine' - }, - required: true - } - ] - end - - it 'sets type to object and does not remove :items' do - expect(params).to eq expected_params - end - end - - context 'when params contains :items without $ref or array type' do - let(:params) do - [ - { - in: 'body', - name: 'address_street_lines', - description: 'street lines', - type: 'array', - items: { - type: 'string' - }, - required: true - } - ] - end - - let(:expected_params) do - [ - { - in: 'body', - name: 'address_street_lines', - description: 'street lines', - type: 'string', - required: true - } - ] - end - - it 'sets type to :items :type and removes :items' do - expect(params).to eq expected_params - end - end - - context 'when params contains :items key with :format' do - let(:params) do - [ - { - in: 'body', - name: 'street_number', - description: 'street number', - type: 'array', - items: { - type: 'integer', - format: 'int32' - }, - required: true - } - ] - end - - let(:expected_params) do - [ - { - in: 'body', - name: 'street_number', - description: 'street number', - type: 'integer', - format: 'int32', - required: true - } - ] - end - - it 'sets format and removes :items' do - expect(params).to eq expected_params - end - end - end - - describe 'recursive_call' do - before :each do - subject.send(:recursive_call, properties, 'test', nested_params) - end - - let(:properties) { {} } - - context 'when nested params is an array' do - let(:nested_params) do - [ - { - in: 'body', - name: 'aliases', - description: 'The aliases of test.', - type: 'array', - items: { type: 'string' }, - required: true - } - ] - end - - let(:expected_properties) do - { - type: 'array', - items: { - type: 'object', - properties: { - aliases: { - type: 'string', - description: 'The aliases of test.' - } - }, - required: [:aliases] - } - } - end - - it 'adds property as symbol with array type and items' do - expect(properties[:test]).to eq expected_properties - end - end - - context 'when nested params is not an array' do - let(:nested_params) do - [ - { - in: 'body', - name: 'id', - description: 'The unique ID of test.', - type: 'string', - required: true - } - ] - end - - let(:expected_properties) do - { - type: 'object', - required: [:id], - properties: { - id: { - type: 'string', - description: 'The unique ID of test.' - } - } - } - end - - it 'adds property as symbol with object type' do - expect(properties[:test]).to eq expected_properties - end - end - end - describe 'add_properties_to_definition' do before :each do subject.send(:add_properties_to_definition, definition, properties, []) diff --git a/spec/swagger_v2/params_array_spec.rb b/spec/swagger_v2/params_array_spec.rb index c3893ff5..42e3b4a6 100644 --- a/spec/swagger_v2/params_array_spec.rb +++ b/spec/swagger_v2/params_array_spec.rb @@ -123,10 +123,10 @@ 'type' => 'object', 'properties' => { 'array_of_string' => { - 'type' => 'string', 'description' => 'nested array of strings' + 'items' => { 'type' => 'string' }, 'type' => 'array', 'description' => 'nested array of strings' }, 'array_of_integer' => { - 'type' => 'integer', 'format' => 'int32', 'description' => 'nested array of integers' + 'items' => { 'type' => 'integer', 'format' => 'int32' }, 'type' => 'array', 'description' => 'nested array of integers' } }, 'required' => %w[array_of_string array_of_integer]