Learning Ring Next Steps

Learning Ring Next Steps

Previously, I wrote about Clojure and Ring in an introductory post where a small debugging application called Echo was developed. If you haven't looked at that post and are looking for an introduction to Ring I suggest you do and then come back here.

https://www.dhirubhai.net/pulse/learning-ring-building-echo-brad-lucas/

For this post, I've created another sample application called ring-next which contains routines to demonstrate a number of concepts in Ring. These are:

  • Middleware
  • Responses
  • Parameters
  • Cookies
  • File Uploads
  • Routes

For each of these, I'll show a few code snippets and then explain what is going on. The repo for this project is linked at the end of this post.

Middleware

Handlers are the functions that make up your application. They accept responses and return responses. Middleware is a set of functions that add additional functionality to handlers. They are higher-level functions in that they accept a handler as a parameter and return a value which is a new handler function that will call the original handler function. This pattern allows you to wrap your handler in a number of middleware functions to build up layers of functionality.

To give you an idea of middleware functions here are three from ring-next. There are meant to show the idea.

The first simply add a new field with a value to all requests. The second prints each request to STDOUT. When you lein run the application from a terminal window you'll see requests printed to the output. The last was built during the development of ring-next because I found that Chrome was during some requests looking for the favicon.ico file. This was cluttering up my debugging with extra requests being printed. The wrap-ignore-favicon-request function solved the problem by effectively neutering the call. By having this function wrapped before all the mothers ensured a 404 was returned before any other middleware and handlers were called.

;; You can add items to the request ahead of your main handler
;; Here we are adding a new field :message with a string which has been passed in as a parameter
;;
;; (wrap-request-method-message handler "This is a test message!!")
;;
(defn wrap-request-method-message [handler s]
  (fn [request]
    (handler (assoc request :message s))))

;; Log/print some debug messages during a handler's execution
;;
;; (wrap-debug-print-request handler)
;;
(defn wrap-debug-print-request [handler]
  (fn [request]
    (pprint/pprint (conj request {:body (slurp (:body request))}))
    (handler request)))

