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/ vendor/bundle/
spec/swagger_v2/x-dummy.rb spec/swagger_v2/x-dummy.rb
coverage/ coverage/
.byebug_history

View File

@ -6,8 +6,9 @@
#### Fixes #### 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-) * [#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. * Your contribution here.

View File

@ -451,6 +451,7 @@ add_swagger_documentation \
* [Collection Format](#collection-format) * [Collection Format](#collection-format)
* [Hiding parameters](#hiding-parameters) * [Hiding parameters](#hiding-parameters)
* [Setting a Swagger default value](#default-value) * [Setting a Swagger default value](#default-value)
* [Setting `additionalProperties` for `object`-type parameters](#additional-properties)
* [Example parameter value](#param-example) * [Example parameter value](#param-example)
* [Response documentation](#response) * [Response documentation](#response)
* [Changing default status codes](#change-status) * [Changing default status codes](#change-status)
@ -779,6 +780,36 @@ params do
end 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> #### 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. 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 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 ### Upgrading to >= 1.4.0
- Official support for ruby < 2.5 removed, ruby 2.5 only in testing mode, but no support. - 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) def document_as_property(param)
property_keys.each_with_object({}) do |x, memo| property_keys.each_with_object({}) do |x, memo|
value = param[x] next unless param.key?(x)
next if value.blank?
value = param[x]
if x == :type && @definitions[value].present? if x == :type && @definitions[value].present?
memo['$ref'] = "#/definitions/#{value}" memo['$ref'] = "#/definitions/#{value}"
else else
@ -181,7 +181,8 @@ module GrapeSwagger
end end
def property_keys 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 end
def deletable?(param) def deletable?(param)
@ -193,8 +194,7 @@ module GrapeSwagger
end end
def includes_body_param?(params) def includes_body_param?(params)
params.map { |x| return true if x[:in] == 'body' || x[:param_type] == 'body' } params.any? { |x| x[:in] == 'body' || x[:param_type] == 'body' }
false
end end
def should_expose_as_array?(params) def should_expose_as_array?(params)
@ -202,8 +202,7 @@ module GrapeSwagger
end end
def should_exposed_as(params) def should_exposed_as(params)
params.map { |x| return 'object' if x[:type] && x[:type] != 'array' } params.any? { |x| x[:type] && x[:type] != 'array' } ? 'object' : 'array'
'array'
end end
end end
end end

View File

@ -25,7 +25,7 @@ module GrapeSwagger
document_default_value(settings) unless value_type[:is_array] document_default_value(settings) unless value_type[:is_array]
document_range_values(settings) unless value_type[:is_array] document_range_values(settings) unless value_type[:is_array]
document_required(settings) document_required(settings)
document_additional_properties(settings) document_additional_properties(definitions, settings) unless value_type[:is_array]
document_add_extensions(settings) document_add_extensions(settings)
document_example(settings) document_example(settings)
@ -105,12 +105,38 @@ module GrapeSwagger
array_items[:default] = value_type[:default] if value_type[:default].present? 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 array_items
end end
def document_additional_properties(settings) def document_additional_properties(definitions, settings)
additional_properties = settings[:additionalProperties] set_additional_properties, additional_properties = parse_additional_properties(definitions, settings)
@parsed_param[:additionalProperties] = additional_properties if additional_properties @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 end
def document_example(settings) 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', detail: 'more details description',
success: Entities::UseNestedWithAddress success: Entities::UseNestedWithAddress
params do 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' } requires :name, type: String, documentation: { desc: 'name', in: 'body' }
optional :addresses, type: Array do optional :addresses, type: Array do
requires :street, type: String, documentation: { desc: 'street', in: 'body' } requires :street, type: String, documentation: { desc: 'street', in: 'body' }
@ -96,6 +96,27 @@ describe 'moving body/formData Params to definitions' do
end end
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 add_swagger_documentation
end end
end end
@ -126,6 +147,7 @@ describe 'moving body/formData Params to definitions' do
'properties' => { 'properties' => {
'contact' => { 'contact' => {
'type' => 'object', 'type' => 'object',
'additionalProperties' => true,
'properties' => { 'properties' => {
'name' => { 'type' => 'string', 'description' => 'name' }, 'name' => { 'type' => 'string', 'description' => 'name' },
'addresses' => { 'addresses' => {
@ -280,4 +302,54 @@ describe 'moving body/formData Params to definitions' do
end end
end 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 end