intro
This post deals with how to deserialize Hiccup content from a Clojure Vector stored in MongoDB. I ran into this problem while rewriting my site. The Monger driver offers some great functions to make Mongo easy to use with Clojure but I needed to write some additional logic to get the behavior I wanted. Over the course of the post we will be dealing with three libraries:
- Monger The standard MongoDB library for Clojure.
- Hiccup The standard HTML templating library for Clojure.
- Clojure Zip Clojure's supplied tree traversal and modification library.
setup
We will use a simple leiningen project with 2 files to follow through this post. We are assuming Mongo is installed on the box. So first off create the project as below.
λ megadrive blog → lein new zippermongo
λ megadrive blog → cd zippermongo
λ megadrive zippermongo → tree
.
├── project.clj
└── src
└── zippermongo
└── core.clj
2 directories, 2 files
Then copy the details below into the project file. This will provide the dependencies for the example.
(defproject zippermongo "0.1.0-SNAPSHOT"
:description "Blog 1: Zipper Mongo"
:url "http://www.arachnid.co.nz"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.4.0"]
[hiccup "1.0.1"]
[com.novemberain/monger "1.4.2"]
[cheshire "5.0.1"]])
the problem
When rewriting my site I wanted to be able to add new blog postings without redeploying the app. To do this I thought I would store the Hiccup representing a post in Mongo along with its tags etc and have the site read its blog data straight from the DB. This turned out to be quite simple but I ran into a small hitch deserializing the Hiccup back out of Mongo. The easiest way to demonstrate this is with an example.
Paste this code into the core.clj file and we will run through it in a REPL session.
(ns zippermongo.core
(:require [monger.core :as mg]
[monger.collection :as col]
[clojure.zip :as zip])
(use clojure.pprint))
;; 1. This is the Hiccup we will store and return from Mongo.
(def example-hiccup
[:body
[:p "Hello World Paragraph"]
[:ul
[:li "Clojure"]
[:li "Mongo"]
[:li "Hiccup"]]])
;; 2. Connect to the local MongoDB Instance.
(mg/connect! {:host "localhost"
:port 27017})
(mg/set-db! (mg/get-db "zipper-mongo-blog"))
;; 3. Define a collection to store our hiccup in.
(def HICCUP_COLLECTION "hiccupcollection")
;; 4. Write our Hiccup to Mongo.
(defn write-data-to-mongo
[name hiccup]
(col/insert HICCUP_COLLECTION {:name name :hiccup hiccup}))
;; 5. Read the Hiccup back from Mongo
(defn read-data-from-mongo
[name]
(col/find-one-as-map HICCUP_COLLECTION {:name name}))
Using these functions in a REPL session we can see that the Hiccup that is read back from Mongo no longer has a keyword as the head of each vector. Instead it has been replaced by a plain string.
user=> (use 'zippermongo.core)
nil
user=> (ns zippermongo.core)
nil
zippermongo.core=> (pprint example-hiccup)
[:body
[:p "Hello World Paragraph"]
[:ul [:li "Clojure"] [:li "Mongo"] [:li "Hiccup"]]]
nil
zippermongo.core=> (write-data-to-mongo "post1" example-hiccup)
zippermongo.core=> (def post1-from-db (read-data-from-mongo "post1"))
#'zippermongo.core/post1-from-db
zippermongo.core=> (pprint (:hiccup post1-from-db))
["body"
["p" "Hello World Paragraph"]
["ul" ["li" "Clojure"] ["li" "Mongo"] ["li" "Hiccup"]]]
the solution - clojure zippers
To navigate the Hiccup tree and update the leading element of each vector to a keyword we are going to use Clojure Zippers. They allow us to navigate and edit the tree in a functional fashion.
Paste the 2 functions below into core.clj and reload the namespace.
;; 6. Use Clojure Zippers to walk the Hiccup content and keywordize the
;; first element of each vector.
(defn keyword-vector-head
"Changes the first element of the vector to a keyword."
[vec]
(apply vector
(cons (keyword (first vec))
(rest vec))))
(defn deserialize-hiccup-content
"Deserialize the stored hiccup content for use by Clojure."
[hiccup-content]
(loop [hiccup-tree (zip/vector-zip hiccup-content)]
;; If we are at the end of the tree then return its root.
(cond (zip/end? hiccup-tree) (zip/root hiccup-tree)
:else
;; If our current node is a vector
;; Then replace its head with a keyword.
;; Else continue traversal.
(let [next-loc (cond (vector? (zip/node hiccup-tree))
(zip/edit hiccup-tree keyword-vector-head)
:else hiccup-tree)]
(recur (zip/next next-loc))))))
Running the deserialization function in our REPL against the results from the Mongo we can see the Hiccup is now being returned in the same format as when it was inserted.
zippermongo.core=> (use 'zippermongo.core :reload-all)
zippermongo.core=> (pprint
(deserialize-hiccup-content
(:hiccup post1-from-db)))
[:body
[:p "Hello World Paragraph"]
[:ul [:li "Clojure"] [:li "Mongo"] [:li "Hiccup"]]]