Skip to content

Commit 37362d8

Browse files
Dieter Späthdblock
Dieter Späth
authored andcommitted
Errors can now be presented with a Grape::Entity class.
1 parent 430fac8 commit 37362d8

File tree

12 files changed

+196
-23
lines changed

12 files changed

+196
-23
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* [#703](https://github.com/intridea/grape/pull/703): Removed `Grape::Middleware::Auth::OAuth2` - [@dspaeth-faber](https://github.com/dspaeth-faber).
1111
* [#719](https://github.com/intridea/grape/pull/719): Allow passing options hash to a custom validator - [@elado](https://github.com/elado).
1212
* [#716](https://github.com/intridea/grape/pull/716): Calling `content-type` will now return the current content-type - [@dblock](https://github.com/dblock).
13+
* [#705](https://github.com/intridea/grape/pull/705): Errors can now be presented with a `Grape::Entity` class - [@dspaeth-faber](https://github.com/dspaeth-faber).
1314
* Your contribution here.
1415

1516
0.8.0 (7/10/2014)

README.md

+30-5
Original file line numberDiff line numberDiff line change
@@ -310,10 +310,10 @@ You can use `status` to query and set the actual HTTP Status Code
310310
```ruby
311311
post do
312312
status 202
313-
313+
314314
if status == 200
315315
# do some thing
316-
end
316+
end
317317
end
318318
```
319319

@@ -944,6 +944,31 @@ instead of a message.
944944
error!({ error: "unexpected error", detail: "missing widget" }, 500)
945945
```
946946

947+
You can present documented errors with a Grape entity using the the [grape-entity](https://github.com/intridea/grape-entity) gem.
948+
949+
```ruby
950+
module API
951+
class Error < Grape::Entity
952+
expose :code
953+
expose :message
954+
end
955+
end
956+
```
957+
958+
The following example specifies the entity to use in the `http_codes` definition.
959+
960+
```
961+
desc 'My Route', http_codes: [[408, 'Unauthorized', API::Error]]
962+
error!({ message: 'Unauthorized' }, 408)
963+
```
964+
965+
The following example specifies the presented entity explicitly in the error message.
966+
967+
```ruby
968+
desc 'My Route', http_codes: [[408, 'Unauthorized']]
969+
error!({ message: 'Unauthorized', with: API::Error }, 408)
970+
```
971+
947972
### Default Error HTTP Status Code
948973

949974
By default Grape returns a 500 status code from `error!`. You can change this with `default_error_status`.
@@ -1468,8 +1493,8 @@ formatter.
14681493

14691494
### Basic and Digest Auth
14701495

1471-
Grape has built-in Basic and Digest authentication (the given `block`
1472-
is executed in the context of the current `Endpoint`). Authentication
1496+
Grape has built-in Basic and Digest authentication (the given `block`
1497+
is executed in the context of the current `Endpoint`). Authentication
14731498
applies to the current namespace and any children, but not parents.
14741499

14751500
```ruby
@@ -1496,7 +1521,7 @@ For registering a Middlewar you need the following options:
14961521

14971522
* `label` - the name for your authenticator to use it later
14981523
* `MiddlewareClass` - the MiddlewareClass to use for authentication
1499-
* `option_lookup_proc` - A Proc with one Argument to lookup the options at
1524+
* `option_lookup_proc` - A Proc with one Argument to lookup the options at
15001525
runtime (return value is an `Array` as Paramter for the Middleware).
15011526

15021527
Example:

grape.gemspec

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
2424
s.add_runtime_dependency 'virtus', '>= 1.0.0'
2525
s.add_runtime_dependency 'builder'
2626

27-
s.add_development_dependency 'grape-entity', '>= 0.2.0'
27+
s.add_development_dependency 'grape-entity', '>= 0.4.4'
2828
s.add_development_dependency 'rake'
2929
s.add_development_dependency 'maruku'
3030
s.add_development_dependency 'yard'

lib/grape/dsl/inside_route.rb

+22-16
Original file line numberDiff line numberDiff line change
@@ -173,22 +173,7 @@ def present(*args)
173173
else
174174
[nil, args.first]
175175
end
176-
entity_class = options.delete(:with)
177-
178-
if entity_class.nil?
179-
# entity class not explicitely defined, auto-detect from relation#klass or first object in the collection
180-
object_class = if object.respond_to?(:klass)
181-
object.klass
182-
else
183-
object.respond_to?(:first) ? object.first.class : object.class
184-
end
185-
186-
object_class.ancestors.each do |potential|
187-
entity_class ||= (settings[:representations] || {})[potential]
188-
end
189-
190-
entity_class ||= object_class.const_get(:Entity) if object_class.const_defined?(:Entity) && object_class.const_get(:Entity).respond_to?(:represent)
191-
end
176+
entity_class = entity_class_for_obj(object, options)
192177

193178
root = options.delete(:root)
194179

@@ -216,6 +201,27 @@ def present(*args)
216201
def route
217202
env["rack.routing_args"][:route_info]
218203
end
204+
205+
def entity_class_for_obj(object, options)
206+
entity_class = options.delete(:with)
207+
208+
if entity_class.nil?
209+
# entity class not explicitely defined, auto-detect from relation#klass or first object in the collection
210+
object_class = if object.respond_to?(:klass)
211+
object.klass
212+
else
213+
object.respond_to?(:first) ? object.first.class : object.class
214+
end
215+
216+
object_class.ancestors.each do |potential|
217+
entity_class ||= (settings[:representations] || {})[potential]
218+
end
219+
220+
entity_class ||= object_class.const_get(:Entity) if object_class.const_defined?(:Entity) && object_class.const_get(:Entity).respond_to?(:represent)
221+
end
222+
223+
entity_class
224+
end
219225
end
220226
end
221227
end

lib/grape/error_formatter/base.rb

+28
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,34 @@ def formatter_for(api_format, options = {})
2626
end
2727
end
2828
end
29+
30+
module_function
31+
32+
def present(message, env)
33+
present_options = {}
34+
present_options[:with] = message.delete(:with) if message.is_a?(Hash)
35+
36+
presenter = env['api.endpoint'].entity_class_for_obj(message, present_options)
37+
38+
unless presenter || env['rack.routing_args'].nil?
39+
# env['api.endpoint'].route does not work when the error occurs within a middleware
40+
# the Endpoint does not have a valid env at this moment
41+
http_codes = env['rack.routing_args'][:route_info].route_http_codes || []
42+
found_code = http_codes.find do |http_code|
43+
(http_code[0].to_i == env['api.endpoint'].status) && http_code[2].respond_to?(:represent)
44+
end
45+
46+
presenter = found_code[2] if found_code
47+
end
48+
49+
if presenter
50+
embeds = { env: env }
51+
embeds[:version] = env['api.version'] if env['api.version']
52+
message = presenter.represent(message, embeds).serializable_hash
53+
end
54+
55+
message
56+
end
2957
end
3058
end
3159
end

lib/grape/error_formatter/json.rb

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ module ErrorFormatter
33
module Json
44
class << self
55
def call(message, backtrace, options = {}, env = nil)
6+
message = Grape::ErrorFormatter::Base.present(message, env)
7+
68
result = message.is_a?(Hash) ? message : { error: message }
79
if (options[:rescue_options] || {})[:backtrace] && backtrace && !backtrace.empty?
810
result = result.merge(backtrace: backtrace)

lib/grape/error_formatter/txt.rb

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ module ErrorFormatter
33
module Txt
44
class << self
55
def call(message, backtrace, options = {}, env = nil)
6+
message = Grape::ErrorFormatter::Base.present(message, env)
7+
68
result = message.is_a?(Hash) ? MultiJson.dump(message) : message
79
if (options[:rescue_options] || {})[:backtrace] && backtrace && !backtrace.empty?
810
result += "\r\n "

lib/grape/error_formatter/xml.rb

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ module ErrorFormatter
33
module Xml
44
class << self
55
def call(message, backtrace, options = {}, env = nil)
6+
message = Grape::ErrorFormatter::Base.present(message, env)
7+
68
result = message.is_a?(Hash) ? message : { message: message }
79
if (options[:rescue_options] || {})[:backtrace] && backtrace && !backtrace.empty?
810
result = result.merge(backtrace: backtrace)

spec/grape/api_spec.rb

+39
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'spec_helper'
22
require 'shared/versioning_examples'
3+
require 'grape-entity'
34

45
describe Grape::API do
56
subject { Class.new(Grape::API) }
@@ -1762,6 +1763,44 @@ def self.call(object, env)
17621763
end
17631764
end
17641765

1766+
context 'http_codes' do
1767+
let(:error_presenter) do
1768+
Class.new(Grape::Entity) do
1769+
expose :code
1770+
expose :static
1771+
1772+
def static
1773+
'some static text'
1774+
end
1775+
end
1776+
end
1777+
1778+
it 'is used as presenter' do
1779+
subject.desc 'some desc', http_codes: [
1780+
[408, 'Unauthorized', error_presenter]
1781+
]
1782+
1783+
subject.get '/exception' do
1784+
error!({ code: 408 }, 408)
1785+
end
1786+
1787+
get '/exception'
1788+
expect(last_response.status).to eql 408
1789+
expect(last_response.body).to eql({ code: 408, static: 'some static text' }.to_json)
1790+
end
1791+
1792+
it 'presented with' do
1793+
error = { code: 408, with: error_presenter }
1794+
subject.get '/exception' do
1795+
error! error, 408
1796+
end
1797+
1798+
get '/exception'
1799+
expect(last_response.status).to eql 408
1800+
expect(last_response.body).to eql({ code: 408, static: 'some static text' }.to_json)
1801+
end
1802+
end
1803+
17651804
context 'routes' do
17661805
describe 'empty api structure' do
17671806
it 'returns an empty array of routes' do

spec/grape/middleware/error_spec.rb

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
require 'spec_helper'
2+
require 'grape-entity'
23

34
describe Grape::Middleware::Error do
5+
module ErrorSpec
6+
class ErrorEntity < Grape::Entity
7+
expose :code
8+
expose :static
9+
10+
def static
11+
'static text'
12+
end
13+
end
14+
end
415
class ErrApp
516
class << self
617
attr_accessor :error
@@ -13,12 +24,16 @@ def call(env)
1324
end
1425

1526
def app
27+
opts = options
1628
Rack::Builder.app do
17-
use Grape::Middleware::Error, default_message: 'Aww, hamburgers.'
29+
use Spec::Support::EndpointFaker
30+
use Grape::Middleware::Error, opts
1831
run ErrApp
1932
end
2033
end
2134

35+
let(:options) { { default_message: 'Aww, hamburgers.' } }
36+
2237
it 'sets the status code appropriately' do
2338
ErrApp.error = { status: 410 }
2439
get '/'
@@ -42,4 +57,21 @@ def app
4257
get '/'
4358
expect(last_response.body).to eq('Aww, hamburgers.')
4459
end
60+
61+
context 'with http code' do
62+
let(:options) { { default_message: 'Aww, hamburgers.' } }
63+
it 'adds the status code if wanted' do
64+
ErrApp.error = { message: { code: 200 } }
65+
get '/'
66+
67+
expect(last_response.body).to eq({ code: 200 }.to_json)
68+
end
69+
70+
it 'presents an error message' do
71+
ErrApp.error = { message: { code: 200, with: ErrorSpec::ErrorEntity } }
72+
get '/'
73+
74+
expect(last_response.body).to eq({ code: 200, static: 'static text' }.to_json)
75+
end
76+
end
4577
end

0 commit comments

Comments
 (0)