Compojure

Listing 1. handler.clj: Source Code for the Simple Appointment-Book System


(ns cjtest.handler
  (:use compojure.core hiccup.core clj-time.format clj-time.coerce)
  (:require [compojure.handler :as handler]
            [compojure.route :as route]
            [clojure.java.jdbc :as sql]))

(defn say-hello
  [req]
  (html [:p [:b "Hello, "  (get (get req :route-params) :name) ]]))


(def db {:classname "org.postgresql.Driver"
         :subprotocol "postgresql"
         :subname (str "//" "localhost" ":" 5432 "/" "cjtest")
         :user "reuven"
         :password ""})

(defn format-meeting [one-meeting]
  (html [:li (:meeting_at one-meeting)
         " : "
         (:meeting_with one-meeting)
         " (" (:notes one-meeting) ")" ]))

(defn new-meeting-form
  [ req ]
  (html [:form {:method "POST" :action "/create-meeting"}
         [:p "Meeting at (in 2013-06-28T11:08 format): " [:input 
         ↪{:type "text" :name "meeting_at"}]]
         [:p "Meeting with: " [:input {:type "text" 
         ↪:name "meeting_with"}]]
         [:p "Notes: " [:input {:type "text" :name "notes"}]]
         [:p [:input {:type "submit" :value "Add meeting"}]]]))

(defn list-meetings
  [req]
  (html
   [:h1 "Current meetings"]
   [:ul
    (sql/with-connection db
      (sql/with-query-results rs ["select * from appointments"]
        (doall
         (map format-meeting rs))))]))


(defn create-meeting
  [req]
  (sql/with-connection db
    (let [form-params (:form-params req)
          meeting-at-string (get form-params "meeting_at")
          meeting-at-parsed (clj-time.format/parse
(clj-time.format/formatters
                  :date-hour-minute)
                  meeting-at-string)
          meeting-at-timestamp (clj-time.coerce/to-timestamp
          ↪meeting-at-parsed)
          meeting-with (get form-params "meeting_with")
          notes (get form-params "notes")]
      (sql/insert-values :appointments
                         [:meeting_at :meeting_with :notes]
                         [meeting-at-timestamp meeting-with notes]))
    "Added!"))

(defroutes app-routes
  (GET "/" [] "Hello World")
  (GET "/meetings" [] list-meetings)
  (GET "/new-meeting" [] new-meeting-form)
  (POST "/create-meeting" [] create-meeting)
  (GET "/fancy/:name" [name] say-hello)
  (route/resources "/")
  (route/not-found "Not Found"))

(def app
  (handler/site app-routes))

Inserting Data

Let's say you also want to insert data into your appointment book. To do that, you need an HTML form that then submits itself to a URL on your site. Let's first create a simple form—as always, written as a function:


(defn new-meeting-form
  [ req ]
  (html [:form {:method "POST" :action "/create-meeting"}
         [:p "Meeting at (in 2013-06-28T11:08 format): " 
         ↪[:input {:type "text" :name "meeting_at"}]]
         [:p "Meeting with: " [:input {:type "text" 
          ↪:name "meeting_with"}]]
         [:p "Notes: " [:input {:type "text" :name "notes"}]]
         [:p [:input {:type "submit" :value "Add meeting"}]]]))

Notice how the Hiccup library again lets you define HTML tags easily. In this case, because it's a form, you need to tell the form to which URL it should be submitted. So in this example, that'll be the /create-meeting URL. Thus, you need to define both /new-meeting and /create-meeting in your defroutes macro call:


(defroutes app-routes
  (GET "/" [] "Hello World")
  (GET "/meetings" [] list-meetings)
  (GET "/new-meeting" [] new-meeting-form)
  (POST "/create-meeting" [] create-meeting)
  (GET "/fancy/:name" [name] say-hello)
  (route/resources "/")
  (route/not-found "Not Found"))

As you can see, the routes distinguish between GET and POST requests. Thus, a GET request to /create-meeting will not have any effect (that is, it will result in the "not found" message being displayed); a POST request is needed to make it work.

Everything comes together when you want to add a new meeting to your database. You get the parameters from the submitted form and then insert them into the database.

I'm still learning about Clojure and Compojure and continue to discover new libraries of functions that can make it easier to create HTML forms and work with databases. For example, I've recently discovered SQLKorma, a library that seems almost like Ruby's ActiveRecord, in that it provides a DSL that creates database queries.

