Ramaze by Example – Part 11: Validation and Error Handling

November 25, 2008 at 22:52

Filed under: Computing — Pistos @ 22:52

This is part 11 of Ramaze by Example, a tutorial on web development. In Part 10: Cosmetics, I improved the look of our application.

Up to this point, we’ve been assuming users will always behave nicely and in predictable ways. As we all know, this is not a safe assumption in practice, so now we’ll add some validation and error handling to our application. I’ll introduce the changes I made somewhat in reverse order, beginning with error message display.

The Flash Helper

Ramaze::Helper::Flash is useful for displaying one-time messages to users. In your controller, you treat flash as a Hash in which you store Strings (usually). These strings can then be interpolated in views, but not just in handling the current request, but also the next one. To understand this tool and technique better, let’s examine how I make use of it in our todo list application. I’ve added a fail method to the controller:

def fail( message )
  flash[ :error ] = message
end

It simply takes an argument and assigns it to the :error key of the flash Hash. I call the fail method whenever I want to send an error message to the user. To actually display the message, we use the method flashbox in our layout:

<body>
  <h1>#{@title}</h1>
  #{flashbox}
  #{@content}
</body>

Checking the operand

Our check off and delete actions have been assuming that there always exists a task that has an id matching the id parameter given. Now we’re going to check whether this is true, and handle the situation gracefully if the condition false.

Since we’re going to perform this check in more than one spot, we stay DRY by making a single method to do the check, a method we will call from multiple places.

def ensure_task_exists( id )
  task = Task[ id ]
  if task.nil?
    fail "No task with id #{id}."
    redirect_referrer
  end
  task
end

If there is no task in the database with the given id, then we call our fail method to setup an error message, and then redirect the user back to wherever she came from with redirect_referrer. If instead the task exists, we simply return the task to the method caller.

So next, we change the action methods to make use of this utility method. Here’s a diff of the relevant part of the controller:

   def check_off( id )
-    Task[ id ].check_off
+    task = ensure_task_exists( id )
+    task.check_off
     redirect Rs( :/ )
   end
 
   def delete( id )
-    Task[ id ].delete
+    task = ensure_task_exists( id )
+    task.delete
     redirect Rs( :/ )
   end

Observe how the error handling and redirection are nicely parcelled away elsewhere, keeping the action code clean and succinct.

Seeing errors in action

Now, run the code:

git checkout 11-error-handling
ruby start.rb

and visit /delete/99999 to see this error handling code work. The important thing to notice here is that even though the error message is set in one action (delete), it is available for usage in the next requested action (index). However, it will not be available to future requests (unless it is set again). That’s why it’s called the “flash” hash: because data only remains for a short while.

Constraining the input

We would like to ensure that task descriptions are not empty. While other developers may choose to constrain data at the model or view (Javascript) level, I’m of the persuasion that one should constrain data as much as possible at the database level, to maximize one’s confidence that the data in the database is as pristine as possible. So I will setup the constraint in the schema:

ALTER TABLE tasks
  ADD CONSTRAINT minimum_description_length
  CHECK ( CHAR_LENGTH( description ) > 1 );

After adding this constraint, if you try to enter a description which is less than 2 characters long, it will fail — but not very gracefully. So we add some grace by rewriting our create method:

def create
  description = h( request[ 'description' ] ).strip
  begin
    Task.create( :description => description )
  rescue DBI::ProgrammingError => e
    if e.message =~ /minimum_description_length/
      fail 'Please enter an adequate description for the new task.'
      redirect Rs( :new )
    else
      raise e
    end
  end
  redirect Rs( :/ )
end

As you can see, we are wrapping the Task.create call with a begin/rescue pair so we can handle the exception thrown by the database. The if block inside the rescue clause is used to handle a specific exception, namely a violation of the minimum_description_length constraint which we named and defined earlier. If an exception of this kind is thrown, then we setup an error message with fail and redirect back to the task addition page. If it’s some other kind of exception, we don’t want to swallow it up; instead we re-raise it. If no exception is raised, then the user is redirected to the task list.

Try this out to see for yourself that it works. (Don’t forget to add the constraint first if you haven’t done so already.)

Custom error handler

Up to this point, we’ve worked with errors which we anticipate and handle. In reality, we can never foresee every possible error. If you check out the previous tutorial part’s code and run it (git checkout 10-cosmetics; ruby start.rb), then try to visit a non-existent page, such as http://localhost:9001/foobar, you will see an error message and stack trace, something like this:

No Action found for `/foobar' on MainController
  /usr/lib/ruby/site_ruby/1.8/ramaze/lib/ramaze/controller/resolve.rb:274:in `raise_no_action'
  /usr/lib/ruby/site_ruby/1.8/ramaze/lib/ramaze/controller/resolve.rb:98:in `default'
  /usr/lib/ruby/site_ruby/1.8/ramaze/lib/ramaze/controller/resolve.rb:25:in `send'
...

Ramaze lets you override this error page with one of your own; simply put something in place to handle an error action. As we’ve learned, that means either a controller method, or a view, or both. Here’s the controller method we add:

def error
  @title = 'Application Error'
  @e = Ramaze::Dispatcher::Error.current
end

Whenever an exception occurs in a Ramaze application, the error action is hit. If no handler is set up by the application programmer, Ramaze will use a default error method.

Ramaze::Dispatcher::Error.current evaluates to the Ruby Exception which has been raised and is causing the error condition. We make it available to our view so we can display information about the exception. Here is our error.xhtml view to go with the error method:

<h2>#{@e.message}</h2>
<ul>
  <?r @e.backtrace.each do |frame| ?>
    <li>#{frame}</li>
  <?r end ?>
</ul>

It should be relatively clear what the above combination of method and view does, but in case it isn’t for you, git checkout 11-error-handling; ruby start.rb and visit http://localhost:9001/foobar again.

Review

Let’s review what we’ve learned over the final parts of this tutorial:

  • Layouts are used to keep view code dry and maintain a consistent look across multiple views.
  • A layout is just another view, one which interpolates #{@content}.
  • @content contains the rendering of a view (or, in other words, a layout “wraps” other views).
  • The layout method is used in a controller definition to tell Ramaze which view to use as the layout view for a controller. It accepts a Hash argument, with the key-value pair being the site path of the layout view, and an array of controller methods (actions) to apply the layout to. e.g.<br/>layout '/page' => [ :index, :new ]
  • Layouts can make use of controller instance variables (e.g. @title) just like normal views.
  • Static files (CSS, Javascript, images, etc.) are placed in a public/ subdirectory in your application’s file tree.
  • flash is a session-wide Hash.
  • Data stored in the flash Hash persists only for two requests, making it useful for temporarily storing messages for the user across redirects.
  • flashbox is used in views, and evaluates to an HTML <div> for each key-value pair in the flash Hash.
  • redirect_referrer is used to redirect a browser back to the referring page.

In the Conclusion, I will wrap up the tutorial and talk about further avenues of exploration and learning.

Share

Related posts:

  1. Ramaze by Example – Part 9: Layout
  2. Ramaze by Example – Part 6: Adding Tasks
  3. Ramaze by Example – Part 8: Deleting Tasks
  4. Ramaze by Example – Part 7: Checking off Tasks
  5. Ramaze by Example – Part 4: View

1 Comment »

RSS feed for comments on this post. TrackBack URI

Leave a comment

You may use Markdown syntax in your comment.

Powered by WordPress.