;; Ignore favicon.ico requests
;;
;; (wrap-ignore-favicon-request [handler]
;;
(defn wrap-ignore-favicon-request [handler]
  (fn [request]
    (if (= (:uri request) "/favicon.ico")
      {:status 404}
      (handler request))))

Notice how each function takes a handler as a parameter and returns a function that accepts a request and closes over the handler. In other words, the returned function is a handler which calls the handler passed to it as a parameter.

With this in mind, assuming you have a main handler called myhandler, then you'd set up your system as follows.

(def app (wrap-ignore-favicon-request (wrap-debug-print-request (wrap-request-method-message myhandler "TESTING"))))

(run-jetty app {:port 3000}

Responses

Your handlers ultimately return responses. The most basic response is a map of three fields.

{:status 200
 :headers {}
 :body "Hello"}

You can build your own response maps but it will become tedious quickly so there are a number of helper functions included in the ring.util.response namespace. For example, to replace our simple map we can use the response function.

(ring.util.response/response "Hello")

Another type of common response is to serve a static file. You can return a file from the file system or from with the resources directory. Later we'll need to return some HTML files so I'll use the function which returns resources, resource-response. The resource-response function is in the ring.util.response namespace. It returns files from the /resources directory of the project and in this case the public sub-directory.

For example:

(defn static-file
  "Return a static file"
  [filename]
  (response/resource-response filename {:root "public"}))

There are other useful functions in the ring.util.response namespace so it is worth taking a look.

Parameters

Parameters are key-value pairs passed to web applications through the query string or in the body of a request. With Ring, there are two middleware libraries to help manage these values. If you've used Echo to submit or post values and have observed the request maps you'll see that without a library you'd need to write functions to parse the key-value pairs out of the submitted query or body. The ring.middleware.params library does this for you.

The way it works is that you include the library and then wrap your handler with the wrap-params function. This function pre-processes your requests and adds a :params map to your request with all of the key-value pairs inside. Nicely, it builds the map with both your query string values as well as your body values in cases where both are present.

A second function wrap-keyword-params is useful to use with wrap-params. It turns your keys into keywords making your :params map more useful. Make sure to wrap the two in order because the keyword function will be looking for the existence of the :params map.

(wrap-params (wrap-keyword-params handler))

Cookies

Support for cookies comes from a few libraries. First, you need to include the wrap-cookies middleware. This function is in the ring.middleware.cookies namespace. You will also use the set-cookie function from the ring.util.response namespace to create your cookie.

The following function from the example code show support for setting, reading and clearing a cookie. The cookie handler is called from three routes:

  • /cookie/get
  • /cookie/set
  • /cookie/clear

These are set up later in the Routes section. The points of interest here are:

The keyword params are pulled out of a request during the set operation. The parameters are generated by a form which you can see in the /resources/public directory called cookie.html. With the value of the cookie in hand, the set-cookie function uses ring.util.response/set-cookie to build a response with the cookie set.

Reading the cookie is possible due to the wrap-cookies middleware function with turns the cookie value in a request into a :cookies field. Lastly, clearing a cookie is done by setting the cookie's max-age to 1.

(defn get-cookie [request cookie-info]
  (:value (get (:cookies request) (:name cookie-info))))

(defn set-cookie [response request cookie-info val]
  (response/set-cookie response (:name cookie-info) val {:path (:path cookie-info)}))
   
(defn clear-cookie [response request cookie-info]
  (response/set-cookie response (:name cookie-info) "" {:max-age 1 :path (:path cookie-info)}))

(defn cookie
  "Handle cookie request. If GET then read and return the cookie value
else if POST then accept posted value and set the cookie."
  [request]
  (let [cookie-command (fn [s] (keyword (get (clojure.string/split s #"/") 2)))
        cmd (cookie-command (:uri request))          ;; :get, :set, :clear
        cookie-info {:name "ring-next-cookie" :path "/cookie"}]
    (if (and (= cmd :set) (= :post (:request-method request)))    ;; only allow set if posted
      (let [val (:value (:params request))]                       ;; (wrap-params (wrap-keyword-params handler))
        (set-cookie (response/response "Set cookie") request cookie-info val))
      (if (= cmd :clear)
        (clear-cookie (response/response "Clear cookie") request cookie-info)
        (response/response (str "Cookie value is '" (get-cookie request cookie-info) "'"))))))

File Uploads

Uploading files is supported by the wrap-multipart-params middleware function. This function is in the ring.middleware.multipart-params namespace. The following example code shows how to use it. To start, look at the file file.html in the resources/public directory. This is a basic upload file form. It lets you submit a file to the application. There are two routes supported.

  • /file/upload
  • /file/download

The upload route accepts the posted file and saves it using the information organized in the request by wrap-multipart-parms. Here we get the original filename and the path to the temporary file in which the uploaded file has been saved. Then we save the temporary file's path in a cookie.

When the download route is exercised the cookie is read which lets the download-file function read and return the file. Notice how the file-response function is used to return the file.

;; Middleware: wrap-multipart-params
;;
;; :params
;;  {"file" {:filename     "words.txt"
;;           :content-type "text/plain"
;;           :tempfile     #object[java.io.File ...]
;;           :size         51}}

(defn upload-file [request cookie-info]
  (let [original-filename (:filename (:file (:params request)))
        tempfile (:tempfile (:file (:params request)))]
    ;; save tempfile location in cookie
    (set-cookie (response/response "File uploaded") request cookie-info (.getPath tempfile))))

(defn download-file [request cookie-info]
  ;; read file from tempfile location stored in cookie
  (let [filepath (get-cookie request cookie-info)]
    (response/file-response filepath)))

(defn file
  "Handle file request. If GET then read the file and return its contents
else if POST then accept the posted file and save it."
  [request]
  (let [file-command (fn [s] (keyword (get (clojure.string/split s #"/") 2)))
        cmd (file-command (:uri request))
        cookie-info {:name "ring-next-file" :path "/file"}]

    (if (and (= cmd :upload) (= :post (:request-method request)))   ;; only allow upload if posted
      (upload-file request cookie-info)
      (download-file request cookie-info))))

Routes

Routing describes the mapping of URLs to specific functions. Here with Ring we'll need to do our own routing and as an example, I've shown the following routes function. The idea with this function is to look at the uri field in the request and decide how to handle the request. If you look you'll see a few static file requests which are served by static-file. Other sections are supported by the cookie and file function to demonstrate those examples.

Another thing to mention is that in this example routes is what I'd call my main handler. It needs to be the handler that is wrapped up by all of the middleware. The middleware will pre-process the request and routes here will dispatch to the specific support functions to build the appropriate responses.

Lastly, this function is meant to show the idea of dispatching to support functions. There are other cleverer ways to do this and at some point, you'll want to look to a library to make this look a lot better. A common library for this is Compojure.

(defn routes [request]
  (let [uri (:uri request)]
    (case uri
      ;; static file
      "/" (static-file "index.html")
      "/index.html" (static-file "index.html")

      ;; cookie
      "/cookie" (static-file "cookie.html")
      "/cookie/get" (cookie request)
      "/cookie/set" (cookie request)
      "/cookie/clear" (cookie request)

      ;; file
      "/file" (static-file "file.html")
      "/file/upload" (file request)
      "/file/download" (file request)

      ;; default to our main 'echo' handler
      (debug-return-request request))))

As the last step, here is my app var which contains the setup of wrapping middleware around my routes function. Notice the commented out the first version so you see how this sort of thing is built and then the cleaner version using the -> threading macro. Both versions work the same.

(def app
  ;; Initial call chain
  ;; (wrap-ignore-favicon-request (wrap-multipart-params (wrap-cookies (wrap-params (wrap-keyword-params (wrap-request-method-message (wrap-debug-print-request routes) "This is a test message!!"))))))

  ;; Using the threading macro
  (-> routes wrap-debug-print-request (wrap-request-method-message "This is a test message!!")
      wrap-keyword-params                                            ;; this needs to be 'after' wrap-params so there is a :params field for it to its work on
      wrap-params
      wrap-cookies
      wrap-multipart-params       
      wrap-ignore-favicon-request)
  )


(defn -main []
  (jetty/run-jetty app {:port 3000}))

Source Code

The repo for ring-next is available here. https://github.com/bradlucas/ring-next.

Here are few tips for working with the sample code. First, open two terminal windows. In the first start, the application with lein run. This window will show debugging output from the wrap-debug-print-request middleware. In the second terminal, you can test with curl.

For a first step try:

$ curl "https://localhost:3000"

You'll get the index.html page. Try the same URL in a browser to see what it looks like.

Next, try:

$ curl "https://localhost:3000/testing/this/app?name=foo&city=anywhere"

This URL doesn't have a specific handler in routes so we have the request echoed back to use. Things to look for are:

The :message field was set by wrap-request-method-message.

:message "This is a test message!!"

The :uri field has our path of /testing/this/app.

And lastly, our parameters have been added to the :params field.

:params {:name "foo", :city "anywhere"}

Notice that our keys are now keywords thanks for wrap-keyword-params.To try the cookie and file features use the following URLs to start from a browser.

After each operation, use the back button to return and try the next.

For example, in the file form. Upload a file. You'll get a message telling you it was uploaded. Then use the back button to return to the form. Then click Download File to view the file.

The cookie form works the same. Set the cookie, view message, backup and view cookie. Same for clearing the cookie. You can verify the cookie using your browser's developer tools as well.

Summary

One last thought. The above examples are meant to show an exploratory way of learning about Ring. There are certainly better and more clever ways of doing things but here I was aiming for simple and straightforward. Suggestions are of course welcome and please feel free to leave comments with alternatives to the above functions.

Links



Dmytro Chaurov

CEO | Quema | Building scalable and secure IT infrastructures and allocating dedicated IT engineers from our team

1 年

Brad, thanks for sharing!

回复

要查看或添加评论,请登录

Brad Lucas的更多文章

  • Fear Of Layoffs

    Fear Of Layoffs

    So your company has had a layoff recently, and you fear another, or you are simply afraid that they will in the future.…

  • Too Many Hats

    Too Many Hats

    Early in my career, I was working at a large financial services company in Boston. A friend of mine was assigned to a…

  • The Importance of the Camera in Managing Remote Teams: Tips for Success

    The Importance of the Camera in Managing Remote Teams: Tips for Success

    Managers looking for ways to increase camera-on participation should think carefully before acting. They may default to…

    1 条评论
  • Learning Ring And Building Echo

    Learning Ring And Building Echo

    When you come to Clojure and want to build a web app you'll discover Ring almost immediately. Even if you use another…

社区洞察

其他会员也浏览了