The power of Clojure, like all Lisps, is partly based on the idea that you do everything in small steps and then combine those steps for the full power. Here, for example, is the function I wrote to add a new record (meeting) to the database:


(defn create-meeting
  [req]
  (sql/with-connection db
    (let [form-params (:form-params req)
          meeting-at-string (get form-params "meeting_at")
          meeting-at-parsed (clj-time.format/parse 
          ↪(clj-time.format/formatters
                   :date-hour-minute)
                   meeting-at-string)
          meeting-at-timestamp (clj-time.coerce/to-timestamp 
          ↪meeting-at-parsed)
          meeting-with (get form-params "meeting_with")
          notes (get form-params "notes")]
   (sql/insert-values :appointments
                      [:meeting_at :meeting_with :notes]
                      [meeting-at-timestamp meeting-with notes]))
    "Added!"))

The first and final parts of the function are similar in many ways to the database row insertion that you executed outside Compojure. You use sql/with-connection to connect to a database, and within that use sql/insert-values to insert a row into a specific table.

The interesting part of this function is, I believe, what happens in the middle. Using the "let" form, which performs local bindings of names to values, I can grab the values from the submitted HTML form elements, preparing them for entry into the database.

I further take advantage of the fact that Clojure's "let" allows you to bind names based on previously bound names. Thus, I can set meeting-at-string to the HTML form value, and then meeting-at-parsed to the value I get after converting the string to a parsed Clojure value, and then meeting-at-timestamp to turn it into a data type that both Clojure and PostgreSQL can handle easily.

Much of the heavy lifting here is being done by the clj-time package, which handles a wide variety of different date/time packages.

In the end, you're able to go to /new-meeting, enter appropriate data into the HTML form and save that data to the database. You then can go to /meetings and view the full list of meetings you have set.

Conclusion

I always have loved Lisp and often have wished I could find a way to use it practically in my day-to-day work. (Not that I dislike Ruby and Python, mind you, but the brainwashing I received in college was quite effective.) Playing with Clojure as a language, and Compojure to develop Web applications, has been a refreshing experience—one that I intend to continue trying and that I encourage you to attempt as well.

Resources

The home page for the Clojure language is at http://clojure.org and includes a great deal of documentation. Documentation for Compojure is at its home page, http://compojure.org, and Hiccup is at https://github.com/weavejester/hiccup.

The SQLKorma library, which I referenced here, is at http://www.sqlkorma.com.

The date and time routines are available at https://github.com/KirinDave/clj-time on GitHub, and they provide a great deal of useful functionality for anyone dealing with dates and times in Clojure.

I found a number of good examples of using SQL and JDBC from within Clojure at Wikibooks: https://en.wikibooks.org/wiki/Clojure_Programming/Examples/JDBC_Examples.

Two good books about Clojure are Programming Clojure by Stuart Halloway and Aaron Bedra (published by the Pragmatic Programmers) and Clojure Programming by Chas Emerick, Brian Carper and Christophe Grand (published by O'Reilly). I've read both during the past year or two, and I enjoyed each of them for different reasons, without a clear preference.

______________________

Reuven M. Lerner, Linux Journal Senior Columnist, a longtime Web developer, consultant and trainer, is completing his PhD in learning sciences at Northwestern University.

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Fully in keeping with the

sollen's picture

Fully in keeping with the tradition of the brand, the name of the Veneno originates from a legendary fighting bull. Veneno is the name of one of the Tractor Work Lights strongest and most aggressive fighting bulls ever.

Webinar
One Click, Universal Protection: Implementing Centralized Security Policies on Linux Systems

As Linux continues to play an ever increasing role in corporate data centers and institutions, ensuring the integrity and protection of these systems must be a priority. With 60% of the world's websites and an increasing share of organization's mission-critical workloads running on Linux, failing to stop malware and other advanced threats on Linux can increasingly impact an organization's reputation and bottom line.

Learn More

Sponsored by Bit9

Webinar
Linux Backup and Recovery Webinar

Most companies incorporate backup procedures for critical data, which can be restored quickly if a loss occurs. However, fewer companies are prepared for catastrophic system failures, in which they lose all data, the entire operating system, applications, settings, patches and more, reducing their system(s) to “bare metal.” After all, before data can be restored to a system, there must be a system to restore it to.

In this one hour webinar, learn how to enhance your existing backup strategies for better disaster recovery preparedness using Storix System Backup Administrator (SBAdmin), a highly flexible bare-metal recovery solution for UNIX and Linux systems.

Learn More

Sponsored by Storix