The work on the framework continues steadily, and I've been integrating some of the feedback I got on the initial release.
I quickly discovered that simply using different files for template modules is insufficient. Many features need to update the project.clj
with dependencies or other options.
To deal with this I made a util which reads in the project file and injects dependencies, plugins and other options. Now each plugin can add its own project elements independently of others.
I'm considering taking the same approach to managing the layout as well. For example, if bootstrap support was selected, then its js/css would be included in layout/common
. Another use case would be to update the application routes if a module provided some new routes of its own.
I'd also like to highlight some of the additions to lib-noir. There are several new namespaces, such as noir.util.cache
, noir.io
, and noir.util.route
. Let's take a look at each of these in turn.
Basic caching is provided via noir.util.cache
. Cache allows wrapping any expr using (cache id expr)
, and the expr will only be evaluated if it's not found in the cache or if the cache has been invalidated. In case expr throws an exception the current cached value will be kept.
There are a couple of helpers for invalidating the cache. First, there's invalidate-cache!
, which takes a key and removes it from the cache. Then, there's clear-cache!
which removes all currently cached items.
It's also possible to set the timeout for cached items using set-cache-timeout!
and passing it a value in seconds. If an item remains in the cache longer than the timeout, the cache will attempt to refresh the value by running the expr associated with the item.
Finally, you can set the maximum size of the cache by calling set-cache-size!
, when the cache grows past the specified size, oldest items will be removed to make room for new ones.
I'm currently using the cache in Luminus for the documentation pages. Luminus fetches the documentation from github as markdown and then translates it to HTML. This is slow enough to be noticeable to the user. On top of that, github is known to have an occasional outage or two. :)
With this scheme, I can keep the docs up to date without having to redeploy the site, and I don't have to worry about the latency or github uptime.
The noir.io
namespace provides some helper functions to make it easier to handle static resources.
You can get the absolute path to the public directory of your application by calling resource-path
.
If you need to read a file located in the public folder you can get a URL for the resource by calling get-resource
and provided the path relative to the public directory.
If the resource is a text file, such as a markdown document, you can use slurp-resource
to read it into a string.
Another addition is the upload-file
function which saves the file generated by a multipart/form-data
form POST to a path relative to the public folder. An example can be seen here:
(ns myapp.upload
...
(:require [noir.io :as io]))
(defn upload-page []
(common/layout
[:h2 "Upload a file"]
(form-to {:enctype "multipart/form-data"}
[:post "/upload"]
(file-upload :file)
(submit-button "upload"))))
(defn handle-upload [file]
(upload-file "/uploads" file)
(redirect
(str "/" (session/get :user) "/" (:filename file))))
(defroutes upload-routes
(GET "/upload" [] (upload-page))
(POST "/upload" [file] (handle-upload file)))
Noir used to have a pre-route
macro, which allowed for filtering and redirecting based on some rules.
Now, lib-noir
provides a restricted
macro which provides similar functionality.
You can define access rules as functions which accept the method, url, and params. The function then returns a boolean to indicate if the rule succeeded or not.
For example, if we wanted to restrict access to a page so that it's only accessible if the id in session matches the id in the page, we could write a rule like this:
(defn user-page [method url params]
(and (= url "/private/:id")
(= (first params) (session/get :user))))
Then we wrap our handler in wrap-access-rules
middleware. The middleware accepts one or more access rule functions, and checks if restricted pages match any of the rules provided.
(def app (-> all-routes
(middleware/app-handler)
(middleware/wrap-access-rules user-page)))
With that in place, we can restrict access to our page as follows.
(restricted GET "/private/:id" [id] "private!")
Note that, you have to use noir.util.middleware/app-handler
for wrap-access-rules
to work correctly. Or manually bind the noir.request/*request*
, eg:
(defn wrap-request-map [handler]
(fn [req]
(binding [noir.request/*request* req]
(handler req))))
update I've since made wrap-request-map
public in lib-noir
, so if you need to wrap the request for any reason, you don't need to roll your own.
I hope you find the new features useful, and as always I'm open to feedback and suggestions for improvements as well as new features.