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

Allows for a block of code to always run after the endpoint #1864

Merged
merged 11 commits into from
Feb 26, 2019
4 changes: 2 additions & 2 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Metrics/BlockLength:
# Offense count: 9
# Configuration parameters: CountComments.
Metrics/ClassLength:
Max: 295
Max: 302

# Offense count: 31
Metrics/CyclomaticComplexity:
Expand All @@ -53,7 +53,7 @@ Metrics/LineLength:
# Offense count: 57
# Configuration parameters: CountComments.
Metrics/MethodLength:
Max: 33
Max: 34

# Offense count: 12
# Configuration parameters: CountComments.
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#### Features

* Your contribution here.
* [#1864](https://github.com/ruby-grape/grape/pull/1864): Adds `finally` on the API - [@myxoh](https://github.com/myxoh).

#### Fixes

Expand Down
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
- [Register custom middleware for authentication](#register-custom-middleware-for-authentication)
- [Describing and Inspecting an API](#describing-and-inspecting-an-api)
- [Current Route and Endpoint](#current-route-and-endpoint)
- [Before and After](#before-and-after)
- [Before, After and Finally](#before-after-and-finally)
- [Anchoring](#anchoring)
- [Using Custom Middleware](#using-custom-middleware)
- [Grape Middleware](#grape-middleware)
Expand Down Expand Up @@ -3089,19 +3089,22 @@ class ApiLogger < Grape::Middleware::Base
end
```

## Before and After
## Before, After and Finally

Blocks can be executed before or after every API call, using `before`, `after`,
`before_validation` and `after_validation`.
If the API fails the `after` call will not be trigered, if you need code to execute for sure
use the `finally`.

Before and after callbacks execute in the following order:

1. `before`
2. `before_validation`
3. _validations_
4. `after_validation`
5. _the API call_
6. `after`
4. `after_validation` (upon successful validation)
5. _the API call_ (upon successful validation)
6. `after` (upon successful validation and API call)
7. `finally` (always)

Steps 4, 5 and 6 only happen if validation succeeds.

Expand All @@ -3121,6 +3124,14 @@ before do
end
```

You can ensure a block of code runs after every request (including failures) with `finally`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a space below.


```ruby
finally do
# this code will run after every request (successful or failed)
end
```

**Namespaces**

Callbacks apply to each API call within and below the current namespace:
Expand Down
20 changes: 20 additions & 0 deletions lib/grape/dsl/callbacks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,26 @@ def after_validation(&block)
def after(&block)
namespace_stackable(:afters, block)
end

# Allows you to specify a something that will always be executed after a call
# API call. Unlike the `after` block, this code will run even on
# unsuccesful requests.
# @example
# class ExampleAPI < Grape::API
# before do
# ApiLogger.start
# end
# finally do
# ApiLogger.close
# end
# end
#
# This will make sure that the ApiLogger is opened and close around every
# request
# @param ensured_block [Proc] The block to be executed after every api_call
def finally(&block)
namespace_stackable(:finallies, block)
end
end
end
end
Expand Down
54 changes: 31 additions & 23 deletions lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -245,33 +245,37 @@ def run
@request = Grape::Request.new(env, build_params_with: namespace_inheritable(:build_params_with))
@params = @request.params
@headers = @request.headers
begin
cookies.read(@request)
self.class.run_before_each(self)
run_filters befores, :before

if (allowed_methods = env[Grape::Env::GRAPE_ALLOWED_METHODS])
raise Grape::Exceptions::MethodNotAllowed, header.merge('Allow' => allowed_methods) unless options?
header 'Allow', allowed_methods
response_object = ''
status 204
else
run_filters before_validations, :before_validation
run_validators validations, request
remove_aliased_params
run_filters after_validations, :after_validation
response_object = @block ? @block.call(self) : nil
end

cookies.read(@request)
self.class.run_before_each(self)
run_filters befores, :before

if (allowed_methods = env[Grape::Env::GRAPE_ALLOWED_METHODS])
raise Grape::Exceptions::MethodNotAllowed, header.merge('Allow' => allowed_methods) unless options?
header 'Allow', allowed_methods
response_object = ''
status 204
else
run_filters before_validations, :before_validation
run_validators validations, request
remove_aliased_params
run_filters after_validations, :after_validation
response_object = @block ? @block.call(self) : nil
end
run_filters afters, :after
cookies.write(header)

run_filters afters, :after
cookies.write(header)
# status verifies body presence when DELETE
@body ||= response_object

# status verifies body presence when DELETE
@body ||= response_object
# The body commonly is an Array of Strings, the application instance itself, or a File-like object
response_object = file || [body]

# The Body commonly is an Array of Strings, the application instance itself, or a File-like object
response_object = file || [body]
[status, header, response_object]
[status, header, response_object]
ensure
run_filters finallies, :finally
end
end
end

Expand Down Expand Up @@ -392,6 +396,10 @@ def afters
namespace_stackable(:afters) || []
end

def finallies
namespace_stackable(:finallies) || []
end

def validations
route_setting(:saved_validations) || []
end
Expand Down
3 changes: 1 addition & 2 deletions lib/grape/middleware/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def default_options
},
rescue_handlers: {}, # rescue handler blocks
base_only_rescue_handlers: {}, # rescue handler blocks rescuing only the base class
all_rescue_handler: nil # rescue handler block to rescue from all exceptions
all_rescue_handler: nil, # rescue handler block to rescue from all exceptions
}
end

Expand All @@ -32,7 +32,6 @@ def initialize(app, **options)

def call!(env)
@env = env

begin
error_response(catch(:error) do
return @app.call(@env)
Expand Down
193 changes: 193 additions & 0 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1628,6 +1628,199 @@ def three
end
end

describe 'lifecycle' do
let!(:lifecycle) { [] }
let!(:standard_cycle) do
%i[before before_validation after_validation api_call after finally]
end

let!(:validation_error) do
%i[before before_validation finally]
end

let!(:errored_cycle) do
%i[before before_validation after_validation api_call finally]
end

before do
current_cycle = lifecycle

subject.before do
current_cycle << :before
end

subject.before_validation do
current_cycle << :before_validation
end

subject.after_validation do
current_cycle << :after_validation
end

subject.after do
current_cycle << :after
end

subject.finally do
current_cycle << :finally
end
end

context 'when the api_call succeeds' do
before do
current_cycle = lifecycle

subject.get 'api_call' do
current_cycle << :api_call
end
end

it 'follows the standard life_cycle' do
get '/api_call'
expect(lifecycle).to eq standard_cycle
end
end

context 'when the api_call has a controlled error' do
before do
current_cycle = lifecycle

subject.get 'api_call' do
current_cycle << :api_call
error!(:some_error)
end
end

it 'follows the errored life_cycle (skips after)' do
get '/api_call'
expect(lifecycle).to eq errored_cycle
end
end

context 'when the api_call has an exception' do
before do
current_cycle = lifecycle

subject.get 'api_call' do
current_cycle << :api_call
raise StandardError
end
end

it 'follows the errored life_cycle (skips after)' do
expect { get '/api_call' }.to raise_error(StandardError)
expect(lifecycle).to eq errored_cycle
end
end

context 'when the api_call fails validation' do
before do
current_cycle = lifecycle

subject.params do
requires :some_param, type: String
end

subject.get 'api_call' do
current_cycle << :api_call
end
end

it 'follows the failed_validation cycle (skips after_validation, api_call & after)' do
get '/api_call'
expect(lifecycle).to eq validation_error
end
end
end

describe '.finally' do
let!(:code) { { has_executed: false } }
let(:block_to_run) do
code_to_execute = code
proc do
code_to_execute[:has_executed] = true
end
end

context 'when the ensure block has no exceptions' do
before { subject.finally(&block_to_run) }

context 'when no API call is made' do
it 'has not executed the ensure code' do
expect(code[:has_executed]).to be false
end
end

context 'when no errors occurs' do
before do
subject.get '/no_exceptions' do
'success'
end
end

it 'executes the ensure code' do
get '/no_exceptions'
expect(last_response.body).to eq 'success'
expect(code[:has_executed]).to be true
end

context 'with a helper' do
let(:block_to_run) do
code_to_execute = code
proc do
code_to_execute[:value] = some_helper
end
end

before do
subject.helpers do
def some_helper
'some_value'
end
end

subject.get '/with_helpers' do
'success'
end
end

it 'has access to the helper' do
get '/with_helpers'
expect(code[:value]).to eq 'some_value'
end
end
end

context 'when an unhandled occurs inside the API call' do
before do
subject.get '/unhandled_exception' do
raise StandardError
end
end

it 'executes the ensure code' do
expect { get '/unhandled_exception' }.to raise_error StandardError
expect(code[:has_executed]).to be true
end
end

context 'when a handled error occurs inside the API call' do
before do
subject.rescue_from(StandardError) { error! 'handled' }
subject.get '/handled_exception' do
raise StandardError
end
end

it 'executes the ensure code' do
get '/handled_exception'
expect(code[:has_executed]).to be true
expect(last_response.body).to eq 'handled'
end
end
end
end

describe '.rescue_from' do
it 'does not rescue errors when rescue_from is not set' do
subject.get '/exception' do
Expand Down
Loading