(ns abra.core (:require clout.core compojure.core [clojure.string :as string])) (def ^:private ^:dynamic *lookup-route* nil) (def ^:private ^:dynamic *request* nil) (def ^:private ^:dynamic *root* nil) (defn ^:private deref-if-var [arg] (if (var? arg) (deref arg) arg)) (defn wrap-reverse-routing "Install the `abra` reverse-routing middleware. This middleware sets up a dynamic scope in which `abra.core/url-for` can be used to lookup routes. The optional second argument, root, provides a prefix to be placed before the url upon generation (for when the routes do not begin at the root path)." [handler & [root]] (fn [request] (binding [*lookup-route* (->> handler deref-if-var meta ::lookup) *request* request *root* (or root "")] (handler request)))) (defn ^:private lookup-route [route & handlers] (->> handlers (map (comp ::lookup meta deref-if-var)) (some #(if % (% route))))) (defn ^:private routing [request & handlers] (some #(% request) handlers)) (defn routes "Create a Ring handler by combining several handlers into one while maintaining `abra` metadata." [& handlers] (vary-meta #(apply routing % handlers) assoc ::lookup #(apply lookup-route % handlers))) (defmacro let-routes "Equivalent to (let [...] (routes ...))" [bindings & handlers] `(let ~bindings (routes ~@handlers))) (defmacro when-routes "Equivalent to (when ... (routes ...))" [cond & handlers] `(if ~cond (routes ~@handlers) (routes))) (defmacro defroutes "Equivalent to (def ... (routes ...))" [name & handlers] `(def ~name (routes ~@handlers))) (defmacro context "Give all routes in the form a common path prefix and set of bindings. The following example demonstrates defining two routes with a common path prefix ('/user/:id') and a common binding ('id'): (context \"/user/:id\" [id] (GET \"/profile\" [] ...) (GET \"/settings\" [] ...)) Additionally maintains `abra` metadata." [path args & routes] (let [string-path (if (vector? path) (first path) path) path-keys (vec (:keys (clout.core/route-compile string-path))) keylen (count path-keys) lookup-fn `(fn [[route-name# args#]] (if (>= (count args#) ~keylen) (let [~args args# r# (try (#'lookup-route [route-name# (vec (drop ~keylen args#))] ~@routes) (catch Exception _#)) {uri# :uri, args# :args} r#] (if r# (assoc r# :uri (str ~string-path uri#) :args (vec (concat ~path-keys args#)))))))] `(vary-meta (compojure.core/context ~path ~args (routes ~@routes)) assoc ::lookup ~lookup-fn))) (defmacro register-route "Provide a name for a given route by which it can be looked up using `abra.core/url-for`" [route-name [type path args & body :as route]] (let [string-path (if (vector? path) (first path) path) route-args (:keys (clout.core/route-compile string-path)) routes-map {:uri string-path :type (keyword (string/lower-case (name type))) :args (vec route-args)}] `(vary-meta ~route assoc ::lookup (fn [[name# args#]] (if (and (= name# ~route-name) (= (count args#) (count ~(vec route-args)))) ~routes-map))))) (defn add-middleware "Add middleware to a Ring stack while maintaining `abra` metadata." [handler middleware & args] (let [result (apply middleware handler args)] (vary-meta result assoc ::lookup (fn [[name args]] (let [lookup-fn (-> (deref-if-var handler) meta ::lookup)] (lookup-fn [name args])))))) (defmacro with-url-fn "Test helper: run body using `f` as a url lookup in `core.abra/url-for`." [f & body] `(binding [*lookup-route* (fn [x#] {:type :get, :uri (apply ~f x#)}) *root* ""] ~@body)) (defn url-for "Lookup the url for a given route. Only valid within the dynamic context set up by `abra.core/wrap-reverse-routing` or `abra.core/with-url-fn`." [route & arg-values] (let [spec (*lookup-route* [route arg-values]) {:keys [uri type args]} spec root-path *root* prefix (if (and *request* (:scheme *request*) (get-in *request* [:headers "host"])) (str (name (:scheme *request*)) "://" (get-in *request* [:headers "host"])))] (if spec (str prefix root-path (reduce (fn [string [name val]] (clojure.string/replace string (str name) (str val))) uri (map vector args arg-values))))))