diff --git a/modules/reitit-core/src/reitit/dependency.cljc b/modules/reitit-core/src/reitit/dependency.cljc new file mode 100644 index 00000000..0a235a20 --- /dev/null +++ b/modules/reitit-core/src/reitit/dependency.cljc @@ -0,0 +1,50 @@ +(ns reitit.dependency + "Dependency resolution for middleware/interceptors.") + +(defn- providers + "Map from provision key to provider. `get-provides` should return the provision keys of a dependent." + [get-provides nodes] + (reduce (fn [acc dependent] + (into acc + (map (fn [provide] + (when (contains? acc provide) + (throw (ex-info (str "multiple providers for: " provide) + {::multiple-providers provide}))) + [provide dependent])) + (get-provides dependent))) + {} nodes)) + +(defn- get-provider + "Get the provider for `k`, throw if no provider can be found for it." + [providers k] + (if (contains? providers k) + (get providers k) + (throw (ex-info (str "provider missing for dependency: " k) + {::missing-provider k})))) + +(defn post-order + "Put `nodes` in post-order. Can also be described as a reverse topological sort. + `get-provides` and `get-requires` are callbacks that you can provide to compute the provide and depend + key sets of nodes, the defaults are `:provides` and `:requires`." + ([nodes] (post-order :provides :requires nodes)) + ([get-provides get-requires nodes] + (let [providers-by-key (providers get-provides nodes)] + (letfn [(toposort [node path colors] + (case (get colors node) + :white (let [requires (get-requires node) + [nodes* colors] (toposort-seq (map (partial get-provider providers-by-key) requires) + (conj path node) + (assoc colors node :grey))] + [(conj nodes* node) + (assoc colors node :black)]) + :grey (throw (ex-info "circular dependency" + {:cycle (drop-while #(not= % node) (conj path node))})) + :black [() colors])) + + (toposort-seq [nodes path colors] + (reduce (fn [[nodes* colors] node] + (let [[nodes** colors] (toposort node path colors)] + [(into nodes* nodes**) colors])) + [[] colors] nodes))] + + (first (toposort-seq nodes [] (zipmap nodes (repeat :white)))))))) diff --git a/test/cljc/reitit/dependency_test.cljc b/test/cljc/reitit/dependency_test.cljc new file mode 100644 index 00000000..d16965c2 --- /dev/null +++ b/test/cljc/reitit/dependency_test.cljc @@ -0,0 +1,34 @@ +(ns reitit.dependency-test + (:require [clojure.test :refer :all] + [reitit.dependency :refer [post-order]]) + #?(:clj (:import [clojure.lang ExceptionInfo]))) + +(deftest post-order-test + (let [base-middlewares [{:name ::bar, :provides #{:bar}, :requires #{:foo}, :wrap identity} + {:name ::baz, :provides #{:baz}, :requires #{:bar :foo}, :wrap identity} + {:name ::foo, :provides #{:foo}, :requires #{}, :wrap identity}]] + (testing "happy cases" + (testing "default ordering works" + (is (= (post-order base-middlewares) + (into (vec (drop 2 base-middlewares)) (take 2 base-middlewares))))) + + (testing "custom provides and requires work" + (is (= (post-order (comp hash-set :name) + (fn [node + ](into #{} (map (fn [k] (keyword "reitit.dependency-test" (name k)))) + (:requires node))) + base-middlewares) + (into (vec (drop 2 base-middlewares)) (take 2 base-middlewares)))))) + + (testing "errors" + (testing "missing dependency detection" + (is (thrown-with-msg? ExceptionInfo #"missing" + (post-order (drop 1 base-middlewares))))) + + (testing "ambiguous dependency detection" + (is (thrown-with-msg? ExceptionInfo #"multiple providers" + (post-order (update-in base-middlewares [0 :provides] conj :foo))))) + + (testing "circular dependency detection" + (is (thrown-with-msg? ExceptionInfo #"circular" + (post-order (assoc-in base-middlewares [2 :requires] #{:baz}))))))))