Fix documentation of `additionalProperties` field when used with array parameters (#840)

* Fix typo in CHANGELOG

* Allow usage of additional_properties, deprecate additionalProperties

All other options are snake-cased rather than camel-cased, so this keeps things consistent.

* Ignore .byebug_history

* Add test covering additional_properties

* Fix additionalProperties for arrays of objects

* Fix settings additional_properties to false

* Handle receiving types and entities in additional_properties

This includes a fix to MoveParams.document_as_property for an issue found while testing these changes.

* Refactor to use Enumerable#any?

* Document additional_properties

* Update CHANGELOG
This commit is contained in:
Paul Padier 2021-10-22 04:43:06 +09:00 committed by GitHub
parent 46e112c048
commit 1fea1f8d19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 230 additions and 13 deletions

1
.gitignore vendored
View File

@ -42,3 +42,4 @@ spec/params_entity_spec.rb
vendor/bundle/
spec/swagger_v2/x-dummy.rb
coverage/
.byebug_history

View File

@ -6,8 +6,9 @@
#### Fixes
* [#840](https://github.com/ruby-grape/grape-swagger/pull/840): Fixes documentation of `additionalProperties` field when used with array parameters, or when setting it to `false` - [@magni-](https://github.com/magni-)
* [#841](https://github.com/ruby-grape/grape-swagger/pull/839): Fixes `type` and `format` values for object fields nested in an array ([#832](https://github.com/ruby-grape/grape-swagger/issue/832)) - [@magni-](https://github.com/magni-)
* #[#839](https://github.com/ruby-grape/grape-swagger/pull/839): Fixes documentation of `false` or `nil` default parameter values - [@magni-](https://github.com/magni-)
* [#839](https://github.com/ruby-grape/grape-swagger/pull/839): Fixes documentation of `false` or `nil` default parameter values - [@magni-](https://github.com/magni-)
* Your contribution here.

View File

@ -451,6 +451,7 @@ add_swagger_documentation \
* [Collection Format](#collection-format)
* [Hiding parameters](#hiding-parameters)
* [Setting a Swagger default value](#default-value)
* [Setting `additionalProperties` for `object`-type parameters](#additional-properties)
* [Example parameter value](#param-example)
* [Response documentation](#response)
* [Changing default status codes](#change-status)
@ -779,6 +780,36 @@ params do
end
```
### Setting `additionalProperties` for `object`-type parameters <a name="additional-properties">
Use the `additional_properties` option in the `documentation` hash for `object`-type parameters to set [`additionalProperties`](https://swagger.io/specification/v2/#model-with-mapdictionary-properties).
#### Allow any additional properties
```ruby
params do
optional :thing, type: Hash, documentation: { additional_properties: true }
end
```
#### Allow any additional properties of a particular type
```ruby
params do
optional :thing, type: Hash, documentation: { additional_properties: String }
end
```
#### Allow any additional properties matching a defined schema
```ruby
class Entity < Grape::Entity
expose :this
end
params do
optional :thing, type: Hash, documentation: { additional_properties: Entity }
end
```
#### Example parameter value <a name="param-example"></a>
The example parameter will populate the Swagger UI with the example value, and can be used for optional or required parameters.

View File

@ -1,5 +1,9 @@
## Upgrading Grape-swagger
### Upgrading to >= 1.4.1
- `additionalProperties` has been deprecated and will be removed in a future version of `grape-swagger`. It has been replaced with `additional_properties`.
### Upgrading to >= 1.4.0
- Official support for ruby < 2.5 removed, ruby 2.5 only in testing mode, but no support.

View File

@ -103,9 +103,9 @@ module GrapeSwagger
def document_as_property(param)
property_keys.each_with_object({}) do |x, memo|
value = param[x]
next if value.blank?
next unless param.key?(x)
value = param[x]
if x == :type && @definitions[value].present?
memo['$ref'] = "#/definitions/#{value}"
else
@ -181,7 +181,8 @@ module GrapeSwagger
end
def property_keys
%i[type format description minimum maximum items enum default additionalProperties example]
%i[type format description minimum maximum items enum default additional_properties additionalProperties
example]
end
def deletable?(param)
@ -193,8 +194,7 @@ module GrapeSwagger
end
def includes_body_param?(params)
params.map { |x| return true if x[:in] == 'body' || x[:param_type] == 'body' }
false
params.any? { |x| x[:in] == 'body' || x[:param_type] == 'body' }
end
def should_expose_as_array?(params)
@ -202,8 +202,7 @@ module GrapeSwagger
end
def should_exposed_as(params)
params.map { |x| return 'object' if x[:type] && x[:type] != 'array' }
'array'
params.any? { |x| x[:type] && x[:type] != 'array' } ? 'object' : 'array'
end
end
end

View File

@ -25,7 +25,7 @@ module GrapeSwagger
document_default_value(settings) unless value_type[:is_array]
document_range_values(settings) unless value_type[:is_array]
document_required(settings)
document_additional_properties(settings)
document_additional_properties(definitions, settings) unless value_type[:is_array]
document_add_extensions(settings)
document_example(settings)
@ -105,12 +105,38 @@ module GrapeSwagger
array_items[:default] = value_type[:default] if value_type[:default].present?
set_additional_properties, additional_properties = parse_additional_properties(definitions, value_type)
array_items[:additionalProperties] = additional_properties if set_additional_properties
array_items
end
def document_additional_properties(settings)
additional_properties = settings[:additionalProperties]
@parsed_param[:additionalProperties] = additional_properties if additional_properties
def document_additional_properties(definitions, settings)
set_additional_properties, additional_properties = parse_additional_properties(definitions, settings)
@parsed_param[:additionalProperties] = additional_properties if set_additional_properties
end
def parse_additional_properties(definitions, settings)
return false unless settings.key?(:additionalProperties) || settings.key?(:additional_properties)
value =
if settings.key?(:additionalProperties)
GrapeSwagger::Errors::SwaggerSpecDeprecated.tell!(:additionalProperties)
settings[:additionalProperties]
else
settings[:additional_properties]
end
parsed_value =
if definitions[value.to_s]
{ '$ref': "#/definitions/#{value}" }
elsif value.is_a?(Class)
{ type: DataType.call(value) }
else
value
end
[true, parsed_value]
end
def document_example(settings)

View File

@ -0,0 +1,83 @@
# frozen_string_literal: true
require 'spec_helper'
describe 'parsing additional_parameters' do
let(:app) do
Class.new(Grape::API) do
namespace :things do
class Element < Grape::Entity
expose :id
end
params do
optional :closed, type: Hash, documentation: { additional_properties: false, in: 'body' } do
requires :only
end
optional :open, type: Hash, documentation: { additional_properties: true }
optional :type_limited, type: Hash, documentation: { additional_properties: String }
optional :ref_limited, type: Hash, documentation: { additional_properties: Element }
optional :fallback, type: Hash, documentation: { additional_properties: { type: 'integer' } }
end
post do
present params
end
end
add_swagger_documentation format: :json, models: [Element]
end
end
subject do
get '/swagger_doc/things'
JSON.parse(last_response.body)
end
describe 'POST' do
specify do
expect(subject.dig('paths', '/things', 'post', 'parameters')).to eql(
[
{ 'name' => 'Things', 'in' => 'body', 'required' => true, 'schema' => { '$ref' => '#/definitions/postThings' } }
]
)
end
specify do
expect(subject.dig('definitions', 'postThings')).to eql(
'type' => 'object',
'properties' => {
'closed' => {
'type' => 'object',
'additionalProperties' => false,
'properties' => {
'only' => { 'type' => 'string' }
},
'required' => ['only']
},
'open' => {
'type' => 'object',
'additionalProperties' => true
},
'type_limited' => {
'type' => 'object',
'additionalProperties' => {
'type' => 'string'
}
},
'ref_limited' => {
'type' => 'object',
'additionalProperties' => {
'$ref' => '#/definitions/Element'
}
},
'fallback' => {
'type' => 'object',
'additionalProperties' => {
'type' => 'integer'
}
}
}
)
end
end
end

View File

@ -13,7 +13,7 @@ describe 'moving body/formData Params to definitions' do
detail: 'more details description',
success: Entities::UseNestedWithAddress
params do
optional :contact, type: Hash do
optional :contact, type: Hash, documentation: { additional_properties: true } do
requires :name, type: String, documentation: { desc: 'name', in: 'body' }
optional :addresses, type: Array do
requires :street, type: String, documentation: { desc: 'street', in: 'body' }
@ -96,6 +96,27 @@ describe 'moving body/formData Params to definitions' do
end
end
namespace :nested_params_array do
desc 'post in body with array of nested parameters',
detail: 'more details description',
success: Entities::UseNestedWithAddress
params do
optional :contacts, type: Array, documentation: { additional_properties: false } do
requires :name, type: String, documentation: { desc: 'name', in: 'body' }
optional :addresses, type: Array do
requires :street, type: String, documentation: { desc: 'street', in: 'body' }
requires :postcode, type: String, documentation: { desc: 'postcode', in: 'body' }
requires :city, type: String, documentation: { desc: 'city', in: 'body' }
optional :country, type: String, documentation: { desc: 'country', in: 'body' }
end
end
end
post '/in_body' do
{ 'declared_params' => declared(params) }
end
end
add_swagger_documentation
end
end
@ -126,6 +147,7 @@ describe 'moving body/formData Params to definitions' do
'properties' => {
'contact' => {
'type' => 'object',
'additionalProperties' => true,
'properties' => {
'name' => { 'type' => 'string', 'description' => 'name' },
'addresses' => {
@ -280,4 +302,54 @@ describe 'moving body/formData Params to definitions' do
end
end
end
describe 'array of nested body parameters given' do
subject do
get '/swagger_doc/nested_params_array'
JSON.parse(last_response.body)
end
describe 'POST' do
specify do
expect(subject['paths']['/nested_params_array/in_body']['post']['parameters']).to eql(
[
{ 'name' => 'NestedParamsArrayInBody', 'in' => 'body', 'required' => true, 'schema' => { '$ref' => '#/definitions/postNestedParamsArrayInBody' } }
]
)
end
specify do
expect(subject['definitions']['postNestedParamsArrayInBody']).to eql(
'type' => 'object',
'properties' => {
'contacts' => {
'type' => 'array',
'items' => {
'type' => 'object',
'additionalProperties' => false,
'properties' => {
'name' => { 'type' => 'string', 'description' => 'name' },
'addresses' => {
'type' => 'array',
'items' => {
'type' => 'object',
'properties' => {
'street' => { 'type' => 'string', 'description' => 'street' },
'postcode' => { 'type' => 'string', 'description' => 'postcode' },
'city' => { 'type' => 'string', 'description' => 'city' },
'country' => { 'type' => 'string', 'description' => 'country' }
},
'required' => %w[street postcode city]
}
}
},
'required' => %w[name]
}
}
},
'description' => 'post in body with array of nested parameters'
)
end
end
end
end