Supporting multiple JSON content types in Grape - ruby

I’ve been using Grape to write an Evernote-like API, and have been using Collection+JSON (MIME type "application/vnd.collection+json"). I now want to also support JSON API (mime type "application/vnd.api+json").
(The reason I want to do this is so it will be easier to write an Ember client, since Ember Data has support for JSON API built in. It might make more sense to write client-side JavaScript to solve my problem, but I’m stronger in Ruby than JavaScript.)
My goal is to have the server support either format, and choose which one based on the Accept header. Is this a reasonable thing to do, or does it not make sense to have multiple JSON types? It appears that Grape doesn’t support this. If I just have the wrong idea, then the code below is probably not relevant to that answer.
Here are some relevant pieces of my server code:
class NotesServer < Grape::API
content_type :json, ‘application/json’
content_type :json_api, 'application/vnd.api+json'
formatter :json_api, lambda { |object, env| object.to_json_api }
content_type :collection_json, 'application/vnd.collection+json'
formatter :collection_json, lambda { |object, env| object.to_collection_json }
resource :notes do
desc 'Get a note.'
params do
requires :id, type: Integer, desc: 'Note ID.'
end
route_param :id do
get do
NoteRepresenter.new Note.find(params[:id])
end
end
end
…where NoteRepresenter defines both to_json_api and to_collection_json.
The idea is to use the same Representer in either case, but to call the appropriate method depending on the Accept header.
When I run curl against this with any Accept header, the response has Content-Type: application/json, and “to_json” is called on the representer. If I leave out the content_type :json, ‘application/json’ line, I get a 406 Not Acceptable response every time.
I figured out that in the Grape library, in lib/grape/middleware/formatter.rb, the "format" of a request is considered JSON for any of these MIME types, since the vnd.collection+ or vnd.api+ parts are parsed out of the header. So my question is, is this a matter of Grape not supporting what I’m trying to do, or am I trying to do something that makes no sense?

I found a reasonable way to do this that I’mr reasonably happy with, but I don’t think it totally aligns with how Grape expects you to do things.
I created a custom formatter called JsonFormatter, so my server code looks like this:
class NotesServer < Grape::API
formatter :json, JsonFormatter
# …
end
And the formatter itself:
module JsonFormatter
def self.call(object, env)
case env['HTTP_ACCEPT']
when 'application/vnd.api+json'
object.to_json_api
when 'application/vnd.collection+json'
object.to_collection_json
else
object.to_json
end
end
end
So now either to_json_api or to_collection_api (or to_json) will be called on the presenter object, depending on the Accept header. One issue I haven’t solved yet is that the Content-Type is set to application/json in all cases.

Related

Accept file upload (without a form) in Sinatra

I have this Sinatra::Base code:
class Crush < Sinatra::Base
post '/upload' do
erb params.inspect
end
end
I am using Postman and its interface for uploading a file. So I send a POST request with form-data, where in the body of the request the name is hello and the value is a file test.txt which contains just a simple string hey there.
When I do params.inspect I get this long string
{"------WebKitFormBoundaryocOEEr26iZGSe75n\r\nContent-Disposition: form-data; name"=>"\"hello\"; filename=\"test.txt\"\r\nContent-Type: text/plain\r\n\r\nhey there\r\n------WebKitFormBoundaryocOEEr26iZGSe75n--\r\n"}
So basically a long has with a single key and a single value. Reading most Sinatra tutorials (where the file is accepted from a form), there's a nice way Sinatra handles this using params[:file], but this doesn't seem to be the case when the file is coming straight from the body of an HTTP request.
I tried a non-modular approach too withou Sinatra::Base, thinking it's some parsing middle-ware missing, but got the same result.
Is there something I'm missing here? Must I go and make my own custom parser to get the content of this long hash? Or is there an easier way?
I figured it's Postman issue. When I switch from 'x-www-form-urlencoded' to 'form-data' in Postman, in the Header section, the field: Content-Type => application/x-www-form-urlencoded is NOT removed. So for those who encounter this problem, make sure you remove it manually.

To 406 or not to 406 (http status code)

I'm developing a RESTful web application in Ruby with Sinatra. It should support CRUD operations, and to respond to Read requests I have the following function that formats the data according to what the request specified:
def handleResponse(data, haml_path, haml_locals)
case true
when request.accept.include?("application/json") #JSON requested
return data.to_json
when request.accept.include?("text/html") #HTML requested
return haml(haml_path.to_sym, :locals => haml_locals, :layout => !request.xhr?)
else # Unknown/unsupported type requested
return 406 # Not acceptable
end
end
Only I don't know what is best to do in the else statement. The main problem is that browsers and jQuery AJAX will accept */*, so technically a 406 error is not really the best idea. But: what do I send? I could do data.to_s which is meaningless. I could send what HAML returns, but they didn't ask for text/html and I would rather notify them of that somehow.
Secondly, supposing the 406 code is the right way to go, how do I format the response to be valid according to the W3 spec?
Unless it was a HEAD request, the response SHOULD include an entity containing a list of available entity characteristics and location(s) from which the user or user agent can choose the one most appropriate. The entity format is specified by the media type given in the Content-Type header field. Depending upon the format and the capabilities of the user agent, selection of the most appropriate choice MAY be performed automatically. However, this specification does not define any standard for such automatic selection.
It looks like you're trying to do a clearing-house method for all the data types you could return, but that can be confusing for the user of the API. Instead, they should know that a particular URL will always return the same data type.
For my in-house REST APIs, I create certain URLs that return HTML for documentation, and others that return JSON for data. If the user crosses the streams, they'll do it during their development phase and they'll get some data they didn't expect and will fix it.
If I had to use something like you're writing, and they can't handle 'application/json' and can't handle 'text/html', I'd return 'text/plain' and send data.to_s and let them sort out the mess. JSON and HTML are pretty well established standards now.
Here's the doc for Setting Sinatra response headers.

