Skip to content

Commit 445d2d4

Browse files
braktarLeFnord
authored andcommitted
Deeply nested objects (#752)
* Depply nested objects * Simple hash syntax edit * Nested objects are not necessary arrays * fix points test * Fix test services * Doesn't require to delete hash or array properties * Edit nested logic * Add vehicle test * Change parameter level is endpoint job * Array body shouldn't lost array type * no more parser role * array of array to array of string * move format data * fix formdata return * Moved params_parser role spec * Move endpoint spec * Revert "Add vehicle test" This reverts commit e5ba63b. * remove redundant interpolation
1 parent 7237ef3 commit 445d2d4

12 files changed

+365
-346
lines changed

lib/grape-swagger.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ module SwaggerRouting
2626
def combine_routes(app, doc_klass)
2727
app.routes.each do |route|
2828
route_path = route.path
29-
route_match = route_path.split(/^.*?#{route.prefix.to_s}/).last
29+
route_match = route_path.split(/^.*?#{route.prefix}/).last
3030
next unless route_match
3131

3232
route_match = route_match.match('\/([\w|-]*?)[\.\/\(]') || route_match.match('\/([\w|-]*)$')

lib/grape-swagger/doc_methods.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require 'grape-swagger/doc_methods/produces_consumes'
55
require 'grape-swagger/doc_methods/data_type'
66
require 'grape-swagger/doc_methods/extensions'
7+
require 'grape-swagger/doc_methods/format_data'
78
require 'grape-swagger/doc_methods/operation_id'
89
require 'grape-swagger/doc_methods/optional_object'
910
require 'grape-swagger/doc_methods/path_string'
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
module GrapeSwagger
4+
module DocMethods
5+
class FormatData
6+
class << self
7+
def to_format(parameters)
8+
parameters.reject { |parameter| parameter[:in] == 'body' }.each do |b|
9+
related_parameters = parameters.select do |p|
10+
p[:name] != b[:name] && p[:name].to_s.include?(b[:name].to_s.gsub(/\[\]\z/, '') + '[')
11+
end
12+
parameters.reject! { |p| p[:name] == b[:name] } if move_down(b, related_parameters)
13+
end
14+
parameters
15+
end
16+
17+
def move_down(parameter, related_parameters)
18+
case parameter[:type]
19+
when 'array'
20+
add_array(parameter, related_parameters)
21+
unless related_parameters.blank?
22+
add_braces(parameter, related_parameters) if parameter[:name].match?(/\A.*\[\]\z/)
23+
return true
24+
end
25+
when 'object'
26+
return true
27+
end
28+
false
29+
end
30+
31+
def add_braces(parameter, related_parameters)
32+
param_name = parameter[:name].gsub(/\A(.*)\[\]\z/, '\1')
33+
related_parameters.each { |p| p[:name] = p[:name].gsub(param_name, param_name + '[]') }
34+
end
35+
36+
def add_array(parameter, related_parameters)
37+
related_parameters.each do |p|
38+
p_type = p[:type] == 'array' ? 'string' : p[:type]
39+
p[:items] = { type: p_type, format: p[:format], enum: p[:enum], is_array: p[:is_array] }
40+
p[:items].delete_if { |_k, v| v.nil? }
41+
p[:type] = 'array'
42+
p[:is_array] = parameter[:is_array]
43+
p.delete(:format)
44+
p.delete(:enum)
45+
p.delete_if { |_k, v| v.nil? }
46+
end
47+
end
48+
end
49+
end
50+
end
51+
end

lib/grape-swagger/doc_methods/move_params.rb

Lines changed: 20 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -51,22 +51,33 @@ def parent_definition_of_params(params, path, route)
5151

5252
def move_params_to_new(definition, params)
5353
params, nested_params = params.partition { |x| !x[:name].to_s.include?('[') }
54-
55-
unless params.blank?
56-
properties, required = build_properties(params)
57-
add_properties_to_definition(definition, properties, required)
54+
params.each do |param|
55+
property = param[:name]
56+
param_properties, param_required = build_properties([param])
57+
add_properties_to_definition(definition, param_properties, param_required)
58+
related_nested_params, nested_params = nested_params.partition { |x| x[:name].start_with?("#{property}[") }
59+
prepare_nested_names(property, related_nested_params)
60+
61+
next if related_nested_params.blank?
62+
63+
nested_definition = if should_expose_as_array?([param])
64+
move_params_to_new(array_type, related_nested_params)
65+
else
66+
move_params_to_new(object_type, related_nested_params)
67+
end
68+
if definition.key?(:items)
69+
definition[:items][:properties][property.to_sym].deep_merge!(nested_definition)
70+
else
71+
definition[:properties][property.to_sym].deep_merge!(nested_definition)
72+
end
5873
end
59-
60-
nested_properties = build_nested_properties(nested_params) unless nested_params.blank?
61-
add_properties_to_definition(definition, nested_properties, []) unless nested_params.blank?
74+
definition
6275
end
6376

6477
def build_properties(params)
6578
properties = {}
6679
required = []
6780

68-
prepare_nested_types(params) if should_expose_as_array?(params)
69-
7081
params.each do |param|
7182
name = param[:name].to_sym
7283

@@ -103,28 +114,6 @@ def document_as_property(param)
103114
end
104115
end
105116

106-
def build_nested_properties(params, properties = {})
107-
property = params.bsearch { |x| x[:name].include?('[') }[:name].split('[').first
108-
109-
nested_params, params = params.partition { |x| x[:name].start_with?("#{property}[") }
110-
prepare_nested_names(property, nested_params)
111-
112-
recursive_call(properties, property, nested_params) unless nested_params.empty?
113-
build_nested_properties(params, properties) unless params.empty?
114-
115-
properties
116-
end
117-
118-
def recursive_call(properties, property, nested_params)
119-
if should_expose_as_array?(nested_params)
120-
properties[property.to_sym] = array_type
121-
move_params_to_new(properties[property.to_sym][:items], nested_params)
122-
else
123-
properties[property.to_sym] = object_type
124-
move_params_to_new(properties[property.to_sym], nested_params)
125-
end
126-
end
127-
128117
def movable_params(params)
129118
to_delete = params.each_with_object([]) { |x, memo| memo << x if deletable?(x) }
130119
delete_from(params, to_delete)
@@ -177,22 +166,6 @@ def object_type
177166
{ type: 'object', properties: {} }
178167
end
179168

180-
def prepare_nested_types(params)
181-
params.each do |param|
182-
next unless param[:items]
183-
184-
param[:type] = if param[:items][:type] == 'array'
185-
'string'
186-
elsif param[:items].key?('$ref')
187-
param[:type] = 'object'
188-
else
189-
param[:items][:type]
190-
end
191-
param[:format] = param[:items][:format] if param[:items][:format]
192-
param.delete(:items) if param[:type] != 'object'
193-
end
194-
end
195-
196169
def prepare_nested_names(property, params)
197170
params.each { |x| x[:name] = x[:name].sub(property, '').sub('[', '').sub(']', '') }
198171
end

lib/grape-swagger/endpoint.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,7 @@ def contact_object(infos)
7878
def path_and_definition_objects(namespace_routes, options)
7979
@paths = {}
8080
@definitions = {}
81-
namespace_routes.each_key do |key|
82-
routes = namespace_routes[key]
81+
namespace_routes.each_value do |routes|
8382
path_item(routes, options)
8483
end
8584

@@ -191,6 +190,8 @@ def params_object(route, options, path)
191190
parameters = GrapeSwagger::DocMethods::MoveParams.to_definition(path, parameters, route, @definitions)
192191
end
193192

193+
GrapeSwagger::DocMethods::FormatData.to_format(parameters)
194+
194195
parameters.presence
195196
end
196197

lib/grape-swagger/endpoint/params_parser.rb

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,9 @@ def parse_request_params
2626
options[:is_array] = true
2727

2828
name += '[]' if array_use_braces?(options)
29-
else
30-
keys = array_keys.find_all { |key| name.start_with? "#{key}[" }
31-
if keys.any?
32-
options[:is_array] = true
33-
if array_use_braces?(options)
34-
keys.sort.reverse_each do |key|
35-
name = name.sub(key, "#{key}[]")
36-
end
37-
end
38-
end
3929
end
4030

41-
memo[name] = options unless %w[Hash Array].include?(param_type) && !options.key?(:documentation)
31+
memo[name] = options
4232
end
4333
end
4434

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe '751 deeply nested objects' do
6+
let(:app) do
7+
Class.new(Grape::API) do
8+
content_type :json, 'application/json; charset=UTF-8'
9+
default_format :json
10+
class Vrp < Grape::API
11+
def self.vrp_request_timewindow(this)
12+
this.optional(:start, types: [String, Float, Integer])
13+
this.optional(:end, types: [String, Float, Integer])
14+
end
15+
16+
def self.vrp_request_point(this)
17+
this.requires(:id, type: String, allow_blank: false)
18+
this.optional(:matrix_index, type: Integer)
19+
this.optional(:location, type: Hash) do
20+
requires(:lat, type: Float, allow_blank: false)
21+
requires(:lon, type: Float, allow_blank: false)
22+
end
23+
this.at_least_one_of :matrix_index, :location
24+
end
25+
26+
def self.vrp_request_activity(this)
27+
this.optional(:duration, types: [String, Float, Integer])
28+
this.requires(:point_id, type: String, allow_blank: false)
29+
this.optional(:timewindows, type: Array) do
30+
Vrp.vrp_request_timewindow(self)
31+
end
32+
end
33+
34+
def self.vrp_request_service(this)
35+
this.requires(:id, type: String, allow_blank: false)
36+
this.optional(:skills, type: Array[String])
37+
38+
this.optional(:activity, type: Hash) do
39+
Vrp.vrp_request_activity(self)
40+
end
41+
this.optional(:activities, type: Array) do
42+
Vrp.vrp_request_activity(self)
43+
end
44+
this.mutually_exclusive :activity, :activities
45+
end
46+
end
47+
48+
namespace :vrp do
49+
resource :submit do
50+
desc 'Submit Problems', nickname: 'vrp'
51+
params do
52+
optional(:vrp, type: Hash, documentation: { param_type: 'body' }) do
53+
optional(:points, type: Array) do
54+
Vrp.vrp_request_point(self)
55+
end
56+
57+
optional(:services, type: Array) do
58+
Vrp.vrp_request_service(self)
59+
end
60+
end
61+
end
62+
post do
63+
{ vrp: params[:vrp] }.to_json
64+
end
65+
end
66+
end
67+
68+
add_swagger_documentation format: :json
69+
end
70+
end
71+
72+
subject do
73+
get '/swagger_doc'
74+
JSON.parse(last_response.body)
75+
end
76+
77+
describe 'Correctness of vrp Points' do
78+
let(:get_points_response) { subject['definitions']['postVrpSubmit']['properties']['vrp']['properties']['points'] }
79+
specify do
80+
expect(get_points_response).to eql(
81+
'type' => 'array',
82+
'items' => {
83+
'type' => 'object',
84+
'properties' => {
85+
'id' => {
86+
'type' => 'string'
87+
},
88+
'matrix_index' => {
89+
'type' => 'integer',
90+
'format' => 'int32'
91+
},
92+
'location' => {
93+
'type' => 'object',
94+
'properties' => {
95+
'lat' => {
96+
'type' => 'number',
97+
'format' => 'float'
98+
},
99+
'lon' => {
100+
'type' => 'number',
101+
'format' => 'float'
102+
}
103+
},
104+
'required' => %w[lat lon]
105+
}
106+
},
107+
'required' => ['id']
108+
}
109+
)
110+
end
111+
end
112+
113+
describe 'Correctness of vrp Services' do
114+
let(:get_service_response) { subject['definitions']['postVrpSubmit']['properties']['vrp']['properties']['services'] }
115+
specify do
116+
expect(get_service_response).to include(
117+
'type' => 'array',
118+
'items' => {
119+
'type' => 'object',
120+
'properties' => {
121+
'id' => {
122+
'type' => 'string'
123+
},
124+
'skills' => {
125+
'type' => 'array',
126+
'items' => {
127+
'type' => 'string'
128+
}
129+
},
130+
'activity' => {
131+
'type' => 'object',
132+
'properties' => {
133+
'duration' => {
134+
'type' => 'string'
135+
},
136+
'point_id' => {
137+
'type' => 'string'
138+
},
139+
'timewindows' => {
140+
'type' => 'array',
141+
'items' => {
142+
'type' => 'object',
143+
'properties' => {
144+
'start' => {
145+
'type' => 'string'
146+
},
147+
'end' => {
148+
'type' => 'string'
149+
}
150+
}
151+
}
152+
}
153+
},
154+
'required' => ['point_id']
155+
}, 'activities' => {
156+
'type' => 'array',
157+
'items' => {
158+
'type' => 'object',
159+
'properties' => {
160+
'duration' => {
161+
'type' => 'string'
162+
},
163+
'point_id' => {
164+
'type' => 'string'
165+
},
166+
'timewindows' => {
167+
'type' => 'array',
168+
'items' => {
169+
'type' => 'object',
170+
'properties' => {
171+
'start' => {
172+
'type' => 'string'
173+
},
174+
'end' => {
175+
'type' => 'string'
176+
}
177+
}
178+
}
179+
}
180+
},
181+
'required' => ['point_id']
182+
}
183+
}
184+
},
185+
'required' => ['id']
186+
}
187+
)
188+
end
189+
end
190+
end

0 commit comments

Comments
 (0)