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

error messages can now be presented #705

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* [#703](https://github.com/intridea/grape/pull/703): Removed `Grape::Middleware::Auth::OAuth2` - [@dspaeth-faber](https://github.com/dspaeth-faber).
* [#719](https://github.com/intridea/grape/pull/719): Allow passing options hash to a custom validator - [@elado](https://github.com/elado).
* [#716](https://github.com/intridea/grape/pull/716): Calling `content-type` will now return the current content-type - [@dblock](https://github.com/dblock).
* [#705](https://github.com/intridea/grape/pull/705): Errors can now be presented - [@dspaeth-faber](https://github.com/dspaeth-faber).
* Your contribution here.

0.8.0 (7/10/2014)
Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ platforms :rbx do
gem 'rubinius-developer_tools'
gem 'racc'
end

gem 'pry'
gem 'pry-byebug'
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,22 @@ instead of a message.
error!({ error: "unexpected error", detail: "missing widget" }, 500)
```

When you'r error message is representable, a Hash with an option `with` or your
status is documented with an `Entity` it will be presented.

Examples:

```ruby


desc 'my route' , http_code: [[408, 'Unauthorized', API::Error]],

error!(Error.new(...), 400)
error!({ message: 'An Error', with: API::Error }, 401)
error!({ message: 'An Error' }, 408)

```

### Default Error HTTP Status Code

By default Grape returns a 500 status code from `error!`. You can change this with `default_error_status`.
Expand Down
2 changes: 1 addition & 1 deletion grape.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Gem::Specification.new do |s|
s.add_runtime_dependency 'virtus', '>= 1.0.0'
s.add_runtime_dependency 'builder'

s.add_development_dependency 'grape-entity', '>= 0.2.0'
s.add_development_dependency 'grape-entity', '>= 0.4.4'
s.add_development_dependency 'rake'
s.add_development_dependency 'maruku'
s.add_development_dependency 'yard'
Expand Down
38 changes: 22 additions & 16 deletions lib/grape/dsl/inside_route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,22 +173,7 @@ def present(*args)
else
[nil, args.first]
end
entity_class = options.delete(:with)

if entity_class.nil?
# entity class not explicitely defined, auto-detect from relation#klass or first object in the collection
object_class = if object.respond_to?(:klass)
object.klass
else
object.respond_to?(:first) ? object.first.class : object.class
end

object_class.ancestors.each do |potential|
entity_class ||= (settings[:representations] || {})[potential]
end

entity_class ||= object_class.const_get(:Entity) if object_class.const_defined?(:Entity) && object_class.const_get(:Entity).respond_to?(:represent)
end
entity_class = entity_class_for_obj(object, options)

root = options.delete(:root)

Expand Down Expand Up @@ -216,6 +201,27 @@ def present(*args)
def route
env["rack.routing_args"][:route_info]
end

def entity_class_for_obj(object, options)
entity_class = options.delete(:with)

if entity_class.nil?
# entity class not explicitely defined, auto-detect from relation#klass or first object in the collection
object_class = if object.respond_to?(:klass)
object.klass
else
object.respond_to?(:first) ? object.first.class : object.class
end

object_class.ancestors.each do |potential|
entity_class ||= (settings[:representations] || {})[potential]
end

entity_class ||= object_class.const_get(:Entity) if object_class.const_defined?(:Entity) && object_class.const_get(:Entity).respond_to?(:represent)
end

entity_class
end
end
end
end
28 changes: 28 additions & 0 deletions lib/grape/error_formatter/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,34 @@ def formatter_for(api_format, options = {})
end
end
end

module_function

def present(message, env)
present_options = {}
present_options[:with] = message.delete(:with) if message.is_a?(Hash)

presenter = env['api.endpoint'].entity_class_for_obj(message, present_options)

unless presenter || env['rack.routing_args'].nil?
# env['api.endpoint'].route does not work when the error occurs within a middleware
# the Endpoint does not have a valid env at this moment
http_codes = env['rack.routing_args'][:route_info].route_http_codes || []
found_code = http_codes.find do |http_code|
(http_code[0].to_i == env['api.endpoint'].status) && http_code[2].respond_to?(:represent)
end

presenter = found_code[2] if found_code
end

if presenter
embeds = { env: env }
embeds[:version] = env['api.version'] if env['api.version']
message = presenter.represent(message, embeds).serializable_hash
end

message
end
end
end
end
2 changes: 2 additions & 0 deletions lib/grape/error_formatter/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module ErrorFormatter
module Json
class << self
def call(message, backtrace, options = {}, env = nil)
message = Grape::ErrorFormatter::Base.present(message, env)

result = message.is_a?(Hash) ? message : { error: message }
if (options[:rescue_options] || {})[:backtrace] && backtrace && !backtrace.empty?
result = result.merge(backtrace: backtrace)
Expand Down
2 changes: 2 additions & 0 deletions lib/grape/error_formatter/txt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module ErrorFormatter
module Txt
class << self
def call(message, backtrace, options = {}, env = nil)
message = Grape::ErrorFormatter::Base.present(message, env)

result = message.is_a?(Hash) ? MultiJson.dump(message) : message
if (options[:rescue_options] || {})[:backtrace] && backtrace && !backtrace.empty?
result += "\r\n "
Expand Down
2 changes: 2 additions & 0 deletions lib/grape/error_formatter/xml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module ErrorFormatter
module Xml
class << self
def call(message, backtrace, options = {}, env = nil)
message = Grape::ErrorFormatter::Base.present(message, env)

result = message.is_a?(Hash) ? message : { message: message }
if (options[:rescue_options] || {})[:backtrace] && backtrace && !backtrace.empty?
result = result.merge(backtrace: backtrace)
Expand Down
30 changes: 30 additions & 0 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'spec_helper'
require 'shared/versioning_examples'
require 'grape-entity'

describe Grape::API do
subject { Class.new(Grape::API) }
Expand Down Expand Up @@ -1762,6 +1763,35 @@ def self.call(object, env)
end
end

describe 'http_codes' do

let(:error_presenter) do
Class.new(Grape::Entity) do
expose :code
expose :static

def static
'some static text'
end

end

end

it 'is used as presenter' do
subject.desc 'some desc', http_codes: [[401, 'Error'], [408, 'Unauthorized', error_presenter], [409, 'Error']]

subject.get '/exception' do
error!({ code: 408 }, 408)
end

get '/exception'
expect(last_response.status).to eql 408
expect(last_response.body).to eql({ code: 408, static: 'some static text' }.to_json)
end

end

context 'routes' do
describe 'empty api structure' do
it 'returns an empty array of routes' do
Expand Down
34 changes: 33 additions & 1 deletion spec/grape/middleware/error_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
require 'spec_helper'
require 'grape-entity'

describe Grape::Middleware::Error do
module ErrorSpec
class ErrorEntity < Grape::Entity
expose :code
expose :static

def static
'static text'
end
end
end
class ErrApp
class << self
attr_accessor :error
Expand All @@ -13,12 +24,16 @@ def call(env)
end

def app
opts = options
Rack::Builder.app do
use Grape::Middleware::Error, default_message: 'Aww, hamburgers.'
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, opts
run ErrApp
end
end

let(:options) { { default_message: 'Aww, hamburgers.' } }

it 'sets the status code appropriately' do
ErrApp.error = { status: 410 }
get '/'
Expand All @@ -42,4 +57,21 @@ def app
get '/'
expect(last_response.body).to eq('Aww, hamburgers.')
end

context 'with http code' do
let(:options) { { default_message: 'Aww, hamburgers.' } }
it 'adds the status code if wanted' do
ErrApp.error = { message: { code: 200 } }
get '/'

expect(last_response.body).to eq({ code: 200 }.to_json)
end

it 'presents an error message' do
ErrApp.error = { message: { code: 200, with: ErrorSpec::ErrorEntity } }
get '/'

expect(last_response.body).to eq({ code: 200, static: 'static text' }.to_json)
end
end
end
13 changes: 13 additions & 0 deletions spec/grape/middleware/exception_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def call(env)

it 'does not trap errors by default' do
@app ||= Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error
run ExceptionApp
end
Expand All @@ -63,6 +64,7 @@ def call(env)
context 'with rescue_all set to true' do
it 'sets the message appropriately' do
@app ||= Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true
run ExceptionApp
end
Expand All @@ -72,6 +74,7 @@ def call(env)

it 'defaults to a 500 status' do
@app ||= Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true
run ExceptionApp
end
Expand All @@ -81,6 +84,7 @@ def call(env)

it 'is possible to specify a different default status code' do
@app ||= Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true, default_status: 500
run ExceptionApp
end
Expand All @@ -90,6 +94,7 @@ def call(env)

it 'is possible to return errors in json format' do
@app ||= Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true, format: :json
run ExceptionApp
end
Expand All @@ -99,6 +104,7 @@ def call(env)

it 'is possible to return hash errors in json format' do
@app ||= Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true, format: :json
run ErrorHashApp
end
Expand All @@ -109,6 +115,7 @@ def call(env)

it 'is possible to return errors in jsonapi format' do
@app ||= Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true, format: :jsonapi
run ExceptionApp
end
Expand All @@ -118,6 +125,7 @@ def call(env)

it 'is possible to return hash errors in jsonapi format' do
@app ||= Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true, format: :jsonapi
run ErrorHashApp
end
Expand All @@ -128,6 +136,7 @@ def call(env)

it 'is possible to return errors in xml format' do
@app ||= Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true, format: :xml
run ExceptionApp
end
Expand All @@ -137,6 +146,7 @@ def call(env)

it 'is possible to return hash errors in xml format' do
@app ||= Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true, format: :xml
run ErrorHashApp
end
Expand All @@ -147,6 +157,7 @@ def call(env)

it 'is possible to specify a custom formatter' do
@app ||= Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: true,
format: :custom,
error_formatters: {
Expand All @@ -162,6 +173,7 @@ def call(env)

it 'does not trap regular error! codes' do
@app ||= Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error
run AccessDeniedApp
end
Expand All @@ -171,6 +183,7 @@ def call(env)

it 'responds to custom Grape exceptions appropriately' do
@app ||= Rack::Builder.app do
use Spec::Support::EndpointFaker
use Grape::Middleware::Error, rescue_all: false
run CustomErrorApp
end
Expand Down
23 changes: 23 additions & 0 deletions spec/support/endpoint_faker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module Spec
module Support
class EndpointFaker
class FakerAPI < Grape::API
get '/' do
end
end

def initialize(app, endpoint = FakerAPI.endpoints.first)
@app = app
@endpoint = endpoint
end

def call(env)
@endpoint.instance_exec do
@request = Grape::Request.new(env.dup)
end

@app.call(env.merge('api.endpoint' => @endpoint))
end
end
end
end