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