Serving XHTML with Sinatra

I'm creating a Sinatra web application which I would like to serve mime type application/xhtml+xml for all web pages served. I am using Builder as template engine. Note that the application will also be serving application/xml for various AJAX api interfaces, also using the Builder template engine, as well as text/css using the scss template engine.
I notice that the webpages that get generated receive a text/html content type while the api interfaces get an application/xml content type instead. However, I don't know how the mime type is decided; presence of the html tag? usage of a layout or not? Whichever the case, I would like the application to issue a application/xhtml+xml mimetype instead of the text/html mime type.
I know I can specify the mime type with the content_type call, but I'd like to refrain from needing this in every routing function. Is there any way I can set a default mime type for a given template engine? Or can the mime type be controlled by the view's filename? Does Tilt provide any means to control mime type in a nice way?
Since your application is factored into separate objects, it should be straightforward to pull all of the AJAX routes into a separate Sinatra application. This will allow you to use Sinatra's after hook. The only requirement is for your AJAX calls to have something that identifies them as AJAX; for example, by setting a request header to a special value, or by using a specially formatted route. Here is an example rackup file of the unique route method:
require 'sinatra/base'
class MainApp
def get_info params
# return some data structure
end
end
class MainAppRoutes < Sinatra::Base
def initialize mainapp
super()
#mainapp = mainapp
end
get '/' do # main page
data = #mainapp.get_info(params)
# render response from data
end
end
class AjaxRoutes < Sinatra::Base
def initialize mainapp
super()
#mainapp = mainapp
end
get '/getinfo' do # handler for /ajax/getinfo
data = #mainapp.get_info(params)
# generate XML response from data
end
after do
content_type 'application/xml'
end
end
mainapp = MainApp.new
map '/ajax' do
run AjaxRoutes.new(mainapp)
end
run MainAppRoutes.new(mainapp)
In this example, all routes starting with /ajax are handled by instances of the AjaxRoutes class. The after hook ensures that the content type for those responses are 'application/xml'. The non-ajax routes are not affected.
A few things to remember:
Rack::Builder.map strips off the parts of the URL it matches. So a '/' route in AjaxRoutes would actually answer a request for '/ajax/'.
Always call super() in the initializers of your Sinatra-derived endpoint classes. For Sinatra middleware, pass the first argument up (Rack calls #new with the next rack app as the first parameter).
Review the doc about Sinatra filters. There is a potentially frustrating quirk about modifying responses with the after hook.

is there a way to modify or send custom headers on grape?

I'm using Goliath and Grape. On my goliath server it calls the grape api like so:
when '/posts' then FrameworksAPI::API.call(env)
On my grape api class, my method is as simple as this:
get '/:id' do
Post.find(params[:id])
end
I'd like to modify the headers - specifically the 'Content-Length' but unsure how to.
Also i'd like to ask an additional question. How do i create callback/filters specifically before the method GET returns the result i'd like to modify the result.
Grape has a header helper for a few versions now.
header 'Content-Length`, 42.to_s
For your second question on modifying the body, try using after do ... at API level.
The return from your FrameworksAPI::API.call(env) method will be a triplet [status_code, headers, body]. So, instead of just returning that from your case you'd do something like:
when '/posts' then
status, headers, body = FrameworksAPI::API.call(env)
headers['whatever'] = blah
[status, headers, body]
You can also change the body, just be careful as the body maybe an array.
There is also a Content-Length middleware that is provided by Goliath. Content-Length is loaded by default although if you set a custom Content-Length it will take precedence. Just be carefull that other middlewares like the formattings don't change the body after you set your content-length.

Passing Sinatra models and validation

As mentioned on another similar thread I started recently, I am porting an ASP MVC application to Sinatra, partly for learning, partly for production purposes.
Currently my ASP MVC application has no views, it just exposes models as Xml/Json etc and accepts them in the same way. So in the scenario of someone sending a model to the server, it would auto bind the object, perform validation then return validation errors if there are any, if not carry on with the relevant actions.
Now in Sinatra it is a bit more barebones that ASP MVC which is a great thing as I have more choice on how to do these parts, however I dont have a clue what gems/libraries functionality is available for doing this.
My ideal scenario would be I have my pure html/js front end posting a model as json and then have it turned into an actual model where I can validate it (through Sinatra or other framework) then make a decision based on the validation results.
Try taking a look at this gem:
json
Then what you should be able to do is pass in the attributes for an object you would like to create as a hash of attributes converted to a json object:
{:attribute1 => "value1", :attribute2 => "value2"}.to_json
And on your Sinatra app, catch them and build the object from those attributes, and you can use Sinatra to validate as well:
def '/create_object' do
content_type :json
obj = Object.new(JSON.parse(params[:object]))
if obj.save
obj.to_json
else
error 400, user.errors.to_json
end
end
Sounds like an ordinary case for an ORM. So you could for example use DataMapper. With that in your hands, you can simply send a form via HTTP to a specific route and do something like:
post '/create' do
#post = Post.new(params[:your_form])
if #post.save
erb :a_template
else
erb :your_form
end
end
Where params[:your_form] is a hash containing the contents of your form. No JSON needed at all. If the saving process fails (for validation reason or whatever), the model object #post will have errors, which you could process in a different view (:your_form for example).
Best Regards
Tobias

Resources