Ruby Grape JSON-over-HTTP API, JSON Custom View

I have a small prototype subtype of Grape::API as a rack service, and I use Grape::Entity to represent the internal objects of my application.

I like DSL Grape::Entity , but it's hard for me to figure out how I should go beyond the default JSON view, which is too easy for our purposes. I was asked to output in jsend or similar format: http://labs.omniti.com/labs/jsend

I'm not at all sure that the nature of the changes is most consistent with the Grape structure (I would like to find the path of least resistance here). Do I have to create my own Grape formatter (I have no idea how to do this), the new rack middleware (I did this to register the API I / O via SysLog), but the formatting seems bad as I will need to parse the body back from JSON to add container level) or change from Grape::Entity to e.g. RABL?

Sample 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 run it:

 rackup -p 8090 

And call him:

 curl http://127.0.0.1:8090/api/v2/thing {"llama_name":"Henry"} 

What I would 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 it doesn’t seem very dry. I am looking for something cleaner and less open to cut and remove errors when this API gets bigger and is supported by the whole team.


Oddly enough, when I tried { :status => "success", :data => present( thing, :with => ThingPresenter ) } with grape 0.3.2 , I couldn't get it to work. The API returned only the value from present - more is going on here than I originally thought.

+8
ruby grape
source share
5 answers

This is what I ended up with a combination of reading Grape documentation, Googling and reading some pull requests on github. Basically, after declaring the format :json (to get all the other default pluses that come with it), I switch to the output formats with new ones that add a jsend wrapper layer. This turns out to be much cleaner than trying to wrap the Grape #present (which doesn't cover errors) or a rack middleware solution (which requires de-serializing and re-serializing JSON, plus it adds a lot of extra code for the coverage error).

 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 
+14
source

I believe this is achieved by having your goal 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 
+2
source

You can use a middleware layer for this. Grape has a Middleware::Base module that you can use for this purpose. My not very pretty 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 should add the line: use ::StatusAdder

+1
source

To date, I believe that the correct way to do this is with Grape:

  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 
+1
source

I am using @ Neil-Slater solution with one additional modification, I thought others might be useful.

With just rescue_from :all result for common 404 errors is returned as 403 Forbidden . In addition, the status is “error” when it should be “failure”. To solve these problems, I added a 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 could not find the correct way to access env, so you can see that I am passing it as a nil value (this is normal since the error handler does not use the value).

I suggest that you can extend this approach to further refine the processing of the response code. For me, the hard part was that I needed a Rack::Response object through which I could pass a formatted error message.

+1
source

All Articles