Implement A Dsl

I’ve been talking a lot about how great DSLs are and how to implement them. How about actually doing it?

Testing Ring handlers

Clojure’s Ring library is great for writing HTTP handlers. I wanted a way to easily test a handler once I’d written it. So I could write something like:

(script
    (request :get “/”)
    (return-code? 200)
    (body-contains? “some str”)

    (request :get “/illegal”)
    (return-code? 404))

(run-script app test-script)

Requirements

To implement such a DSL, there are some specific requirements. - Requests and tests (like return-code?) are types of actions - Actions compose sequentially - A response needs to be passed through a series of actions until the next request - Cookies might be returned in the response and need to returned in the next request

Fortunately, a lot of the work to handle these requirements is already done in the state monad. In that case, actions are just monadic values, which are functions like:

(fn [s]
   ...
   [v new-s])

And script is just m-seq. Additionally, script produces monadic values and these can be assigned to symbols allowing you to factor scripts out into pieces that are easily reusable.

Another requirement is that if a test fails in the middle of a script, the script should be aborted and a failure value should be returned. That is easily accomplished using the maybe monad. So for our DSL we need a monad that combines the state monad and the maybe monad. Using the state-t monad transfer lets us create that easily. (I’ll cover monad transformers in a future post. For now, you just need to know that script-m is a monad that combines the effects of the state monad and the maybe monad.)

(def script-m
    (state-t maybe-m))

That’s a good example of why I consider monads to be like design patterns for DSL’s. Much of the work of implementing a DSL is already done by monads. This particular monad is really useful and can be used for a parser combinator DSL, among others.

Implementation

With the script-m monad defined above the rest of the DSL is easily implemented. script is defined as:

(defn script [& actions]
    (with-monad script-m
    (m-seq actions)))

And a helper macro to make things a little cleaner:

(defmacro script-let [bindings body]
    `(domonad script-m
    ~bindings
    ~body))

Implementing request is the hardest part of the DSL.

(defn request [method url & [req-map]]
  (let [method (->> method
                 name
                 .toLowerCase
                 keyword)]
    (script-let
      [handler get-handler
       cookies (get-val :cookies)
       :let [request (assoc req-map
                            :request-method method
                            :uri url
                            :cookies cookies)
             result (handler request)]
       _ (set-val :response result)
       _ set-cookies]
      result)))

The outer let just makes sure the method is in a canonical form, e.g. :get. The real fun begins with the script-let. This expression creates the monadic value request is going to return. In other words, request takes the parameters for the request and returns a function that accepts a state, performs the request and returns the response with an updated state.

To get that reponse, the script-let expression first gets the handler and any cookies from previous responses from the state. Then it assembles a request map, request which it passes to the handler. The response is captured in result. Finally, the state is updated with the new response and the cookies from that response are added to the cookies held in the state. The response is also returned in case someone wants to make use of it later, but that is not usually the case.

With the ability to query the handler and get a response back, you need a way to test the response. These are almost trivial to write. Here are two examples.

(defn body-contains? [strn]
  (script-let
    [response (get-val :response)
     :when (.contains (:body response) strn)]
    true))

(defn response-code? [code]
  (script-let
    [response (get-val :response)
     :when (= code (:status response))]
    true))

Each of these just get the appropriate value from the response and performs a test on it. The magic part is that if the test fails, this aborts the entire script, no matter deeply nested this might be. That’s one of the advantages of using the maybe monad.

One final piece is the run function.

(defn run-script [handler script]
  (-> (script {:handler handler})
    first      ;; discard the final state
    last       ;; get the value returned from the last action
    boolean))  ;; convert it to a boolean

This calls the script with an initial state (the hash-map containing the handler) and then extracts the result.

Wrapping up

The code for this library, as well as some tests using it, are here on github.

Jim Duey 02 March 2012
blog comments powered by Disqus