Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.

# Offense count: 7
# Offense count: 8
# Configuration parameters: AllowedMethods.
# AllowedMethods: enums
Lint/ConstantDefinitionInBlock:
Exclude:
- 'spec/grape-swagger/entities/response_model_spec.rb'
- 'spec/issues/44_desc_in_entity_type_spec.rb'
- 'spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb'
- 'spec/support/shared_contexts/inheritance_api.rb'
- 'spec/support/shared_contexts/this_api.rb'
Expand Down Expand Up @@ -43,7 +44,7 @@ Metrics/AbcSize:
# Offense count: 2
# Configuration parameters: CountComments, CountAsOne.
Metrics/ClassLength:
Max: 135
Max: 145

# Offense count: 2
# Configuration parameters: AllowedMethods, AllowedPatterns.
Expand Down Expand Up @@ -100,7 +101,7 @@ RSpec/DescribeClass:
# Offense count: 4
# Configuration parameters: CountAsOne.
RSpec/ExampleLength:
Max: 213
Max: 225

# Offense count: 30
RSpec/LeakyConstantDeclaration:
Expand Down
19 changes: 17 additions & 2 deletions lib/grape-swagger/entity/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,23 @@ def parse_entity_options(entity_options, entity_name, parent_model)
def add_documentation_to_memo(memo_entry, documentation)
return unless documentation

memo_entry[:readOnly] = documentation[:read_only].to_s == 'true' if documentation[:read_only]
memo_entry[:description] = documentation[:desc] if documentation[:desc]
has_read_only = documentation[:read_only]
has_description = documentation[:desc]

return unless has_read_only || has_description

# In OpenAPI/Swagger 2.0 and 3.0.x, $ref cannot have sibling properties - they are ignored.
# To add description or readOnly to a $ref, wrap it in allOf.
# See: https://swagger.io/docs/specification/using-ref/
wrap_ref_in_all_of(memo_entry) if memo_entry.key?('$ref')

memo_entry[:readOnly] = documentation[:read_only].to_s == 'true' if has_read_only
memo_entry[:description] = documentation[:desc] if has_description
end

def wrap_ref_in_all_of(memo_entry)
ref = memo_entry.delete('$ref')
memo_entry['allOf'] = [{ '$ref' => ref }]
end

