Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby Grape JSON-over-HTTP API, custom JSON representation

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.

like image 464
Neil Slater Avatar asked Mar 19 '13 11:03

Neil Slater


3 Answers

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
like image 158
Neil Slater Avatar answered Nov 19 '22 13:11

Neil Slater


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
like image 2
abc123 Avatar answered Nov 19 '22 13:11

abc123


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

like image 1
Kashyap Avatar answered Nov 19 '22 13:11

Kashyap