Skip to content

Commit

Permalink
Allows for a block of code to always run after the endpoint (#1864)
Browse files Browse the repository at this point in the history
Closes #1865
  • Loading branch information
myxoh authored and dblock committed Feb 26, 2019
1 parent ecaf332 commit b79bd9c
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 32 deletions.
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`:

```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

0 comments on commit b79bd9c

Please sign in to comment.