A url shortener service in 35 lines of Clojure
Who doesn't love a petty LOC competition?
I saw this post by Grasswire today, and I couldn’t resist the urge to throw down a Fizz-Buzz-pocalypse. So, here’s my implementation in Clojure, along with a list of ways that Clojure clearly wins. (That last sentence was to be read tongue-in-cheek, Scala is a good language too. Besties!).
Dependencies
Both projects depend on libraries for Redis (Scredis for Scala, Carmine for Clojure), and the apache commons-validator. The scala version additionally requires Spray and Akka, pretty heavyweight libraries. My clojure version just needs http-kit, a nice light ring-compatible asynchronous webserver.
Here are the lein
deps for the Clojure project:
[org.clojure/clojure "1.6.0"]
[http-kit "2.1.16"]
[com.taoensso/carmine "2.6.2"]
[commons-validator/commons-validator "1.4.0"]
Code
Take a read of the Scala version for comparison, then scope this sweet action:
(ns urlshortener.core
(:require
[org.httpkit.server :refer [run-server]] ; Web server
[taoensso.carmine :as redis]) ; Redis client
(:import
clojure.lang.Murmur3 ; Look what I found!
org.apache.commons.validator.routines.UrlValidator))
(def validator (UrlValidator. (into-array ["http" "https"])))
(def hash-url (comp (partial format "%x")
#(Murmur3/hashUnencodedChars %)))
(defn create-short-url [path]
(let [rand-str (hash-url path)]
(redis/wcar nil
(redis/set (str "/" rand-str) path))
(str "http://mydomain.com/" rand-str)))
(defn handle-create [{path :uri :as request}]
(if (.isValid validator (apply str (rest path))) ; Drop '/'
{:status 200 :body (create-short-url path)}
{:status 401 :body "Invalid Url provided"}))
(defn handle-redirect [{path :uri :as request}]
(let [url (redis/wcar nil (redis/get path))]
(if url
{:status 302 :body "" :headers {"Location" url}}
{:status 404 :body "Unknown destination."})))
(defn handler [{method :request-method :as req}]
(case method
:get (handle-redirect req)
:post (handle-create req)))
(run-server handler {:port 8080})
That’s 35 lines and 1151 characters vs. 44 and 1483 for Scala. But
more importantly, I think this version is a bit easier to understand;
the only complex thing (in my opinion) is the use of redis/wcar
to
wrap redis calls with a default (nil
) pooling configuration.
My favorite part of this compared to the Scala version is the straightforwardness of
handle-redirect
and handle-create
. Since http-kit
expects to be given a handler function,
it can automatically take care of making things async without having to do it manually.
I just hope UrlValidator.isValid
is thread-safe.
Bonus: I don’t know enough about Scala/Spray to say whether the same is true, but the clojure version is really easy to test. No, really:
(comment
; Everyone loves comment tests
(use 'clojure.test)
(import 'java.net.URL)
; Check redis works
(redis/wcar nil
(redis/set "/asdf" "http://google.com"))
(is (= (redis/wcar nil
(redis/get "/asdf"))
"http://google.com"))
(let [resp (handle-redirect {:uri "/asdf"})]
(is (= "http://google.com" (-> resp :headers (get "Location")))))
; 404 works
(is (= 404
(:status (handle-redirect {:path "/unknown"}))))
; Gen and retrieve
(let [orig-url "http://google.com?asdf"
new-url (create-short-url orig-url)
path (.getPath (URL. new-url))]
; Saved to redis
(is (= (redis/wcar nil (redis/get path))
orig-url))
; Returns correct value
(is (= (:status (handle-redirect {:uri path}))
302))
(is (= (-> (handle-redirect {:uri path})
:headers
(get "Location"))
orig-url))
; Hashes sensibly
(is (= (:body (handle-create {:uri orig-url}))
new-url))
))
Ok, I’m done. If someone is inclined to benchmark these two, that might be pretty interesting as well.