Skip to content

Commit

Permalink
Custom headers responses (#924)
Browse files Browse the repository at this point in the history
* Support headers in responses

* Reorder methods

---------
  • Loading branch information
PanosCodes authored May 28, 2024
1 parent 1ea8d7a commit 41a6c5a
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 10 deletions.
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

0 comments on commit 41a6c5a

Please sign in to comment.