CatFace gets a face (Diving into Clojure, part five)

While at Open Source Bridge 2012, I’ve been working on a fun little project as part of my ongoing investigation into Clojure (part 1, parts 2 and 3, part 4). It’s a fake social networking app for professional cats: CatFace, where cats have a face. I’m not much for graphic design or CSS, but I’m hoping to get the code written and then maybe to recruit someone to style it up. Yesterday, I worked on the pure functional model for generating random cat profiles predictably based on a seed number. Today, I’m discussing how I wired it up with Noir.

You can see what I ended up with here: CatFace. It’s a single-dyno Heroku app, so it might be slow to come up – be patient!

Noir is a lightweight web framework for Clojure. It provides URL routing, HTML templating, and a little more. It’s analogous to Sinatra in Ruby-land. Other people have written about Noir in some detail, so this will probably be one of my briefer entries.

A Leiningen plugin called lein-noir can be used to simplify the process of creating a Noir app. I installed it using the instructions on the website, attempting to apply it to my existing project cat-face.

    % lein plugin install lein-noir 1.2.1
    % lein noir new cat-face

The first thing I found out was (a) yes, you can apply lein noir new to an existing app directory. Unfortunately I also found out (b) this is because it clobbers whatever files it likes. Most of these are likely to be unique to Noir, but it will clobber your .gitignore and project.clj files. In my case, neither of those had anything useful in them, but watch out.

It updates/clobbers your project.clj to declare a dependency on Noir and tell Leiningen to run your web app when lein run is invoked:

(defproject cat-face "0.1.0-SNAPSHOT"
            :description "FIXME: write this!"
            :dependencies [[org.clojure/clojure "1.3.0"]
                           [noir "1.2.1"]]
            :main cat-face.server)

Whatever namespace is indicated in the :main clause will be loaded and the -main function will be called. When you run your app, Leiningen will automatically download your dependencies and stash them in the lib/ directory.

lein noir new also creates a skeleton for running your server:

(ns cat-face.server
  (:require [noir.server :as server]))

(server/load-views "src/cat_face/views/")

(defn -main [& m]
  (let [mode (keyword (or (first m) :dev))
        port (Integer. (get (System/getenv) "PORT" "8080"))]
    (server/start port {:mode mode
                        :ns 'cat-face})))

This starts up the Noir server listening to whatever port is indicated by the PORT environment variable, or 8080 if none is provided. Noir seems to provide Rails-style deployment environments, but I’m not sure yet what they do (or if they do anything in particular). Views are loaded as well. In Noir, view files contain both HTML templates and URL routing (that is, “controller”) code. welcome.clj sets up a default route for you:

(ns cat-face.views.welcome
  (:require [cat-face.views.common :as common]
  (:use [noir.core :only [defpage]]
        [hiccup.core :only [html]]))

(defpage "/welcome" []
           [:p "Welcome to cat-face"]))

This loads the cat-face.views.common namespace, where the layout is defined, and uses it to render a response to /welcome. noir.content.getting-started is also included. This namespace defines a default route for the root of your site, so if you start up the server and load localhost:8080 you’ll get a little guide to get started (including how to disable the guide). I ended up removing this file entirely and lumping everything into cat-face.views.common, which was probably inappropriate but has the advantage of making this very small program simpler. There’s also no reason you can’t define views directly in your server.clj file – all of this is standard Clojure namespace loading, with no magical paths or automatic loading.

First, I define the pages I want available. The homepage of the site should (for now) be a random cat’s profile, so that people have an idea of what’s going on. I also want to be able to view a cat’s profile given an ID. That’s enough for now – I might want more pages in the future, though. So, in views/common.clj:

(defpage "/" {}
  (layout (cat-profile 

(defpage "/cats/:profile-id" {:keys [profile-id]}
  (layout (cat-profile 
    (cats/cat-profile (Integer. profile-id)))))

Both pages are identical, so they share a layout and a cat-profile partial. We’ve required cat-face.core as cats, so all of the functions defined there are available for us to use. The URL matching syntax is much like Rails or Sinatra. One nice feature is that you can use destructuring assignment to assign parameter values (passed as a Clojure map) directly to variables.

cat-profile and its constellation of partials and helpers do most of the heavy lifting here. It’s large, but mostly markup:

(defpartial cat-profile [cat]
      (cat-image cat :full)]
    [:h2 (cat-link cat (cat :name))]
    [:h3 "\"" (cat :destiny) "\""]
      [:strong "Current Employment:"]
      (cat :home)]
      [:strong "Color:"]
      (cat :color)]
      [:strong "Colleagues:"]
      [:ul (map cat-friend (cat :friends))]]
    [:p#padd " "]])

This should be fairly transparent to anyone familiar with HTML, and reminds me somewhat of Haml. Elements are declared as vectors where the first entry is a keyword (Clojure keywords, somewhat analogous to Ruby symbols, start with a colon and are scoped to a namespace). The second entry, if it is a map, provides the attributes of the element. The remaining entries, either sub-elements or plain text, provide the content of the element.

I use several partials here. Partials are simply functions; they take arguments and return a string. As far as I know, the only special treatment is that you can use the HTML-building vectors within them. cat-image, cat-link, and cat-friend are defined thus:

(def cat-image-sizes 
  { :thumb { :width "100" :height "100" }
    :full  { :width "250" :height "250" }})

(defpartial cat-image [cat size]
  (let [scale (cat-image-sizes size)]
    [:img (merge scale { :src (cat-image-url cat) })]))

(defpartial cat-link [cat & content]
  [:a {:href (cat-url cat)} content])

(defpartial cat-friend [friend]
    (cat-link friend 
      (cat-image friend :thumb)
      (friend :name))])

Most of this is non-notable stuff I’ve covered before, but this is the first time I’ve used an ampersand argument. Ampersand arguments work like Ruby’s splat arguments – extra arguments are passed in as a list. This allows me to embed whatever elements I like inside cat-link. Finally, the layout itself is defined as a partial:

(defpartial layout [& content]
               [:title "CatFace, where cats have a face"]
               (include-css "/css/reset.css")
               (include-css "/css/catface.css")]
                 [:h1 "CatFace"]
                 [:p#slogan "Where cats have a face"]

The way partials are used in Noir is pleasantly simple and surprisingly flexible. There’s no distinction between layouts, views, and partials – they’re all partials, and all a partial does is take arguments and return HTML. No special ‘render’ function is required to interact with them – they’re simply functions. This is very pleasant for a small app, but I’m not sure how well it would work in a large, complex one. Small Ruby web frameworks like Camping and Sinatra have historically used inline code-based templates like this, but most larger Sinatra apps I’ve seen use views split out into separate files (and in a DSL like Haml or a templating tool like ERB).

Once I had my app written, it was easy to deploy to Heroku – just like a Rails app. You have to use some Ruby tools, still (like the heroku command-line tool and foreman) but deployment is through Git, and Foreman can handle running Leiningen. Heroku has an excellent guide to this.

That’s all for today. Next I’ll be returning to the mass-downloader tool, attempting to apply more of Clojure’s unique concurrency constructs instead of Java’s. I’ll also be trying my hand at interacting with Clojure code from JRuby, and researching what existing packages in the Clojure ecosystem could have solved my mass-download problem.