Month: July 2016

Typesafe routes

July 28, 2016 Web frameworks, Yesod No comments

Greetings and salutations!

Todays’ post will be about – you guessed it – typesafe routes in Yesod. First, let’s talk a little about what does a “typesafe route” actually mean. Type safety is related to compile time (perhaps with exception for dependent types, but let’s not dig into it for now), so all guarantees you can get are compile-time guarantees. Compile time guarantees are related to the structure of data rather than the data itself. So, for example, if we used a typesafe route for the example from previous post (/project/#Int) it would prevent us from directing to /project/phew, but not from /project/423, regardless whether /project/423 did actually exist – it is not possible to decide in compile time, and it might exist, so the route is allowed.

And there is one more drawback – typesafe routes are type-safe only in scope of your site (and of course do not guarantee reachability, network connection etc.). It might be a lot or a little, depending on your use case – still, it’s more than other frameworks offer out-of-the-box. What’s important is to remember that – while they might simplify your life – they are no magical way to solve dead link problems. And of course 404s will still occur, so usual testing is still needed.

Enough talking, let’s write some code. Today we’re going to make a link-per-project on the /projects route. We already have the routes both for main projects page and for each project (no content though!), so today we’re just going to link them together.

First, let’s extract the projects to a separate file – in the end it’ll be a database, but for now a separate module is sufficient. Let’s create a new top-level directory, Database, a module Projects there and export projects constant from there:

module Database.Projects (projects) where
import Import

projects::[(Int, Text)]
projects = [(1, "Project 1"), (2, "Project 2"), 
  (4, "My extra project"), (5, "Project 10")]::[(Int, Text)]

then add an import to both Projects.hs and Project.hs:
import Database.Projects(projects)
.

In Projects.hs remove projects = ["Project 1", "Project 2", "My extra project", "Project 10"]::[Text] (it is loaded from Database.Projects module) and in projects.hamlet change projectName variable assignment to a tuple: (projectId, projectName) (compiler will complain that projectId is not used, but we’ll use it in a minute). You can check that everything still works fine.

Now let’s change Project.hs file. We’re going to display both title and id of the project. Database.Projects is already loaded, but there is a little trick: it might be that requested project does not exist. Ideally we should return a 404: NotFound in such case, but for now we can live with some default behavior (no worries – we’re gonna raise a 404 in the next post)

getProjectR projectId = defaultLayout $ do
  setTitle $ ("Project " ++ (createTitle projectId))
  $(widgetFile "project")
  where
    createTitle projId = (toHtml projId) ++ projectName
    projectName = toHtml $ maybe "" snd projectEntry
    projectEntry = find (\x -> (fst x) == projectId) projects

this might look a little complicated, but the toughest thing here is handling the default case. Let’s also add 

Name: #{projectName}

to project.hamlet, so that it’ll be easier to see if it works. Check that everything works fine – it should.

And now, time for the clue – routes. They’re quite simple – just change

<li .list-group-item>#{projectName}

to

<li .list-group-item>
  <a href="@{ProjectR projectId}">#{projectName}</a>

in projects.hamlet.

And that’s it. Congratulations, you’ve just created a set of type safe routes in Yesod! Next episode – about handling failure cases – comes still this week.

Stay tuned!

Routes with parameters

July 25, 2016 Web frameworks, Yesod No comments

Hi all,

sorry for the long break. It started with me getting the last DLC for The Witcher 3 – Blood & Wine (check it out if you didn’t play it yet), and then loads of stuff happened. Anyway, I’m back and I’m going to do my best to make up for the delay. Let’s start with parametrized routes, like /project/10, eat/200 etc. We’re going to add support for such routes in our project management app for each of the projects, indexed by some unique ID (artificially given).

Yesod handles routes in a relatively simple way – whole route is splitted by ‘/’ (forward slash) and a list is generated – for example, /project/2 will translate to ["project", "2"]. Don’t worry about trailing slashes or double-slashes, Yesod has you covered – routes are translated to a canonical form and users are redirected to them. In Yesod you specify a route parameter by it’s expected type name preceeded by # (if the value is between two forward slashes) or * (in case of more complex type).

Hence adding a new route is simple – just add

/project/#Int ProjectR GET

line to config/routes and we’re done. Well, not entirely – we still need to add proper handler, and that’s what we’re going to do now.
To do it properly, create a Project.hs file and add it to all required places (you can check which places are required in one of earlier posts). It’s content should be simple:

module Handler.Project where
import Import

import Yesod.Bootstrap()

getProjectR :: Int -> Handler Html
getProjectR projectId = defaultLayout $ dodo
  setTitle $ ("Project " ++ (toHtml projectId))
$(widgetFile "project")

This also requires a projects.hamlet file, equally simple:

<div>
  <h1 .page-header> Project: #{show projectId}

aaaand it’s working – check it out yourself! Here’s a little bonus – on a request for a resource that doesn’t match the type signature (for example, project/x) Yesod will automatically return 404: Not Found without even bothering you. Current 404 page isn’t nice, but it’s customizeable, obviously (we’re gonna deal with it in the future).

You might’ve wondered what will happen if you create two routes matching the same pattern, for example /project/#Int and /project/#Text (overlapping part is everything that is parseable by /project/#Int). Try it out yourself! Apprently, quite a nice exception is thrown:

Overlapping routes:
("ProjectR","ProjectR")

but what if you actually want routes to overlap, and simply choose “numeric handler when argument is numeric, and textual when it isn’t”? Good news is that you don’t need to push parsing logic into the handler – you simply need to change the route definition from /project/#Int and /project/#Text to disable overlap checking – but only for these routes. If you do that, be aware – first matching route wins, so be careful with the order!

Next episode – linking with typesafe routes – coming still this week. Stay tuned!

Working on the Hamlet template

July 9, 2016 Web frameworks, Yesod No comments

If you’re following the series, you definitely saw how ugly our page was at the end of the last post. Well, good news – we’re gonna fix it right now. Before you close the window, let me explain: I’m not going to start doing graphical design here, as I do not know enough about it. Instead, I’m going to show you how to use Yesod with one of the best design libraries in the world – Bootstrap. Yesod has kind of built-in support for Bootstrap – you might’ve guessed it by the import Yesod.Form.Bootstrap3() line in Projects.hs. It’s actually not exactly what we’re looking for, so let’s change it to import Yesod.Bootstrap() and add Yesod.Bootstrap to build dependencies in .cabal file. You might need to run stack solver --update-config && stack build before next run.
And that’s it – Bootstrap is already included in our files. Now let’s get rid of the ugly projects.cassius and let’s replace it with some nice styling!

But before this, you might be a little surprised that including Bootstrap didn’t require us to add any additional CSS files. That’s not actually true – we simply already have one. It’s located in static/css/bootstrap.css. If you are able to access the site after running stack exec -- yesod devel, you have this file – otherwise the site wouldn’t even compile.

Now you can delete all the classes and IDs we used in previous post and replace the .hamlet file with something like this:

<div>
  <h1 .page-header> Available projects:
  <div .jumbotron>
  <ul .list-group>
    <li .list-group-item> Project 1
    <li .list-group-item> Project 2
    <li .list-group-item> Super project

looks much better now, doesn’t it?

The hardcoded project names are quite a shame, we should get rid of them. Let’s make our Hamlet template an actual template that is filled with some data! For the first shot let it be a simple list of project names – typed [Text]. Wonder why not [String]? Short answer is: it’s more efficient. For a slightly longer one you can check out The Yesod Book. Anyway, our goal now is to have a list of Texts passed to the template and same output rendered. To do this, we need to add a little big of logic to our template. Lines starting with $ are treated as special instructions, virtually typical Haskell operations. #{someName} is used for variable interpolation. Check out the code for projects.hamlet:

<div>
  <h1 .page-header> Available projects:
  <div .jumbotron>
    $if null projects
      There are no ongoing projects
    $else
    <ul .list-group>
      $forall projectName <- projects
      <li .list-group-item> #{projectName}

Quite a change, isn’t it? But it’s quite simple – if there are no elements in projects container we show a message, otherwise – list all elements one-by-one.

But the code doesn’t compile again. To make it work you have to add projects variable to the scope of widgetFile invocation. This can be done for example by adding

    where
      projects = ["Project 1", "Project 2", "My extra project", "Project 10"]::[Text]

to getProjectsR function.

And that’s it! we can feed the template via the list in Projects.hs file. Nice, eh?

Actually, no. Right now it’s even worse than it was – previously it was sufficient to modify projects.hamlet and yesod runner automatically reloaded all the stuff. Now the compilation has to be done manually, by running stack build. Have no fear, we will change this in the future. but the next step is different. The next step will introduce Yesod’s killer feature – typesafe routes.

Stay tuned!