Skip to content

Deeply nested objects #752

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Aug 6, 2019
Merged
2 changes: 1 addition & 1 deletion lib/grape-swagger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|-]*)$')
Expand Down
1 change: 1 addition & 0 deletions lib/grape-swagger/doc_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
51 changes: 51 additions & 0 deletions lib/grape-swagger/doc_methods/format_data.rb
Original file line number Diff line number Diff line change
@@ -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
67 changes: 20 additions & 47 deletions lib/grape-swagger/doc_methods/move_params.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions lib/grape-swagger/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
12 changes: 1 addition & 11 deletions lib/grape-swagger/endpoint/params_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
190 changes: 190 additions & 0 deletions spec/issues/751_deeply_nested_objects_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading