March 29, 2014 —

Happy to say that a little utility library of mine, clj-metasearch was released earlier this week.

This library allows you to search through all Clojure namespaces in the current classpath for vars which have metadata matching a predicate. This has a number of potential uses, such as automatically finding functions, or other types of vars that have metadata marking them as some kind of "plugin" for the application. You could do something like:

(find-vars :myapp-plugin)

Which would scan the currently loaded namespaces only for any vars containing metadata with a :myapp-plugin key which had a "truthy" value. find-vars returns a sequence of maps:

({:ns myapp.plugins, :var (var myapp.plugins/awesome-plugin)}
 {:ns myapp.plugins, :var (var myapp.plugins/foobar-plugin)})

You can use var-get on the :var value to get the actual value found. I'll likely be changing this return value format somewhat and possibly introducing a kind of "transform" function as an additional optional argument to find-vars that can be used to transform these results as they are found so the return value is exactly in the format you need.

If you wanted to scan all namespaces, including ones that have yet to be loaded:

  :require-all-namespaces? true)

The extra parameter will cause each namespace to be loaded via require before it is checked. This can potentially cause problems or warnings to be output. For example, if namespaces are loaded that redefine vars, or other namespaces that can't be loaded in the current JVM version (clojure.core.reducers is an example of this, it can't be loaded on Java 6 without some extra library also being present).

When using the :require-all-namespaces? parameter it's usually best to supply an extra predicate to limit the number of Clojure namespaces that are checked (if this is at all possible for your use case):

  :require-all-namespaces? true
  :namespace-pred #(.contains (str %) "myapp-plugin"))

Example: Automatic Compojure Route Discovery

So, I originally put this library together because I wanted to be able to automatically find Compojure routes and have them get added to the Ring handler routes vector without me needing to explicitly list them all out. This is admittedly quite a minor thing to need to do, but it has always seemed silly to me that adding routes to a Ring handler is a manual process in your typical Clojure web app.

Right now I just use a couple macros and a single function for this automatic Compojure route discovery:

(ns yourwebapp.route-utils
  (:require [clj-metasearch.core :refer [find-vars]]
            [compojure.core :refer [defroutes]]
            [noir.util.route :refer [def-restricted-routes]]))

(defmacro register-routes [name & routes]
     ~(with-meta name {:compojure-routes? true})

(defmacro register-restricted-routes [name & routes]
     ~(with-meta name {:compojure-routes? true})

(defn find-routes
  "finds all routes created with the above two macros. namespace-filter is a string or a collection of
   strings used to limit the namespaces searched for available routes. more-routes is any additional
   routes you want to manually append to the list of found routes."
  [namespace-filter & more-routes]
  (let [routes (find-vars
                 :require-all-namespaces? true
                 :namespace-pred (if (coll? namespace-filter)
                                   (fn [namespace]
                                     (some #(.startsWith (str namespace) %) namespace-filter))
                                   (fn [namespace]
                                     (.startsWith (str namespace) namespace-filter))))]
    (as-> routes x
          (map :var x)
          (map var-get x)
          (concat x more-routes)
          (vec x))))

The register-routes and register-restricted-routes macros are drop-in replacements for Compojure's defroutes and lib-noir's def-restricted-routes. Example use of find-routes:

(find-routes "yourwebapp.routes." app-routes)

Would find all the Compojure routes defined with the register-routes and/or register-restricted-routes macros and also append the routes in app-routes to this list. You would then pass the returned list off to your Ring handler.