From c54ed8f35cb7591b951f7a89b8990586a9921b66 Mon Sep 17 00:00:00 2001 From: Grzegorz Jakubiak Date: Fri, 22 Nov 2024 12:30:27 +0100 Subject: [PATCH 1/4] Add support for primitive data types in responses --- lib/grape-swagger/endpoint.rb | 31 +++++++-- ...se_with_models_and_primitive_types_spec.rb | 69 +++++++++++++++++++ 2 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 spec/swagger_v2/api_swagger_v2_response_with_models_and_primitive_types_spec.rb diff --git a/lib/grape-swagger/endpoint.rb b/lib/grape-swagger/endpoint.rb index 61a6a82f..75d92106 100644 --- a/lib/grape-swagger/endpoint.rb +++ b/lib/grape-swagger/endpoint.rb @@ -207,11 +207,8 @@ def response_object(route, options) next build_file_response(memo[value[:code]]) if file_response?(value[:model]) - if memo.key?(200) && route.request_method == 'DELETE' && value[:model].nil? - memo[204] = memo.delete(200) - value[:code] = 204 - next - end + next build_delete_response(memo, value) if delete_response?(memo, route, value) + next build_primitive_response(memo, route, value, options) if value[:type] # Explicitly request no model with { model: '' } next if value[:model] == '' @@ -284,6 +281,15 @@ def default_code_from_route(route) [default_code] end + def build_delete_response(memo, value) + memo[204] = memo.delete(200) + value[:code] = 204 + end + + def delete_response?(memo, route, value) + memo.key?(200) && route.request_method == 'DELETE' && value[:model].nil? + end + def build_memo_schema(memo, route, value, response_model, options) if memo[value[:code]][:schema] && value[:as] memo[value[:code]][:schema][:properties].merge!(build_reference(route, value, response_model, options)) @@ -304,6 +310,18 @@ def build_memo_schema(memo, route, value, response_model, options) end end + def build_primitive_response(memo, _route, value, _options) + type = GrapeSwagger::DocMethods::DataType.call(value[:type]) + + if memo[value[:code]].include?(:schema) && value.include?(:as) + memo[value[:code]][:schema][:properties].merge!(value[:as] => { type: type }) + elsif value.include?(:as) + memo[value[:code]][:schema] = { type: :object, properties: { value[:as] => { type: type } } } + else + memo[value[:code]][:schema] = { type: type } + end + end + def build_reference(route, value, response_model, settings) # TODO: proof that the definition exist, if model isn't specified reference = if value.key?(:as) @@ -387,7 +405,7 @@ def get_path_params(stackable_values) return param unless stackable_values return params unless stackable_values.is_a? Grape::Util::StackableValues - stackable_values&.new_values&.dig(:namespace)&.each do |namespace| + stackable_values&.new_values&.dig(:namespace)&.each do |namespace| # rubocop:disable Style/SafeNavigationChainLength space = namespace.space.to_s.gsub(':', '') params[space] = namespace.options || {} end @@ -464,6 +482,7 @@ def success_code_from_entity(route, entity) default_code[:as] = entity[:as] if entity[:as] default_code[:is_array] = entity[:is_array] if entity[:is_array] default_code[:required] = entity[:required] if entity[:required] + default_code[:type] = entity[:type] if entity[:type] else default_code = GrapeSwagger::DocMethods::StatusCodes.get[route.request_method.downcase.to_sym] default_code[:model] = entity if entity diff --git a/spec/swagger_v2/api_swagger_v2_response_with_models_and_primitive_types_spec.rb b/spec/swagger_v2/api_swagger_v2_response_with_models_and_primitive_types_spec.rb new file mode 100644 index 00000000..95bd97aa --- /dev/null +++ b/spec/swagger_v2/api_swagger_v2_response_with_models_and_primitive_types_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'response' do + include_context "#{MODEL_PARSER} swagger example" + + before :all do + module TheApi + class ResponseApiModelsAndPrimitiveTypes < Grape::API + format :json + + desc 'This returns something', + success: [ + { type: 'Integer', as: :integer_response }, + { model: Entities::UseResponse, as: :user_response }, + { type: 'String', as: :string_response } + ], + failure: [ + { code: 400, message: 'NotFound', model: '' }, + { code: 404, message: 'BadRequest', model: Entities::ApiError } + ], + default_response: { message: 'Error', model: Entities::ApiError } + get '/use-response' do + { 'declared_params' => declared(params) } + end + + add_swagger_documentation + end + end + end + + def app + TheApi::ResponseApiModelsAndPrimitiveTypes + end + + describe 'uses entity as response object implicitly with route name' do + subject do + get '/swagger_doc/use-response' + JSON.parse(last_response.body) + end + + specify do + expect(subject['paths']['/use-response']['get']).to eql( + 'description' => 'This returns something', + 'produces' => ['application/json'], + 'responses' => { + '200' => { + 'description' => 'This returns something', + 'schema' => { + 'type' => 'object', + 'properties' => { + 'user_response' => { '$ref' => '#/definitions/UseResponse' }, + 'integer_response' => { 'type' => 'integer' }, + 'string_response' => { 'type' => 'string' } + } + } + }, + '400' => { 'description' => 'NotFound' }, + '404' => { 'description' => 'BadRequest', 'schema' => { '$ref' => '#/definitions/ApiError' } }, + 'default' => { 'description' => 'Error', 'schema' => { '$ref' => '#/definitions/ApiError' } } + }, + 'tags' => ['use-response'], + 'operationId' => 'getUseResponse' + ) + expect(subject['definitions']).to eql(swagger_entity_as_response_object) + end + end +end From e7a8acea51373991922cf0df886855fcec0cb53f Mon Sep 17 00:00:00 2001 From: Grzegorz Jakubiak Date: Fri, 22 Nov 2024 15:05:31 +0100 Subject: [PATCH 2/4] Use `format` option --- lib/grape-swagger/endpoint.rb | 8 +++++--- ...er_v2_response_with_models_and_primitive_types_spec.rb | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/grape-swagger/endpoint.rb b/lib/grape-swagger/endpoint.rb index 75d92106..58b54f58 100644 --- a/lib/grape-swagger/endpoint.rb +++ b/lib/grape-swagger/endpoint.rb @@ -311,12 +311,14 @@ def build_memo_schema(memo, route, value, response_model, options) end def build_primitive_response(memo, _route, value, _options) - type = GrapeSwagger::DocMethods::DataType.call(value[:type]) + data_type = GrapeSwagger::DocMethods::DataType.call(value[:type]) + type, format = GrapeSwagger::DocMethods::DataType.mapping(data_type) if memo[value[:code]].include?(:schema) && value.include?(:as) - memo[value[:code]][:schema][:properties].merge!(value[:as] => { type: type }) + memo[value[:code]][:schema][:properties].merge!(value[:as] => { type: type, format: format }.compact) elsif value.include?(:as) - memo[value[:code]][:schema] = { type: :object, properties: { value[:as] => { type: type } } } + memo[value[:code]][:schema] = + { type: :object, properties: { value[:as] => { type: type, format: format }.compact } } else memo[value[:code]][:schema] = { type: type } end diff --git a/spec/swagger_v2/api_swagger_v2_response_with_models_and_primitive_types_spec.rb b/spec/swagger_v2/api_swagger_v2_response_with_models_and_primitive_types_spec.rb index 95bd97aa..7f0d9f9a 100644 --- a/spec/swagger_v2/api_swagger_v2_response_with_models_and_primitive_types_spec.rb +++ b/spec/swagger_v2/api_swagger_v2_response_with_models_and_primitive_types_spec.rb @@ -14,7 +14,8 @@ class ResponseApiModelsAndPrimitiveTypes < Grape::API success: [ { type: 'Integer', as: :integer_response }, { model: Entities::UseResponse, as: :user_response }, - { type: 'String', as: :string_response } + { type: 'String', as: :string_response }, + { type: 'Float', as: :float_response } ], failure: [ { code: 400, message: 'NotFound', model: '' }, @@ -51,8 +52,9 @@ def app 'type' => 'object', 'properties' => { 'user_response' => { '$ref' => '#/definitions/UseResponse' }, - 'integer_response' => { 'type' => 'integer' }, - 'string_response' => { 'type' => 'string' } + 'integer_response' => { 'type' => 'integer', 'format' => 'int32' }, + 'string_response' => { 'type' => 'string' }, + 'float_response' => { 'type' => 'number', 'format' => 'float' } } } }, From 1cce60702d9ca553e6b682663f147b081b0fcf16 Mon Sep 17 00:00:00 2001 From: Grzegorz Jakubiak Date: Fri, 22 Nov 2024 15:55:17 +0100 Subject: [PATCH 3/4] Handle `Hash` type --- lib/grape-swagger/endpoint.rb | 17 +++++++++++++---- ...onse_with_models_and_primitive_types_spec.rb | 6 ++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/grape-swagger/endpoint.rb b/lib/grape-swagger/endpoint.rb index 58b54f58..15509be5 100644 --- a/lib/grape-swagger/endpoint.rb +++ b/lib/grape-swagger/endpoint.rb @@ -208,7 +208,7 @@ def response_object(route, options) next build_file_response(memo[value[:code]]) if file_response?(value[:model]) next build_delete_response(memo, value) if delete_response?(memo, route, value) - next build_primitive_response(memo, route, value, options) if value[:type] + next build_response_for_type_parameter(memo, route, value, options) if value[:type] # Explicitly request no model with { model: '' } next if value[:model] == '' @@ -310,9 +310,8 @@ def build_memo_schema(memo, route, value, response_model, options) end end - def build_primitive_response(memo, _route, value, _options) - data_type = GrapeSwagger::DocMethods::DataType.call(value[:type]) - type, format = GrapeSwagger::DocMethods::DataType.mapping(data_type) + def build_response_for_type_parameter(memo, _route, value, _options) + type, format = prepare_type_and_format(value) if memo[value[:code]].include?(:schema) && value.include?(:as) memo[value[:code]][:schema][:properties].merge!(value[:as] => { type: type, format: format }.compact) @@ -324,6 +323,16 @@ def build_primitive_response(memo, _route, value, _options) end end + def prepare_type_and_format(value) + data_type = GrapeSwagger::DocMethods::DataType.call(value[:type]) + + if GrapeSwagger::DocMethods::DataType.primitive?(data_type) + GrapeSwagger::DocMethods::DataType.mapping(data_type) + else + data_type + end + end + def build_reference(route, value, response_model, settings) # TODO: proof that the definition exist, if model isn't specified reference = if value.key?(:as) diff --git a/spec/swagger_v2/api_swagger_v2_response_with_models_and_primitive_types_spec.rb b/spec/swagger_v2/api_swagger_v2_response_with_models_and_primitive_types_spec.rb index 7f0d9f9a..8050f1b6 100644 --- a/spec/swagger_v2/api_swagger_v2_response_with_models_and_primitive_types_spec.rb +++ b/spec/swagger_v2/api_swagger_v2_response_with_models_and_primitive_types_spec.rb @@ -15,7 +15,8 @@ class ResponseApiModelsAndPrimitiveTypes < Grape::API { type: 'Integer', as: :integer_response }, { model: Entities::UseResponse, as: :user_response }, { type: 'String', as: :string_response }, - { type: 'Float', as: :float_response } + { type: 'Float', as: :float_response }, + { type: 'Hash', as: :hash_response } ], failure: [ { code: 400, message: 'NotFound', model: '' }, @@ -54,7 +55,8 @@ def app 'user_response' => { '$ref' => '#/definitions/UseResponse' }, 'integer_response' => { 'type' => 'integer', 'format' => 'int32' }, 'string_response' => { 'type' => 'string' }, - 'float_response' => { 'type' => 'number', 'format' => 'float' } + 'float_response' => { 'type' => 'number', 'format' => 'float' }, + 'hash_response' => { 'type' => 'object' } } } }, From ec556528267d4d035fd9a06048b9a2e1cb2b3571 Mon Sep 17 00:00:00 2001 From: Grzegorz Jakubiak Date: Tue, 7 Jan 2025 08:39:01 +0100 Subject: [PATCH 4/4] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09ae4c7d..9ef85cf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ #### Features -* Your contribution here. +* [#945](https://github.com/ruby-grape/grape-swagger/pull/945): Add support for primitive data types in responses - [@gregg-platogo](https://github.com/gregg-platogo). #### Fixes