Skip to content

Commit af9b11d

Browse files
blakenumbata
authored andcommitted
Initial support for openapi3 requestBody
1 parent 6bac652 commit af9b11d

File tree

3 files changed

+240
-1
lines changed

3 files changed

+240
-1
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# frozen_string_literal: true
2+
3+
module GrapeSwagger
4+
module DocMethods
5+
class ParseRequestBody
6+
class << self
7+
def call(param, settings, path, route, _definitions)
8+
method = route.request_method
9+
additional_documentation = settings.fetch(:documentation, {})
10+
settings.merge!(additional_documentation)
11+
data_type = DataType.call(settings)
12+
13+
value_type = settings.merge(data_type: data_type, path: path, param_name: param, method: method)
14+
15+
type = param_type(value_type)
16+
return nil if type.nil?
17+
18+
# required properties
19+
@parsed_param = {
20+
in: type,
21+
name: settings[:full_name] || param
22+
}
23+
24+
# optional properties
25+
document_description(settings)
26+
document_type_and_format(settings, data_type)
27+
document_array_param(value_type, definitions) if value_type[:is_array]
28+
document_default_value(settings) unless value_type[:is_array]
29+
document_range_values(settings) unless value_type[:is_array]
30+
document_required(settings)
31+
32+
@parsed_param
33+
end
34+
35+
private
36+
37+
def document_description(settings)
38+
description = settings[:desc] || settings[:description]
39+
@parsed_param[:description] = description if description
40+
end
41+
42+
def document_required(settings)
43+
@parsed_param[:required] = settings[:required] || false
44+
@parsed_param[:required] = true if @parsed_param[:in] == 'path'
45+
end
46+
47+
def document_range_values(settings)
48+
values = settings[:values] || nil
49+
enum_or_range_values = parse_enum_or_range_values(values)
50+
@parsed_param.merge!(enum_or_range_values) if enum_or_range_values
51+
end
52+
53+
def document_default_value(settings)
54+
@parsed_param[:default] = settings[:default] if settings[:default].present?
55+
end
56+
57+
def document_type_and_format(settings, data_type)
58+
if DataType.primitive?(data_type)
59+
data = DataType.mapping(data_type)
60+
@parsed_param[:type], @parsed_param[:format] = data
61+
else
62+
@parsed_param[:type] = data_type
63+
end
64+
@parsed_param[:format] = settings[:format] if settings[:format].present?
65+
end
66+
67+
def document_array_param(value_type, definitions)
68+
if value_type[:documentation].present?
69+
param_type = value_type[:documentation][:param_type]
70+
doc_type = value_type[:documentation][:type]
71+
type = DataType.mapping(doc_type) if doc_type && !DataType.request_primitive?(doc_type)
72+
collection_format = value_type[:documentation][:collectionFormat]
73+
end
74+
75+
param_type ||= value_type[:param_type]
76+
77+
array_items = {}
78+
if definitions[value_type[:data_type]]
79+
array_items['$ref'] = "#/definitions/#{@parsed_param[:type]}"
80+
else
81+
array_items[:type] = type || @parsed_param[:type] == 'array' ? 'string' : @parsed_param[:type]
82+
end
83+
array_items[:format] = @parsed_param.delete(:format) if @parsed_param[:format]
84+
85+
values = value_type[:values] || nil
86+
enum_or_range_values = parse_enum_or_range_values(values)
87+
array_items.merge!(enum_or_range_values) if enum_or_range_values
88+
89+
array_items[:default] = value_type[:default] if value_type[:default].present?
90+
91+
@parsed_param[:in] = param_type || 'formData'
92+
@parsed_param[:items] = array_items
93+
@parsed_param[:type] = 'array'
94+
@parsed_param[:collectionFormat] = collection_format if DataType.collections.include?(collection_format)
95+
end
96+
97+
def param_type(value_type)
98+
if value_type[:path].include?("{#{value_type[:param_name]}}")
99+
nil
100+
elsif %w[POST PUT PATCH].include?(value_type[:method])
101+
DataType.request_primitive?(value_type[:data_type]) ? 'formData' : 'body'
102+
end
103+
end
104+
105+
def parse_enum_or_range_values(values)
106+
case values
107+
when Proc
108+
parse_enum_or_range_values(values.call) if values.parameters.empty?
109+
when Range
110+
parse_range_values(values) if values.first.is_a?(Integer)
111+
else
112+
{ enum: values } if values
113+
end
114+
end
115+
116+
def parse_range_values(values)
117+
{ minimum: values.first, maximum: values.last }
118+
end
119+
end
120+
end
121+
end
122+
end

lib/grape-swagger/openapi_3/endpoint.rb

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'active_support'
44
require 'active_support/core_ext/string/inflections'
55
require 'grape-swagger/endpoint/params_parser'
6+
require 'grape-swagger/openapi_3/doc_methods/parse_request_body'
67

