From 1bad03e0fd7b789b0aeeb26720e27ecf60c50215 Mon Sep 17 00:00:00 2001 From: Panos Dalitsouris Date: Thu, 4 Apr 2024 09:31:57 +0300 Subject: [PATCH 1/2] Support headers in responses --- README.md | 19 ++++++ app/views/apipie/apipies/_method_detail.erb | 2 + .../method_description/response_service.rb | 15 ++++- lib/apipie/response_description.rb | 26 ++++++++ .../response_service_spec.rb | 62 +++++++++++++++++++ .../response_object_spec.rb | 22 +++++++ spec/lib/apipie/response_description_spec.rb | 56 +++++++++++++++++ 7 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 spec/lib/apipie/generator/swagger/method_description/response_service_spec.rb create mode 100644 spec/lib/apipie/response_description/response_object_spec.rb create mode 100644 spec/lib/apipie/response_description_spec.rb diff --git a/README.md b/README.md index 80be5af00..825569db4 100644 --- a/README.md +++ b/README.md @@ -630,6 +630,25 @@ end Note the use of the `property` keyword rather than `param`. This is the preferred mechanism for documenting response-only fields. +#### Specify response headers + +We can specify the response headers using the `header` keyword within the `returns` block. + +##### Example +```ruby +api :GET, "/pets/:id/with-extra-details", "Get a detailed pet record" +returns code: 200, desc: "Detailed info about the pet" do + param_group :pet + property :num_legs, Integer, :desc => "How many legs the pet has" + header 'Link', String, 'Relative links' + header 'Current-Page', Integer, 'The current page', required: true +end + +def show + render JSON({ :pet_name => "Barkie", :animal_type => "iguana", :legs => 4 }) +end +``` + #### The Property keyword `property` is very similar to `param` with the following differences: diff --git a/app/views/apipie/apipies/_method_detail.erb b/app/views/apipie/apipies/_method_detail.erb index 15767ecc4..390a9b655 100644 --- a/app/views/apipie/apipies/_method_detail.erb +++ b/app/views/apipie/apipies/_method_detail.erb @@ -55,6 +55,8 @@ <%= render(:partial => "params", :locals => {:params => item[:returns_object]}) %> + + <%= render(:partial => "headers", :locals => {:headers => item[:headers], :h_level => h_level+2 }) %> <% end %> <% end %> diff --git a/lib/apipie/generator/swagger/method_description/response_service.rb b/lib/apipie/generator/swagger/method_description/response_service.rb index 0b7fc7ec3..c1d5449f1 100644 --- a/lib/apipie/generator/swagger/method_description/response_service.rb +++ b/lib/apipie/generator/swagger/method_description/response_service.rb @@ -39,7 +39,8 @@ def responses allow_null: false, http_method: @http_method, controller_method: @method_description - ).to_swagger + ).to_swagger, + headers: response_headers(response.headers) }.compact end end @@ -55,4 +56,16 @@ def empty_returns { 200 => { description: 'ok' } } end + + # @param [Array] headers + # + # https://swagger.io/specification/v2/#header-object + def response_headers(headers) + headers.each_with_object({}) do |header, result| + result[header[:name].to_s] = { + description: header[:description], + type: header[:validator] + } + end + end end diff --git a/lib/apipie/response_description.rb b/lib/apipie/response_description.rb index 7c11eea1f..775998c41 100644 --- a/lib/apipie/response_description.rb +++ b/lib/apipie/response_description.rb @@ -6,6 +6,7 @@ class ResponseObject include Apipie::DSL::Param attr_accessor :additional_properties, :typename + attr_reader :headers def initialize(method_description, scope, block, typename) @method_description = method_description @@ -13,6 +14,7 @@ def initialize(method_description, scope, block, typename) @param_group = {scope: scope} @additional_properties = false @typename = typename + @headers = [] self.instance_exec(&block) if block @@ -43,6 +45,18 @@ def prepare_hash_params end end + # @param [String] header_name + # @param [String, symbol, Class] validator + # @param [String] description + # @param [Hash] options + def header(header_name, validator, description, options = {}) + @headers << { + name: header_name, + validator: validator.to_s.downcase, + description: description, + options: options + } + end end end @@ -118,6 +132,17 @@ def additional_properties end alias allow_additional_properties additional_properties + # @return [Array] + def headers + # TODO: Support headers for Apipie::ResponseDescriptionAdapter + if @response_object.is_a?(Apipie::ResponseDescriptionAdapter) + return [] + end + + @response_object.headers + end + + # @return [Hash{Symbol->TrueClass | FalseClass}] def to_json(lang = nil) { :code => code, @@ -125,6 +150,7 @@ def to_json(lang = nil) :is_array => is_array?, :returns_object => params_ordered.map{ |param| param.to_json(lang).tap{|h| h.delete(:validations) }}.flatten, :additional_properties => additional_properties, + :headers => headers } end end diff --git a/spec/lib/apipie/generator/swagger/method_description/response_service_spec.rb b/spec/lib/apipie/generator/swagger/method_description/response_service_spec.rb new file mode 100644 index 000000000..71adc015f --- /dev/null +++ b/spec/lib/apipie/generator/swagger/method_description/response_service_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe Apipie::Generator::Swagger::MethodDescription::ResponseService do + let(:http_method) { nil } + let(:language) { :en } + let(:dsl_data) { ActionController::Base.send(:_apipie_dsl_data_init) } + + let(:method_description) do + Apipie::Generator::Swagger::MethodDescription::Decorator.new( + Apipie::MethodDescription.new( + 'create', + Apipie::ResourceDescription.new(ApplicationController, 'pets'), + dsl_data + ) + ) + end + + let(:returns) { [] } + + let(:service) do + described_class.new( + method_description, + http_method: http_method, + language: language + ) + end + + describe '#call' do + describe 'headers' do + subject(:headers) { service.call[status_code][:headers] } + + let(:status_code) { 200 } + + it { is_expected.to be_blank } + + context 'when headers exists' do + let(:dsl_data) { super().merge({ returns: returns }) } + let(:returns) { { status_code => [{}, nil, returns_dsl, nil] } } + + let(:returns_dsl) do + proc do + header 'link', String, 'Relative links' + header 'Current-Page', Integer, 'The current page' + end + end + + it 'returns the correct format headers' do + expect(headers).to eq({ + 'link' => { + description: 'Relative links', + type: 'string' + }, + 'Current-Page' => { + description: 'The current page', + type: 'integer' + } + }) + end + end + end + end +end diff --git a/spec/lib/apipie/response_description/response_object_spec.rb b/spec/lib/apipie/response_description/response_object_spec.rb new file mode 100644 index 000000000..47adf3be8 --- /dev/null +++ b/spec/lib/apipie/response_description/response_object_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Apipie::ResponseDescription::ResponseObject do + describe '#header' do + let(:response_object) { described_class.new(nil, nil, nil, nil) } + let(:name) { 'Current-Page' } + let(:description) { 'The current page in the pagination' } + + before { response_object.header(name, String, description) } + + it 'adds it to the headers' do + expect(response_object.headers).to( + contain_exactly({ + name: name, + description: description, + validator: 'string', + options: {} + }) + ) + end + end +end diff --git a/spec/lib/apipie/response_description_spec.rb b/spec/lib/apipie/response_description_spec.rb new file mode 100644 index 000000000..f9f0fb2ee --- /dev/null +++ b/spec/lib/apipie/response_description_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe Apipie::ResponseDescription do + let(:resource_description) do + Apipie::ResourceDescription.new(PetsController, 'pets') + end + + let(:method_description) do + Apipie::MethodDescription.new( + 'create', + resource_description, + ActionController::Base.send(:_apipie_dsl_data_init) + ) + end + + let(:response_description_dsl) { proc {} } + let(:options) { {} } + + let(:response_description) do + described_class.new( + method_description, + 204, + options, + PetsController, + response_description_dsl, + nil + ) + end + + describe '#to_json' do + let(:to_json) { response_description.to_json } + + describe 'headers' do + subject(:headers) { to_json[:headers] } + + it { is_expected.to be_blank } + + context 'when response has headers' do + let(:response_description_dsl) do + proc do + header 'Current-Page', Integer, 'The current page in the pagination', required: true + end + end + + it 'returns the header' do + expect(headers).to contain_exactly({ + name: 'Current-Page', + description: 'The current page in the pagination', + validator: 'integer', + options: { required: true } + }) + end + end + end + end +end From 5887cad7b90f94db37a103d7e835b71a79c8593b Mon Sep 17 00:00:00 2001 From: Panos Dalitsouris Date: Thu, 4 Apr 2024 09:32:19 +0300 Subject: [PATCH 2/2] Reorder methods --- lib/apipie/response_description.rb | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/apipie/response_description.rb b/lib/apipie/response_description.rb index 775998c41..fd226bfd9 100644 --- a/lib/apipie/response_description.rb +++ b/lib/apipie/response_description.rb @@ -78,15 +78,6 @@ def self.from_dsl_data(method_description, code, args) adapter) end - def is_array? - @is_array_of != false - end - - def typename - @response_object.typename - end - - def initialize(method_description, code, options, scope, block, adapter) @type_ref = options[:param_group] @@ -119,6 +110,14 @@ def initialize(method_description, code, options, scope, block, adapter) @response_object.additional_properties ||= options[:additional_properties] end + def is_array? + @is_array_of != false + end + + def typename + @response_object.typename + end + def param_description nil end