Fringe benefits of type-safe URLs
June 24, 2010
I speak a lot about how awesome type-safe URLs are. They give you a huge level of confidence in the validity of all your links, let you change your URL structure at will and let you pass type-safe parameters to your handler functions. That's all well and good, but let's talk about some of the minor benefits of these things. Consider this the first in a mini-series focusing on some of the lesser known features of Yesod.
Let me describe a project from hell. Imagine a site that has a navigational structure sometimes 8 levels deep. Imagine a client that can't write a spec, but knows exactly what he wants. And imagine that the "exactly what he wants" changes on a daily basis.
Breadcrumbs was a major annoyance on this project. Let's say we suddenly need to move one branch of the sitemap somewhere else. Using a normal, manual breadcrumbs approach, you have to change every single child page in the hierarchy to reflect things. Let's say the client decides you need to suddenly rename the root of the tree: you have to change every single page.
But if you think about it, breadcrumbs should really be very simple. For any page on your site, you need to know 1) the title of the page and 2) the parent page, if any. Turns out that type-safe URLs make this really easy to express as:
breadcrumb :: url -> (String, Maybe url)
If this is a top-level page, return Nothing in the second part of the tuple. Otherwise, return the parent. The first part in the tuple is the page title. I went ahead and packaged this into a typeclass, made the type signature a little more complex to run inside the Handler monad, and you have:
class YesodBreadcrumbs y where breadcrumb :: Routes y -> Handler y (String, Maybe (Routes y)) breadcrumbs :: YesodBreadcrumbs y => Handler y (String, [(Routes y, String)])
breadcrumbs function returns the title of the current page and the chain of parents along with their titles. You can easily turn that into some HTML using Hamlet:
1 (title, parents) <- breadcrumbs 2 return [$hamlet| 3 #breadcrumbs 4 $forall parents parent 5 %email@example.com@ $string.snd.parent$ 6 > $ 7 $string.title$ 8 |]
Just to point out some of the Hamlet syntax: the trailing dollar sign on line 6 is simply to demarcate the extra space at the end of the line; Hamlet ignores it. On line 5, the at signs denote a type-safe URL, whereas the dollar signs represent HTML content.
string is a function from BlazeHtml to convert a String into the Html datatype.
Static files host
As fast as Yesod is, it's not as fast as a static file server. It makes a lot of sense to server the static content from a different server (not to mention you avoid the cookie-transport cost). However, when developing the application, we want to be able to simply use the simple server and not set up a dedicated static file server.
That's where urlRenderOverride comes into play. Given a type-safe URL, it might return a replacement absolute URL. urlRenderOverride is part of the Yesod typeclass, where we keep a lot of optional settings. By default, the function always returns
Nothing, which allows normal URL rendering to take place. However, you can set up your production code to intercept all of your static file requests and send them to a different server.
For an example of how to do static file server, see the Ajax tutorial.
One of the more recent additions to the Yesod typeclass is the
isAuthorized function. Unfortunately, it was not done properly in 0.3.0, but I intend to fix that in 0.4.0. In any event, here's the idea: Given a type-safe URL, it returns Nothing if the request is authorized, or Just with a message if unauthorized. If you want the user to authenticate first, or provide some other information, you can use a redirect to send them to a different page.
The problem is that I wrote this to live in the IO monad instead of the Handler monad, so it doesn't have access to request information or the ability to send a redirect. Once this is fixed, it should provide a very light-weight access-control-list mechanism.
All three of these methods offer two serious benefits from a program safety standpoint:
- It puts all of the code for a specific concept into a single location.
- The compiler can check to make sure you've considered all possibilities.
I'm sure a lot of this could be implemented in a different way than type-safe URLs, but I think that we see here a natural result of a good design decision.