En las últimas semanas, mi pequeña biblioteca clojure para grizzly ha ido creciendo y ya puede aceptar cualquier petición web y renderizar una respuesta.

Desde el punto de vista de cliente, el proceso es ahora mismo este:

Enrutamos una petición a una función que procesará la petición:

  (route! (url-pattern GET "greetings" :name)
             say-hello)
 

Con la anterior expresión estamos enrutando las peticiones "GET /greetings/PARAMETRO" a la función say-hello, que además recibirá el valor de PARAMETRO con el nombre :name.

Definamos ahora la función say-hello:

 (defn say-hello
   ([] (render (if (= (parameter :name) "Helena")
                        "Hola chica guapa!"
                        (str "Hola " (parameter :name)))
                    200
                    {:Content-Type "text/plain"})))
 

En esta función básicamente decimos que queremos renderizar un mensaje u otro en función del valor del parámetro :name de la URL.


Hasta aquí todo sencillo, pero lo interesante es que hay debajo de todo esto ¿qué hacen las llamadas a render y parameter? Bien, la respuesta es que toda la lógica de servicio de una petición se encuentra encerrado dentro de una mónada, la Mónada WebIO. Esta mónada se define como:

 ;;
 ;; WebIO X = WebIOUnfinished X |
 ;;           WebIOFinished X
 ;;           WebIOtFailed
 ;;
 
 (defstruct web-request-processing :environment ;; The Rack request
                                   :response ;; The response of the request
                                   :parameters) ;; the parameters hash
 

La petición, la respuesta y los parámetros se guardan en la mónada que puede tener tres estados pendiente, terminada o fallida que establecen el estado de la respuesta.

La función de binding para la mónada WebIO, define como se deben combinar funciones monádicas según el resultado de aplicar la petición contenida en la mónda:

 (defmethod >>= :WebIO [f m]
   "Instance of the >>= function for the WebIO monad. If the computation
    result of m is WebIO Unfinished, the next function f is applied to the
    content of the m. If the result of m is WebIO Failed, next function
    f is not applied and m is returned. If the result of m is WebIO
    Finished, f is not invoked"
   (if (or (web-io-failed? m) (web-io-finished? m))
     m
     (f (:content m))))
 

Para poder realizar el procesamiento de la petición sólo se necesitan dos cosas una función que transforme una petición web, a través de la interfaz Rack que usamos, en una mónada WebIO y algún método para transformar una función normal como la anterior usada como ejemplo (say-hello), que no sabe nada sobre mónada alguna en una función monádica capaz de ser procesada por el operador de binding (>>=).

De lo primero se encarga la función (wrap-request) que usa la función general monádica (return) para envolver una petición Rack, además de protegerla con una referencia para poder ser usada facilmente mediante el mecanismo de memoria transaccional software (STM) que implementa Clojure:

 (defn wrap-request
   "Wrapper around return that builds the new request monad initializing it
    with the values of the rack-request"
   {:monad :WebIO}
   ([rack-request rack-response parameters subtype]
      (return :WebIO subtype
            (struct web-request-processing
                    rack-request
                    (if (= (class rack-response) clojure.lang.Ref)
                      rack-response
                      (ref rack-response))
                    parameters)))
   ([rack-request]
     (wrap-request rack-request (create-rack-response) {} :Unfinished))
   ([rack-request rack-response]
     (wrap-request rack-request rack-response {} :Unfinished))
   ([rack-request rack-response parameters]
     (wrap-request rack-request rack-response parameters :Unfinished)))
 

El segúndo requisito, transformar una función normal en una función monádica, lo realiza la función (with-web-io) y se conoce normalmente como "lifting". En este caso además, define unas variables accesibles sólo por el hilo que ejecuta esa función, para la petición, respuesta y parámetros, de forma que no sea necesario para el programador que implementa la función con la lógica de procesamiento, tener que pasar explícitamente estos parámetros a funciones como (parameter) o (render):

 (defn with-web-io
   "Lifts a call to a function for a web-io-processing struct into the web-io monad"
   {:monad :WebIO}
   ([f web-io-processing]
      (try
       (let [result (binding [*request* (:environment web-io-processing)
                              *response* (:response web-io-processing)
                              *parameters* (:parameters web-io-processing)]
                      (apply f []))]
         (if (web-io-monad? result)
           result
           (wrap-request (:environment web-io-processing)
                         (:response web-io-processing)
                         (:parameters web-io-processing)
                         :Unfinished)))
       (catch Exception ex (raise (:environment web-io-processing)
                                  500
                                  (. ex getMessage))))))
 

Sobre estas funciones, el mecanismo que sirve una petición resulta trivial, el proceso cuenta con los siguientes pasos:

  • Recibir la petición Rack
  • Extraer la ruta y método de la petición y comprobar si se puede enrutar a alguna función de respuesta
  • Transformar la petición Rack en una mónada WebIO
  • Crear una colección con los filtros anteriores, la función de respuesta, y los filtros posteriores a los que se va a enrutar la petición
  • Hacer un lift de cada una de estas funciones a funciones monádicas a través de (with-web-io)
  • Ejecutar la secuencia de funciones monádicas con el operador de binding (>>=)
  • Devolver el resultado a través de Rack