Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom headers responses #924

Merged
merged 3 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions app/views/apipie/apipies/_method_detail.erb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
<%= render(:partial => "params", :locals => {:params => item[:returns_object]}) %>
</tbody>
</table>

<%= render(:partial => "headers", :locals => {:headers => item[:headers], :h_level => h_level+2 }) %>
<% end %>
<% end %>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -55,4 +56,16 @@ def empty_returns

{ 200 => { description: 'ok' } }
end

# @param [Array<Hash>] 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
43 changes: 34 additions & 9 deletions lib/apipie/response_description.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ 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
@scope = scope
@param_group = {scope: scope}
@additional_properties = false
@typename = typename
@headers = []

self.instance_exec(&block) if block

Expand Down Expand Up @@ -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

Expand All @@ -64,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]
Expand Down Expand Up @@ -105,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
Expand All @@ -118,13 +131,25 @@ def additional_properties
end
alias allow_additional_properties additional_properties

# @return [Array<Hash>]
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,
:description => Apipie.app.translate(description, lang),
: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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions spec/lib/apipie/response_description/response_object_spec.rb
Original file line number Diff line number Diff line change
@@ -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
56 changes: 56 additions & 0 deletions spec/lib/apipie/response_description_spec.rb
Original file line number Diff line number Diff line change
@@ -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