From 300572e4d36d81471047d621b9e538a580836b24 Mon Sep 17 00:00:00 2001 From: Grzegorz Jakubiak Date: Fri, 22 Nov 2024 11:13:52 +0100 Subject: [PATCH] WIP: Add support for primitive data types in responses --- .envrc | 1 + lib/grape-swagger/endpoint.rb | 31 +++++++-- shell.nix | 9 +++ ...se_with_models_and_primitive_types_spec.rb | 69 +++++++++++++++++++ 4 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 .envrc create mode 100644 shell.nix create mode 100644 spec/swagger_v2/api_swagger_v2_response_with_models_and_primitive_types_spec.rb diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..4a4726a5 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use_nix 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/shell.nix b/shell.nix new file mode 100644 index 00000000..6120fd96 --- /dev/null +++ b/shell.nix @@ -0,0 +1,9 @@ +{ pkgs ? import {} }: + let + pkgs = import (builtins.fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/9957cd48326fe8dbd52fdc50dd2502307f188b0d.tar.gz"; + }) {}; + in + pkgs.mkShell { + nativeBuildInputs = with pkgs.buildPackages; [ pkgs.ruby_3_3 ]; +} 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