Reputation: 27207
I have a small prototype subclass of Grape::API
as a rack service, and am using Grape::Entity
to present my application's internal objects.
I like the Grape::Entity
DSL, but am having trouble finding out how I should go beyond the default JSON representation, which is too lightweight for our purposes. I have been asked to produce output in "jsend or similar" format: http://labs.omniti.com/labs/jsend
I am not at all sure what nature of change is most in keeping with the Grape framework (I'd like a path-of-least-resistance here). Should I create a custom Grape formatter (I have no idea how to do this), new rack middleware (I have done this in order to log API ins/outs via SysLog - but formatting seems bad as I'd need to parse the body back from JSON to add container level), or change away from Grape::Entity
to e.g. RABL?
Example code ("app.rb")
require "grape"
require "grape-entity"
class Thing
def initialize llama_name
@llama_name = llama_name
end
attr_reader :llama_name
end
class ThingPresenter < Grape::Entity
expose :llama_name
end
class MainService < Grape::API
prefix 'api'
version 'v2'
format :json
rescue_from :all
resource :thing do
get do
thing = Thing.new 'Henry'
present thing, :with => ThingPresenter
end
end
end
Rackup file ("config.ru")
require File.join(File.dirname(__FILE__), "app")
run MainService
I start it up:
rackup -p 8090
And call it:
curl http://127.0.0.1:8090/api/v2/thing
{"llama_name":"Henry"}
What I'd like to see:
curl http://127.0.0.1:8090/api/v2/thing
{"status":"success","data":{"llama_name":"Henry"}}
Obviously I could just do something like
resource :thing do
get do
thing = Thing.new 'Henry'
{ :status => "success", :data => present( thing, :with => ThingPresenter ) }
end
end
in every route - but that doesn't seem very DRY. I'm looking for something cleaner, and less open to cut&paste errors when this API becomes larger and maintained by the whole team
Weirdly, when I tried { :status => "success", :data => present( thing, :with => ThingPresenter ) }
using grape 0.3.2
, I could not get it to work. The API returned just the value from present
- there is more going on here than I initially thought.
Upvotes: 10
Views: 8366
Reputation: 18763
I believe this accomplishes what your goal is while using grape
require "grape"
require "grape-entity"
class Thing
def initialize llama_name
@llama_name = llama_name
end
attr_reader :llama_name
end
class ThingPresenter < Grape::Entity
expose :llama_name
end
class MainService < Grape::API
prefix 'api'
version 'v2'
format :json
rescue_from :all
resource :thing do
get do
thing = Thing.new 'Henry'
present :status, 'success'
present :data, thing, :with => ThingPresenter
end
end
end
Upvotes: 2
Reputation: 91
I'm using @Neil-Slater's solution with one additional modification I thought others may find useful.
With just a rescue_from :all
the result for common 404 errors are returned as 403 Forbidden
. Also, the status is 'error' when it should be 'fail'. To address these issues I added a rescue handler for RecordNotFound:
rescue_from ActiveRecord::RecordNotFound do |e|
Rails.logger.info e.message
error = JSendErrorFormatter.call({message: e.message}, e.backtrace, {}, nil)
Rack::Response.new(error, 404,
{ "Content-type" => "text/error" }).finish
end
note - I couldn't figure out the proper way to access the rack env so you can see I am passing it in as a nil value (which is okay since the error handler doesn't use the value).
I suppose you could further extend this approach to further refine response code handling. For me, the tricky part was figuring that I needed a Rack::Response
object that I could pass the formatted error message into.
Upvotes: 1
Reputation: 1043
As of today's date, I believe the correct way to do this with Grape is:
rescue_from Grape::Exceptions::ValidationErrors do |e|
response =
{
'status' => 'fail',
'data' => {
'status' => e.status,
'message' => e.message,
'errors' => e.errors
}
}
Rack::Response.new(response.to_json, e.status)
end
Upvotes: 1
Reputation: 27207
This is what I ended up with, through a combination of reading the Grape documentation, Googling and reading some of the pull requests on github. Basically, after declaring :json
format (to get all the other default goodies that come with it), I over-ride the output formatters with new ones that add jsend's wrapper layer. This turns out much cleaner to code than trying to wrap Grape's #present
helper (which doesn't cover errors well), or a rack middleware solution (which requires de-serialising and re-serialising JSON, plus takes lots of extra code to cover errors).
require "grape"
require "grape-entity"
require "json"
module JSendSuccessFormatter
def self.call object, env
{ :status => 'success', :data => object }.to_json
end
end
module JSendErrorFormatter
def self.call message, backtrace, options, env
# This uses convention that a error! with a Hash param is a jsend "fail", otherwise we present an "error"
if message.is_a?(Hash)
{ :status => 'fail', :data => message }.to_json
else
{ :status => 'error', :message => message }.to_json
end
end
end
class Thing
def initialize llama_name
@llama_name = llama_name
end
attr_reader :llama_name
end
class ThingPresenter < Grape::Entity
expose :llama_name
end
class MainService < Grape::API
prefix 'api'
version 'v2'
format :json
rescue_from :all
formatter :json, JSendSuccessFormatter
error_formatter :json, JSendErrorFormatter
resource :thing do
get do
thing = Thing.new 'Henry'
present thing, :with => ThingPresenter
end
end
resource :borked do
get do
error! "You broke it! Yes, you!", 403
end
end
end
Upvotes: 15
Reputation: 4796
You could use a middleware layer for that. Grape has a Middleware::Base
module that you can use for this purpose. My not so super beautiful implementation:
class StatusAdder < Grape::Middleware::Base
def initialize(app)
@app = app
end
def call(env)
status, headers, response = @app.call
response_hash = JSON.parse response.body.first
body = { :status => "success", :data => response_hash } if status == 200
response_string = body.to_json
headers['Content-Length'] = response_string.length.to_s
[status, headers, [response_string]]
end
end
And in the MainService
class, you'd add a line: use ::StatusAdder
Upvotes: 1