def handle_discriminator(parsed, required)
Expand Down
42 changes: 26 additions & 16 deletions spec/grape-swagger/entities/response_model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,16 @@ def app
{ 'text' => { 'type' => 'string', 'description' => 'Content of something.' },
'colors' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Colors' },
'created_at' => { 'type' => 'string', 'format' => 'date-time', 'description' => 'Created at the time.' },
'kind' => { '$ref' => '#/definitions/ThisApi_Entities_Kind',
'kind' => { 'allOf' => [{ '$ref' => '#/definitions/ThisApi_Entities_Kind' }],
'description' => 'The kind of this something.' },
'kind2' => { '$ref' => '#/definitions/ThisApi_Entities_Kind', 'description' => 'Secondary kind.' },
'kind3' => { '$ref' => '#/definitions/ThisApi_Entities_Kind', 'description' => 'Tertiary kind.' },
'kind2' => { 'allOf' => [{ '$ref' => '#/definitions/ThisApi_Entities_Kind' }],
'description' => 'Secondary kind.' },
'kind3' => { 'allOf' => [{ '$ref' => '#/definitions/ThisApi_Entities_Kind' }],
'description' => 'Tertiary kind.' },
'tags' => { 'type' => 'array', 'items' => { '$ref' => '#/definitions/ThisApi_Entities_Tag' },
'description' => 'Tags.' },
'relation' => { '$ref' => '#/definitions/ThisApi_Entities_Relation', 'description' => 'A related model.',
'x-other' => 'stuff' },
'relation' => { 'allOf' => [{ '$ref' => '#/definitions/ThisApi_Entities_Relation' }],
'description' => 'A related model.', 'x-other' => 'stuff' },
'code' => { 'type' => 'string', 'description' => 'Error code' },
'message' => { 'type' => 'string', 'description' => 'Error message' },
'attr' => { 'type' => 'string', 'description' => 'Attribute' } },
Expand Down Expand Up @@ -409,8 +411,10 @@ def app
expect(subject['TheseApi_Entities_Polymorphic']).to eql(
'type' => 'object',
'properties' => {
'kind' => { '$ref' => '#/definitions/TheseApi_Entities_Kind', 'description' => 'Polymorphic Kind' },
'values' => { '$ref' => '#/definitions/TheseApi_Entities_Values', 'description' => 'Polymorphic Values' },
'kind' => { 'allOf' => [{ '$ref' => '#/definitions/TheseApi_Entities_Kind' }],
'description' => 'Polymorphic Kind' },
'values' => { 'allOf' => [{ '$ref' => '#/definitions/TheseApi_Entities_Values' }],
'description' => 'Polymorphic Values' },
'str' => { 'type' => 'string', 'description' => 'Polymorphic String' },
'num' => { 'type' => 'integer', 'format' => 'int32', 'description' => 'Polymorphic Number' }
}
Expand All @@ -432,22 +436,28 @@ def app
'type' => 'object',
'properties' => {
'text' => { 'type' => 'string', 'description' => 'Content of something.' },
'kind' => { '$ref' => '#/definitions/TheseApi_Entities_Kind', 'description' => 'The kind of this something.' },
'kind2' => { '$ref' => '#/definitions/TheseApi_Entities_Kind', 'description' => 'Secondary kind.' },
'kind3' => { '$ref' => '#/definitions/TheseApi_Entities_Kind', 'description' => 'Tertiary kind.' },
'kind' => { 'allOf' => [{ '$ref' => '#/definitions/TheseApi_Entities_Kind' }],
'description' => 'The kind of this something.' },
'kind2' => { 'allOf' => [{ '$ref' => '#/definitions/TheseApi_Entities_Kind' }],
'description' => 'Secondary kind.' },
'kind3' => { 'allOf' => [{ '$ref' => '#/definitions/TheseApi_Entities_Kind' }],
'description' => 'Tertiary kind.' },
'tags' => { 'type' => 'array', 'items' => { '$ref' => '#/definitions/TheseApi_Entities_Tag' },
'description' => 'Tags.' },
'relation' => { '$ref' => '#/definitions/TheseApi_Entities_Relation', 'description' => 'A related model.' },
'values' => { '$ref' => '#/definitions/TheseApi_Entities_Values', 'description' => 'Tertiary kind.' },
'nested' => { '$ref' => '#/definitions/TheseApi_Entities_Nested', 'description' => 'Nested object.' },
'nested_child' => { '$ref' => '#/definitions/TheseApi_Entities_NestedChild',
'relation' => { 'allOf' => [{ '$ref' => '#/definitions/TheseApi_Entities_Relation' }],
'description' => 'A related model.' },
'values' => { 'allOf' => [{ '$ref' => '#/definitions/TheseApi_Entities_Values' }],
'description' => 'Tertiary kind.' },
'nested' => { 'allOf' => [{ '$ref' => '#/definitions/TheseApi_Entities_Nested' }],
'description' => 'Nested object.' },
'nested_child' => { 'allOf' => [{ '$ref' => '#/definitions/TheseApi_Entities_NestedChild' }],
'description' => 'Nested child object.' },
'code' => { 'type' => 'string', 'description' => 'Error code' },
'message' => { 'type' => 'string', 'description' => 'Error message' },
'polymorphic' => { '$ref' => '#/definitions/TheseApi_Entities_Polymorphic',
'polymorphic' => { 'allOf' => [{ '$ref' => '#/definitions/TheseApi_Entities_Polymorphic' }],
'description' => 'A polymorphic model.' },
'mixed' => {
'$ref' => '#/definitions/TheseApi_Entities_MixedType',
'allOf' => [{ '$ref' => '#/definitions/TheseApi_Entities_MixedType' }],
'description' => 'A model with mix of types.'
},
'attr' => { 'type' => 'string', 'description' => 'Attribute' }
Expand Down
7 changes: 4 additions & 3 deletions spec/grape-swagger/entity/parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
let(:endpoint) { nil }

it 'parses the model with the correct :using definition' do
expect(properties[:kind]['$ref']).to eq('#/definitions/Kind')
expect(properties[:kind2]['$ref']).to eq('#/definitions/Kind')
expect(properties[:kind3]['$ref']).to eq('#/definitions/Kind')
# $ref is wrapped in allOf when description is present (OpenAPI compliance)
expect(properties[:kind]['allOf']).to eq([{ '$ref' => '#/definitions/Kind' }])
expect(properties[:kind2]['allOf']).to eq([{ '$ref' => '#/definitions/Kind' }])
expect(properties[:kind3]['allOf']).to eq([{ '$ref' => '#/definitions/Kind' }])
end

it 'does not mark hidden attributes as required' do
Expand Down
86 changes: 86 additions & 0 deletions spec/issues/44_desc_in_entity_type_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# frozen_string_literal: true

describe '#44 desc not rendered for non-array entity references' do
subject(:swagger_doc) do
get '/swagger_doc'
JSON.parse(last_response.body)
end

let(:app) do
Class.new(Grape::API) do
namespace :issue44 do
class Address < Grape::Entity
expose :street, documentation: { type: 'string', desc: 'Street' }
end

class Client < Grape::Entity
expose :name, documentation: { type: 'string', desc: 'Name' }
expose :address,
using: Address,
documentation: { type: 'Address', desc: 'Client address', is_array: false }
expose :billing_address,
using: Address,
documentation: { type: 'Address', read_only: true }
end

class ClientWithArrayAddress < Grape::Entity
expose :name, documentation: { type: 'string', desc: 'Name' }
expose :addresses,
using: Address,
documentation: { type: 'Address', desc: 'Client addresses', is_array: true }
end

desc 'Get a client', success: Client
get '/client' do
present({ name: 'John', address: { street: '123 Main St' } }, with: Client)
end

desc 'Get a client with addresses', success: ClientWithArrayAddress
get '/client_with_addresses' do
present({ name: 'John', addresses: [{ street: '123 Main St' }] }, with: ClientWithArrayAddress)
end
end

add_swagger_documentation format: :json
end
end

describe 'non-array entity reference' do
subject(:address_property) { swagger_doc['definitions']['Client']['properties']['address'] }

it 'includes description using allOf wrapper for OpenAPI compatibility' do
# In OpenAPI/Swagger, $ref cannot have sibling properties.
# To add description to a $ref, it must be wrapped in allOf.
# See: https://swagger.io/docs/specification/using-ref/#syntax
expect(address_property).to eq({
'allOf' => [{ '$ref' => '#/definitions/Address' }],
'description' => 'Client address'
})
end
end

describe 'non-array entity reference with readOnly' do
subject(:billing_property) { swagger_doc['definitions']['Client']['properties']['billing_address'] }

it 'includes readOnly using allOf wrapper for OpenAPI compatibility' do
# readOnly is also a sibling property that needs allOf wrapping
expect(billing_property).to eq({
'allOf' => [{ '$ref' => '#/definitions/Address' }],
'readOnly' => true
})
end
end

describe 'array entity reference' do
subject(:addresses_property) { swagger_doc['definitions']['ClientWithArrayAddress']['properties']['addresses'] }

it 'includes description directly since array wrapper allows siblings' do
# For arrays, the description can be a sibling to 'type' and 'items'
expect(addresses_property).to eq({
'type' => 'array',
'items' => { '$ref' => '#/definitions/Address' },
'description' => 'Client addresses'
})
end
end
end
Loading