diff --git a/.rspec b/.rspec index c2e29743..de4044ae 100644 --- a/.rspec +++ b/.rspec @@ -1,3 +1,4 @@ --color --profile --format documentation +--require 'spec_helper' diff --git a/CHANGELOG.md b/CHANGELOG.md index b98063cc..aa559f7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * [#958](https://github.com/ruby-grape/grape-swagger/pull/958): Drop ruby-head from test matrix - [@numbata](https://github.com/numbata). * [#953](https://github.com/ruby-grape/grape-swagger/pull/953): Added `super_diff` - [@numbata](https://github.com/numbata). * [#951](https://github.com/ruby-grape/grape-swagger/pull/951): Use `x-example` for non-body parameters - [@olivier-thatch](https://github.com/olivier-thatch). +* [#963](https://github.com/ruby-grape/grape-swagger/pull/963): Allow empty model definitions for swagger 2.0 - [@numbata](https://github.com/numbata). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index 9cfcb559..66255fc2 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ # frozen_string_literal: true -source 'http://rubygems.org' +source 'https://rubygems.org' gemspec diff --git a/lib/grape-swagger/doc_methods/build_model_definition.rb b/lib/grape-swagger/doc_methods/build_model_definition.rb index 444b17f8..864d4471 100644 --- a/lib/grape-swagger/doc_methods/build_model_definition.rb +++ b/lib/grape-swagger/doc_methods/build_model_definition.rb @@ -22,7 +22,7 @@ def parse_params_from_model(parsed_response, model, model_name) } else properties, required = parsed_response - unless properties&.any? + if properties.nil? raise GrapeSwagger::Errors::SwaggerSpec, "Empty model #{model_name}, swagger 2.0 doesn't support empty definitions." end diff --git a/spec/issues/539_array_post_body_spec.rb b/spec/issues/539_array_post_body_spec.rb index 0bf43eca..2476db74 100644 --- a/spec/issues/539_array_post_body_spec.rb +++ b/spec/issues/539_array_post_body_spec.rb @@ -74,7 +74,8 @@ class ArrayOfElements < Grape::Entity 'id' => { 'type' => 'string' }, 'description' => { 'type' => 'string' }, 'role' => { 'type' => 'string' } - } + }, + 'required' => %w[id description role] } ) end diff --git a/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb b/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb new file mode 100644 index 00000000..46f6237e --- /dev/null +++ b/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +describe '#962 polymorphic entity with custom documentation' do + let(:app) do + Class.new(Grape::API) do + namespace :issue_962 do + module Issue962 + class EmptyEntity < Grape::Entity + end + + class EntityWithHiddenProperty < Grape::Entity + expose :hidden_prop, documentation: { hidden: true, desc: 'This property is not exposed.' } + end + + class EntityWithNestedEmptyEntity < Grape::Entity + expose :array_of_empty_entities, + as: :empty_items, + using: Issue962::EmptyEntity, + documentation: { + is_array: true, + desc: 'This is a nested empty entity.' + } + expose :array_of_hidden_entities, + as: :hidden_items, + using: Issue962::EntityWithHiddenProperty, + documentation: { + is_array: true, + desc: 'This is a nested entity with hidden props' + } + end + end + + desc 'Get a report', + success: Issue962::EntityWithNestedEmptyEntity + get '/' do + present({ foo: [] }, with: Issue962::EntityWithNestedEmptyEntity) + end + end + + add_swagger_documentation format: :json + end + end + + subject do + get '/swagger_doc' + JSON.parse(last_response.body) + end + + let(:definitions) { subject['definitions'] } + let(:entity_definition) { definitions['Issue962_EntityWithNestedEmptyEntity'] } + let(:empty_items_property) { entity_definition['properties']['empty_items'] } + let(:hidden_items_property) { entity_definition['properties']['hidden_items'] } + let(:empty_entity_definition) { definitions['Issue962_EmptyEntity'] } + let(:hidden_entity_definition) { definitions['Issue962_EntityWithHiddenProperty'] } + + specify 'should generate swagger documentation without error' do + expect { subject }.not_to raise_error + end + + specify do + expect(definitions.keys).to include( + 'Issue962_EntityWithNestedEmptyEntity', + 'Issue962_EntityWithHiddenProperty', + 'Issue962_EmptyEntity' + ) + end + + specify do + expect(empty_items_property).to eql({ + 'type' => 'array', + 'description' => 'This is a nested empty entity.', + 'items' => { + '$ref' => '#/definitions/Issue962_EmptyEntity' + } + }) + end + + specify do + expect(hidden_items_property).to eql({ + 'type' => 'array', + 'description' => 'This is a nested entity with hidden props', + 'items' => { + '$ref' => '#/definitions/Issue962_EntityWithHiddenProperty' + } + }) + end + + specify do + expect(empty_entity_definition).to eql({ + 'type' => 'object', + 'properties' => {} + }) + end + + specify do + expect(hidden_entity_definition).to eql({ + 'type' => 'object', + 'properties' => {}, + 'required' => ['hidden_prop'] + }) + end + + let(:response_schema) { subject['paths']['/issue_962']['get']['responses']['200']['schema'] } + + specify do + expect(response_schema).to eql({ + '$ref' => '#/definitions/Issue962_EntityWithNestedEmptyEntity' + }) + end +end diff --git a/spec/support/model_parsers/entity_parser.rb b/spec/support/model_parsers/entity_parser.rb index 5f2a47e9..a9984d54 100644 --- a/spec/support/model_parsers/entity_parser.rb +++ b/spec/support/model_parsers/entity_parser.rb @@ -135,33 +135,44 @@ class DocumentedHashAndArrayModel < Grape::Entity let(:swagger_definitions_models) do { - 'ApiError' => { 'type' => 'object', 'properties' => { 'code' => { 'type' => 'integer', 'format' => 'int32', 'description' => 'status code' }, 'message' => { 'type' => 'string', 'description' => 'error message' } } }, - 'ResponseItem' => { 'type' => 'object', 'properties' => { 'id' => { 'type' => 'integer', 'format' => 'int32' }, 'name' => { 'type' => 'string' } } }, - 'UseResponse' => { 'type' => 'object', 'properties' => { 'description' => { 'type' => 'string' }, '$responses' => { 'type' => 'array', 'items' => { '$ref' => '#/definitions/ResponseItem' } } } }, - 'RecursiveModel' => { 'type' => 'object', 'properties' => { 'name' => { 'type' => 'string', 'description' => 'The name.' }, 'children' => { 'type' => 'array', 'items' => { '$ref' => '#/definitions/RecursiveModel' }, 'description' => 'The child nodes.' } } }, - 'DocumentedHashAndArrayModel' => { 'type' => 'object', 'properties' => { 'raw_hash' => { 'type' => 'object', 'description' => 'Example Hash.' }, 'raw_array' => { 'type' => 'array', 'description' => 'Example Array' } } } + 'ApiError' => { 'type' => 'object', 'properties' => { 'code' => { 'type' => 'integer', 'format' => 'int32', 'description' => 'status code' }, 'message' => { 'type' => 'string', 'description' => 'error message' } }, 'required' => %w[code message] }, + 'ResponseItem' => { 'type' => 'object', 'properties' => { 'id' => { 'type' => 'integer', 'format' => 'int32' }, 'name' => { 'type' => 'string' } }, 'required' => %w[id name] }, + 'UseResponse' => { 'type' => 'object', 'properties' => { 'description' => { 'type' => 'string' }, '$responses' => { 'type' => 'array', 'items' => { '$ref' => '#/definitions/ResponseItem' } } }, 'required' => ['description', '$responses'] }, + 'RecursiveModel' => { 'type' => 'object', 'properties' => { 'name' => { 'type' => 'string', 'description' => 'The name.' }, 'children' => { 'type' => 'array', 'items' => { '$ref' => '#/definitions/RecursiveModel' }, 'description' => 'The child nodes.' } }, 'required' => %w[name children] }, + 'DocumentedHashAndArrayModel' => { 'type' => 'object', 'properties' => { 'raw_hash' => { 'type' => 'object', 'description' => 'Example Hash.' }, 'raw_array' => { 'type' => 'array', 'description' => 'Example Array' } }, 'required' => %w[raw_hash raw_array] } } end let(:swagger_nested_type) do { - 'ApiError' => { 'type' => 'object', 'properties' => { 'code' => { 'type' => 'integer', 'format' => 'int32', 'description' => 'status code' }, 'message' => { 'type' => 'string', 'description' => 'error message' } }, 'description' => 'ApiError model' }, - 'ResponseItem' => { 'type' => 'object', 'properties' => { 'id' => { 'type' => 'integer', 'format' => 'int32' }, 'name' => { 'type' => 'string' } } }, - 'UseItemResponseAsType' => { 'type' => 'object', 'properties' => { 'description' => { 'type' => 'string' }, 'responses' => { '$ref' => '#/definitions/ResponseItem' } }, 'description' => 'UseItemResponseAsType model' } + 'ApiError' => { 'type' => 'object', 'properties' => { 'code' => { 'type' => 'integer', 'format' => 'int32', 'description' => 'status code' }, 'message' => { 'type' => 'string', 'description' => 'error message' } }, 'required' => %w[code message], 'description' => 'ApiError model' }, + 'ResponseItem' => { 'type' => 'object', 'properties' => { 'id' => { 'type' => 'integer', 'format' => 'int32' }, 'name' => { 'type' => 'string' } }, 'required' => %w[id name] }, + 'UseItemResponseAsType' => { 'type' => 'object', 'properties' => { 'description' => { 'type' => 'string' }, 'responses' => { '$ref' => '#/definitions/ResponseItem' } }, 'required' => %w[description responses], 'description' => 'UseItemResponseAsType model' } } end let(:swagger_entity_as_response_object) do { - 'ApiError' => { 'type' => 'object', 'properties' => { 'code' => { 'type' => 'integer', 'format' => 'int32', 'description' => 'status code' }, 'message' => { 'type' => 'string', 'description' => 'error message' } }, 'description' => 'ApiError model' }, - 'ResponseItem' => { 'type' => 'object', 'properties' => { 'id' => { 'type' => 'integer', 'format' => 'int32' }, 'name' => { 'type' => 'string' } } }, - 'UseResponse' => { 'type' => 'object', 'properties' => { 'description' => { 'type' => 'string' }, '$responses' => { 'type' => 'array', 'items' => { '$ref' => '#/definitions/ResponseItem' } } }, 'description' => 'UseResponse model' } + 'ApiError' => { 'type' => 'object', 'properties' => { 'code' => { 'type' => 'integer', 'format' => 'int32', 'description' => 'status code' }, 'message' => { 'type' => 'string', 'description' => 'error message' } }, 'required' => %w[code message], 'description' => 'ApiError model' }, + 'ResponseItem' => { 'type' => 'object', 'properties' => { 'id' => { 'type' => 'integer', 'format' => 'int32' }, 'name' => { 'type' => 'string' } }, 'required' => %w[id name] }, + 'UseResponse' => { 'type' => 'object', 'properties' => { 'description' => { 'type' => 'string' }, '$responses' => { 'type' => 'array', 'items' => { '$ref' => '#/definitions/ResponseItem' } } }, 'required' => ['description', '$responses'], 'description' => 'UseResponse model' } } end let(:swagger_params_as_response_object) do { - 'ApiError' => { 'type' => 'object', 'properties' => { 'code' => { 'description' => 'status code', 'type' => 'integer', 'format' => 'int32' }, 'message' => { 'description' => 'error message', 'type' => 'string' } }, 'description' => 'ApiError model' } + 'ApiError' => { + 'type' => 'object', + 'properties' => { + 'code' => { 'description' => 'status code', 'type' => 'integer', 'format' => 'int32' }, + 'message' => { 'description' => 'error message', 'type' => 'string' } + }, + 'required' => %w[ + code + message + ], + 'description' => 'ApiError model' + } } end @@ -310,6 +321,7 @@ class DocumentedHashAndArrayModel < Grape::Entity 'ApiError' => { 'type' => 'object', 'properties' => { 'code' => { 'type' => 'integer', 'format' => 'int32', 'description' => 'status code' }, 'message' => { 'type' => 'string', 'description' => 'error message' } }, + 'required' => %w[code message], 'description' => 'ApiError model' }, 'Something' => { @@ -320,6 +332,7 @@ class DocumentedHashAndArrayModel < Grape::Entity 'links' => { 'type' => 'array', 'items' => { 'type' => 'link' } }, 'others' => { 'type' => 'text' } }, + 'required' => %w[id text links others], 'description' => 'Something model' } } diff --git a/spec/swagger_v2/errors_spec.rb b/spec/swagger_v2/errors_spec.rb index f42eae43..957de022 100644 --- a/spec/swagger_v2/errors_spec.rb +++ b/spec/swagger_v2/errors_spec.rb @@ -34,8 +34,8 @@ end end - it 'should raise SwaggerSpec exception' do - expect { get '/v3/swagger_doc' }.to raise_error(GrapeSwagger::Errors::SwaggerSpec, "Empty model EmptyClass, swagger 2.0 doesn't support empty definitions.") + it 'should not raise SwaggerSpec exception' do + expect { get '/v3/swagger_doc' }.not_to raise_error(GrapeSwagger::Errors::SwaggerSpec) end end diff --git a/spec/swagger_v2/reference_entity_spec.rb b/spec/swagger_v2/reference_entity_spec.rb index ab7ae247..834dc85e 100644 --- a/spec/swagger_v2/reference_entity_spec.rb +++ b/spec/swagger_v2/reference_entity_spec.rb @@ -90,7 +90,9 @@ def app expect(subject['definitions'].keys).to include 'SomethingCustom' expect(subject['definitions']['SomethingCustom']).to eq( - 'type' => 'object', 'properties' => { 'text' => { 'type' => 'string', 'description' => 'Content of something.' } } + 'type' => 'object', + 'properties' => { 'text' => { 'type' => 'string', 'description' => 'Content of something.' } }, + 'required' => ['text'] ) expect(subject['definitions'].keys).to include 'KindCustom' @@ -103,6 +105,7 @@ def app 'description' => 'Something interesting.' } }, + 'required' => %w[title something], 'description' => 'KindCustom model' ) end @@ -122,6 +125,7 @@ def app 'title' => { 'type' => 'string', 'description' => 'Title of the parent.' }, 'child' => { 'type' => 'string', 'description' => 'Child property.' } }, + 'required' => %w[title child], 'description' => 'MyAPI::Child model' ) end