August 22, 2014

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.