78
module Grape
89
class Endpoint
@@ -116,10 +117,13 @@ def method_object(route, options, path)
116117
method = {}
117118
method[:summary] = summary_object(route)
118119
method[:description] = description_object(route)
119-
# method[:consumes] = consumes_object(route, options[:format])
120120
method[:parameters] = params_object(route, options, path)
121121
method[:security] = security_object(route)
122+
if %w[POST PUT PATCH].include?(route.request_method)
123+
method[:requestBody] = response_body_object(route, options, path)
124+
end
122125

126+
# method[:consumes] = consumes_object(route, options[:format])
123127
produces = produces_object(route, options[:produces] || options[:format])
124128

125129
method[:responses] = response_object(route, produces)
@@ -196,6 +200,36 @@ def params_object(route, options, path)
196200
parameters
197201
end
198202

203+
def response_body_object(route, options, path)
204+
parameters = partition_params(route, options).map do |param, value|
205+
value = { required: false }.merge(value) if value.is_a?(Hash)
206+
_, value = default_type([[param, value]]).first if value == ''
207+
if value[:type]
208+
expose_params(value[:type])
209+
elsif value[:documentation]
210+
expose_params(value[:documentation][:type])
211+
end
212+
213+
GrapeSwagger::DocMethods::ParseRequestBody.call(param, value, path, route, @definitions)
214+
end.flatten
215+
216+
parameters = {
217+
'content' => parameters.group_by { |p| p[:in] }.map do |_k, v|
218+
required_values = v.select { |param| param[:required] }
219+
[
220+
'application/x-www-form-urlencoded',
221+
{ 'schema' => {
222+
'type' => 'object',
223+
'required' => required_values.map { |required| required[:name] },
224+
'properties' => v.map { |value| [value[:name], value.except(:name, :in, :required)] }.to_h
225+
} }
226+
]
227+
end.to_h
228+
}
229+
230+
parameters
231+
end
232+
199233
def response_object(route, content_types)
200234
codes = http_codes_from_route(route)
201235
codes.map! { |x| x.is_a?(Array) ? { code: x[0], message: x[1], model: x[2], examples: x[3], headers: x[4] } : x }

spec/openapi_3/params_hash_spec.rb

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe 'Group Params as Hash' do
6+
def app
7+
Class.new(Grape::API) do
8+
format :json
9+
10+
params do
11+
requires :required_group, type: Hash do
12+
requires :required_param_1
13+
requires :required_param_2
14+
end
15+
end
16+
post '/use_groups' do
17+
{ 'declared_params' => declared(params) }
18+
end
19+
20+
params do
21+
requires :typed_group, type: Hash do
22+
requires :id, type: Integer, desc: 'integer given'
23+
requires :name, type: String, desc: 'string given'
24+
optional :email, type: String, desc: 'email given'
25+
optional :others, type: Integer, values: [1, 2, 3]
26+
end
27+
end
28+
post '/use_given_type' do
29+
{ 'declared_params' => declared(params) }
30+
end
31+
32+
add_swagger_documentation openapi_version: '3.0'
33+
end
34+
end
35+
36+
describe 'grouped parameters' do
37+
subject do
38+
get '/swagger_doc/use_groups'
39+
JSON.parse(last_response.body)
40+
end
41+
42+
specify do
43+
expect(subject['paths']['/use_groups']['post']).to include('requestBody')
44+
expect(subject['paths']['/use_groups']['post']['requestBody']['content']).to eql(
45+
'application/x-www-form-urlencoded' => {
46+
'schema' => {
47+
'properties' => {
48+
'required_group[required_param_1]' => { 'type' => 'string' },
49+
'required_group[required_param_2]' => { 'type' => 'string' }
50+
},
51+
'required' => %w(required_group[required_param_1] required_group[required_param_2]),
52+
'type' => 'object'
53+
}
54+
}
55+
)
56+
end
57+
end
58+
59+
describe 'grouped parameters with given type' do
60+
subject do
61+
get '/swagger_doc/use_given_type'
62+
JSON.parse(last_response.body)
63+
end
64+
65+
specify do
66+
expect(subject['paths']['/use_given_type']['post']).to include('requestBody')
67+
expect(subject['paths']['/use_given_type']['post']['requestBody']['content']).to eql(
68+
'application/x-www-form-urlencoded' => {
69+
'schema' => {
70+
'properties' => {
71+
'typed_group[email]' => { 'description' => 'email given', 'type' => 'string' },
72+
'typed_group[id]' => { 'description' => 'integer given', 'format' => 'int32', 'type' => 'integer' },
73+
'typed_group[name]' => { 'description' => 'string given', 'type' => 'string' },
74+
'typed_group[others]' => { 'enum' => [1, 2, 3], 'format' => 'int32', 'type' => 'integer' }
75+
},
76+
'required' => ['typed_group[id]', 'typed_group[name]'],
77+
'type' => 'object'
78+
}
79+
}
80+
)
81+
end
82+
end
83+
end

0 commit comments

Comments
 (0)