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: 297

# 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 `ensure_block` on the API - [@myxoh](https://github.com/myxoh).

#### Fixes

Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
- [Nested mutually_exclusive, exactly_one_of, at_least_one_of, all_or_none_of](#nested-mutually_exclusive-exactly_one_of-at_least_one_of-all_or_none_of)
- [Namespace Validation and Coercion](#namespace-validation-and-coercion)
- [Custom Validators](#custom-validators)
- [Validation Errors](#validation-errors)
- [Validation Errors and Rescuing](#validation-errors-and-rescuing)
- [I18n](#i18n)
- [Custom Validation messages](#custom-validation-messages)
- [presence, allow_blank, values, regexp](#presence-allow_blank-values-regexp)
Expand Down Expand Up @@ -1603,7 +1603,7 @@ end

Every validation will have it's own instance of the validator, which means that the validator can have a state.

### Validation Errors
### Validation Errors and Rescuing

Validation and coercion errors are collected and an exception of type `Grape::Exceptions::ValidationErrors` is raised. If the exception goes uncaught it will respond with a status of 400 and an error message. The validation errors are grouped by parameter name and can be accessed via `Grape::Exceptions::ValidationErrors#errors`.

Expand Down Expand Up @@ -1657,6 +1657,13 @@ params do
end
```

You can ensure a block of code runs after every request (including failures) with `ensure`:
```ruby
ensure_block do
This.block(of: code) # Will definetely run after every request
end
```

### I18n

Grape supports I18n for parameter-related error messages, but will fallback to English if
Expand Down
5 changes: 5 additions & 0 deletions lib/grape/dsl/request_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ def rescue_from(*args, &block)
namespace_stackable(:rescue_options, options)
end

def ensure_block(&ensured_block)
myxoh marked this conversation as resolved.
Show resolved Hide resolved
namespace_inheritable(:ensured, true)
namespace_inheritable(:ensured_block, ensured_block)
end

# Allows you to specify a default representation entity for a
# class. This allows you to map your models to their respective
# entities once and then simply call `present` with the model.
Expand Down
4 changes: 3 additions & 1 deletion lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,9 @@ def build_stack(helpers)
rescue_options: namespace_stackable_with_hash(:rescue_options) || {},
rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers) || {},
base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers) || {},
all_rescue_handler: namespace_inheritable(:all_rescue_handler)
all_rescue_handler: namespace_inheritable(:all_rescue_handler),
ensured: namespace_inheritable(:ensured),
ensured_block: namespace_inheritable(:ensured_block)

stack.concat namespace_stackable(:middleware)

Expand Down
15 changes: 13 additions & 2 deletions lib/grape/middleware/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ 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
ensured: false,
ensured_block: nil # This should always be present if ensured is present
}
end

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

def call!(env)
@env = env

begin
error_response(catch(:error) do
return @app.call(@env)
Expand All @@ -46,9 +47,19 @@ def call!(env)
raise

run_rescue_handler(handler, error)
ensure
execute_ensured_block! if should_ensure?
end
end

def should_ensure?
options[:ensured]
end

def execute_ensured_block!
options[:ensured_block].call
myxoh marked this conversation as resolved.
Show resolved Hide resolved
end

def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil)
headers = headers.reverse_merge(Grape::Http::Headers::CONTENT_TYPE => content_type)
rack_response(format_message(message, backtrace, original_exception), status, headers)
Expand Down
61 changes: 61 additions & 0 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1628,6 +1628,67 @@ def three
end
end

describe '.ensure_block' do
let!(:code) { { has_executed: false } }

context 'when the ensure block has no exceptions' do
before do
code_to_execute = code
subject.ensure_block do
code_to_execute[:has_executed] = true
end
end

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