Search code examples
rubysinatrapadrino

How to dry up (failing) entity fetching in sinatra/padrino app/controller?


In building Sinatra or Padrino apps, I often write code like

get '/resource/:id' do
  resource = Resource.find(params[:id])
  return status 404 if resource.nil?
  # ..
end

Or actually, I like to

flash[:warning] = "A Resource with id #{params[:id]} coud not be found".
redirect back

I think in Rails this is modeled via "Ressources". My controllers tend to be mixed, part of the routes will depend on a resource id (which will be fetched from whatever db), other do not.

Which patterns can be used to dry this up? I know of before handlers (pseudo code, but have not seen a really smart implementation - it is sure out there somewhere!)

 before "*" do
   @resource = Resource.get(params[:id])
   redirect_with_flash if @resource.nil?
 end

or to put similar code in a method to call first in each route with that requirement.

Still, I see similar pieces of code in nearly every Sinatra tutorial, isn't there a better option? I am especially interested in a padrino-approach to that, if I overlooked it.

Here is how the code I would like to have could look like

MyPadrinoApp::App.controllers :user do
  associated_resource = User
  associated_resource_error_flashs = { "404": "A User with %s could not be found" }

  get :show, :with => :id, :resource_bound => :user do
    render '/user/show' # in which @user is available
  end
end

Solution

  • If you want to stop processing a request as soon as you know the request is invalid / an error occurred you can use Sinatras halt. It stops further processing immediately and allows you to define a http status code and a message to display, if your App is not about a REST API you can define the corresponding error template.

    In your example the request becomes invalid because the requested resource doesn't exists. To answer with a 404 is correct and you can tell halt to use this status code in the response.

    A very simple implementation can look like this:

    get '/resource/:id' do
      resource = Resource.find(params[:id])
      halt 404, "A Resource with id #{params[:id]} could not be found" if resource.nil?
      # ..
    end
    

    A more elegant way is loading the resource using a helper method which cares about the error handling and you are good to use the same call in all your routes.

    helpers do
      def load_resource
        Resource.find(params[:id]) || halt(404, "A Resource with id #{params[:id]} could not be found")
      end
    end
    
    get '/resource/:id' do
      # load the resource via helper method
      resource = load_resource
    
      # if the resource doesn't exists, the request and processing is already dropped
      ..
    end
    

    There are even more output options for halt, as mentioned you can return a erb template, you can also return JSON instead of plain text and so on. Check the docs here.