diff --git a/README.md b/README.md index 0b7c5fa..54b18bd 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,6 @@ -# pl-bidi +# comidi -[![Build Status](https://magnum.travis-ci.com/puppetlabs/pl-bidi.svg?token=ApBsaKK1zdeqHwzhXLzw&branch=master)](https://magnum.travis-ci.com/puppetlabs/pl-bidi) +[![Build Status](https://magnum.travis-ci.com/puppetlabs/comidi.svg?token=ApBsaKK1zdeqHwzhXLzw&branch=master)](https://magnum.travis-ci.com/puppetlabs/comidi) -A Clojure library designed to ... well, that part is up to you. - -## Usage - -FIXME - -## License - -Copyright © 2015 FIXME - -Distributed under the Eclipse Public License either version 1.0 or (at -your option) any later version. +Puppet Labs utility functions and compojure-like syntax-sugar wrappers around +the [bidi](https://github.com/juxt/bidi) web routing library. diff --git a/project.clj b/project.clj index 9d42ce3..10aa4a4 100644 --- a/project.clj +++ b/project.clj @@ -1,8 +1,8 @@ -(defproject puppetlabs/pl-bidi "0.1.0-SNAPSHOT" - :description "Puppet Labs utility functions for use with the bidi web routing library" - :url "https://github.com/puppetlabs/pl-bidi" +(defproject puppetlabs/comidi "0.1.0-SNAPSHOT" + :description "Puppet Labs utility functions and compojure-like wrappers for use with the bidi web routing library" + :url "https://github.com/puppetlabs/comidi" :dependencies [[org.clojure/clojure "1.6.0"] [bidi "1.18.9"] [compojure "1.3.2"] - [prismatic/schema "0.2.2"] + [prismatic/schema "0.4.0"] [puppetlabs/kitchensink "1.1.0"]]) diff --git a/src/puppetlabs/bidi.clj b/src/puppetlabs/bidi.clj deleted file mode 100644 index 144e9e0..0000000 --- a/src/puppetlabs/bidi.clj +++ /dev/null @@ -1,284 +0,0 @@ -(ns puppetlabs.bidi - (:require [bidi.ring :as bidi-ring] - [bidi.bidi :as bidi] - [clojure.zip :as zip] - [compojure.core :as compojure] - [compojure.response :as compojure-response] - [ring.util.response :as ring-response] - [schema.core :as schema] - [puppetlabs.kitchensink.core :as ks]) - (:import (java.util.regex Pattern))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; Schemas - -(defn pattern? - [x] - (instance? Pattern x)) - -(def Zipper - (schema/pred ks/zipper?)) - -(def http-methods - #{:any :get :post :put :delete :head}) - -(def RequestMethod - (schema/enum :any :get :post :put :delete :head)) - -(def RegexPathElement - [(schema/one Pattern "regex") (schema/one schema/Keyword "variable")]) - -(def PathElement - (schema/conditional - string? schema/Str - keyword? schema/Keyword - vector? RegexPathElement)) - -(def RouteInfo - {:path [PathElement] - :request-method RequestMethod}) - -(def Handler - (schema/conditional - keyword? schema/Keyword - fn? (schema/pred fn?) - map? {RequestMethod (schema/recursive #'Handler)})) - -(def RouteMetadata - {:routes [RouteInfo] - :handlers {Handler RouteInfo}}) - -(def BidiPattern - (schema/conditional - keyword? schema/Keyword - string? schema/Str - sequential? [PathElement])) - -(def BidiRouteDestination - (schema/conditional - #(nil? (schema/check Handler %)) Handler - :else [[(schema/one BidiPattern "pattern") - (schema/one (schema/recursive #'BidiRouteDestination) "destination")]])) - -(def BidiRoute - [(schema/one BidiPattern "pattern") - (schema/one BidiRouteDestination "destination")]) - - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; Private - -(defmacro handler-fn* - "Helper macro, used by the compojure-like macros (GET/POST/etc.) to generate - a function that provides compojure's destructuring and rendering support." - [bindings body] - `(fn [request#] - (compojure-response/render - (compojure/let-request [~bindings request#] ~@body) - request#))) - -(defn route-with-method* - "Helper function, used by the compojure-like macros (GET/POST/etc.) to generate - a bidi route that includes a wrapped handler function." - [method pattern bindings body] - `[~pattern {~method (handler-fn* ~bindings ~body)}]) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; Private - route metadata computation - -(schema/defn ^:always-validate - update-route-info* :- RouteInfo - "Helper function, used to maintain a RouteInfo data structure that represents - the current path elements of a route as we traverse the Bidi route tree via - zipper." - [route-info :- RouteInfo - pattern :- BidiPattern] - (cond - (contains? http-methods pattern) - (assoc-in route-info [:request-method] pattern) - - (nil? (schema/check RegexPathElement pattern)) - (update-in route-info [:path] concat [pattern]) - - (sequential? pattern) - (if-let [next (first pattern)] - (update-route-info* - (update-in route-info [:path] conj next) - (rest pattern)) - route-info) - - :else - (update-in route-info [:path] conj pattern))) - -(declare breadth-route-metadata*) - -(schema/defn ^:always-validate - depth-route-metadata* :- RouteMetadata - "Helper function used to traverse branches of the Bidi route tree, depth-first." - [route-meta :- RouteMetadata - route-info :- RouteInfo - loc :- Zipper] - (let [[pattern matched] (zip/node loc)] - (cond - (map? matched) - (depth-route-metadata* - route-meta - route-info - (-> loc zip/down zip/right (zip/edit #(into [] %)) zip/up)) - - (vector? matched) - (breadth-route-metadata* - route-meta - (update-route-info* route-info pattern) - (-> loc zip/down zip/right zip/down)) - - :else - (let [route-info (update-route-info* route-info pattern)] - (-> route-meta - (update-in [:routes] conj route-info) - (assoc-in [:handlers matched] route-info)))))) - -(schema/defn ^:always-validate - breadth-route-metadata* :- RouteMetadata - "Helper function used to traverse branches of the Bidi route tree, breadth-first." - [route-meta :- RouteMetadata - route-info :- RouteInfo - loc :- Zipper] - (loop [route-meta route-meta - loc loc] - (let [routes (depth-route-metadata* route-meta route-info loc)] - (if-let [next (zip/right loc)] - (recur routes next) - routes)))) - -(schema/defn ^:always-validate - route-metadata :- RouteMetadata - "Traverses a Bidi route tree and returns route metadata, which includes a list - of RouteInfo objects (one per route), plus a mechanism to look up the - RouteInfo for a given handler." - [routes :- BidiRoute] - (let [route-info {:path [] - :request-method :any} - loc (-> [routes] zip/vector-zip zip/down)] - (breadth-route-metadata* {:routes [] - :handlers {}} route-info loc))) - -(schema/defn ^:always-validate - make-handler :- (schema/pred fn?) - "Create a Ring handler from the route definition data structure. (This code - is largely borrowed from bidi core.) Arguments: - - - route-meta: metadata about the routes; allows us to look up the route info - by handler. You can get this by calling `route-metadata`. - - routes: the Bidi route tree - - handler-fn: this fn will be called on all of the handlers found in the bidi - route tree; it is expected to return a ring handler fn for that - route. If you are using the compojure-like macros in this - namespace or have nested your ring handler functions in the bidi - tree by other means, you can just pass `identity` here, or pass - in some middleware fn to wrap around the nested ring handlers. - The handlers will have access to the `RouteInfo` of the matching - bidi route via the `:route-info` key in the request map." - [route-meta :- RouteMetadata - routes :- BidiRoute - handler-fn :- (schema/pred fn?)] - (let [compiled-routes (bidi/compile-route routes)] - (fn [{:keys [uri path-info] :as req}] - (let [path (or path-info uri) - {:keys [handler route-params] :as match-context} - (apply bidi/match-route compiled-routes path (apply concat (seq req)))] - (when handler - (let [req (-> req - (update-in [:params] merge route-params) - (update-in [:route-params] merge route-params) - (assoc-in [:route-info] (get-in route-meta - [:handlers handler])))] - (bidi-ring/request - (handler-fn handler) - req - (apply dissoc match-context :handler (keys req))))))))) - - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;;; Public - core functions - -(schema/defn ^:always-validate - routes :- BidiRoute - "Combines multiple bidi routes into a single data structure; this is largely - just a convenience function for grouping several routes together as a single - object that can be passed around." - [& routes :- [BidiRoute]] - ["" (vec routes)]) - -(schema/defn ^:always-validate - context :- BidiRoute - "Combines multiple bidi routes together into a single data structure, but nests - them all under the given url-prefix. This is similar to compojure's `context` - macro, but does not accept a binding form. You can still destructure variables - by passing a bidi pattern for `url-prefix`, and the variables will be available - to all nested routes." - [url-prefix :- BidiPattern - & routes :- [BidiRoute]] - [url-prefix (vec routes)]) - -(schema/defn ^:always-validate - routes->handler :- (schema/pred fn?) - "Given a bidi route tree, converts into a ring request handler function. You - may pass an optional middleware function that will be wrapped around the - request handling; the middleware fn will have access to the `RouteInfo` of the - matching bidi route via the `:route-info` key in the request map." - ([routes :- BidiRoute - route-middleware-fn :- (schema/maybe (schema/pred fn?))] - (let [route-meta (route-metadata routes)] - (with-meta - (make-handler route-meta - routes - route-middleware-fn) - {:route-metadata route-meta}))) - ([routes] - (routes->handler routes identity))) - -(schema/defn ^:always-validate - context-handler :- (schema/pred fn?) - "Convenience function that effectively composes `context` and `routes->handler`." - [url-prefix :- BidiPattern - & routes :- [BidiRoute]] - (routes->handler - (apply context url-prefix routes))) - - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; Public - compojure-like convenience macros - -(defmacro ANY - [pattern bindings & body] - `[~pattern (handler-fn* ~bindings ~body)]) - -(defmacro GET - [pattern bindings & body] - (route-with-method* :get pattern bindings body)) - -(defmacro HEAD - [pattern bindings & body] - (route-with-method* :head pattern bindings body)) - -(defmacro PUT - [pattern bindings & body] - (route-with-method* :put pattern bindings body)) - -(defmacro POST - [pattern bindings & body] - (route-with-method* :post pattern bindings body)) - -(defmacro DELETE - [pattern bindings & body] - (route-with-method* :delete pattern bindings body)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; Public - pre-built routes - -(defn not-found - [body] - [[[#".*" :rest]] (fn [request] - (-> (compojure-response/render body request) - (ring-response/status 404)))]) diff --git a/src/puppetlabs/comidi.clj b/src/puppetlabs/comidi.clj new file mode 100644 index 0000000..ea7dbd4 --- /dev/null +++ b/src/puppetlabs/comidi.clj @@ -0,0 +1,284 @@ +(ns puppetlabs.comidi + (:require [bidi.ring :as bidi-ring] + [bidi.bidi :as bidi] + [clojure.zip :as zip] + [compojure.core :as compojure] + [compojure.response :as compojure-response] + [ring.util.response :as ring-response] + [schema.core :as schema] + [puppetlabs.kitchensink.core :as ks]) + (:import (java.util.regex Pattern))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Schemas + +(defn pattern? + [x] + (instance? Pattern x)) + +(def Zipper + (schema/pred ks/zipper?)) + +(def http-methods + #{:any :get :post :put :delete :head}) + +(def RequestMethod + (schema/enum :any :get :post :put :delete :head)) + +(def RegexPathElement + [(schema/one Pattern "regex") (schema/one schema/Keyword "variable")]) + +(def PathElement + (schema/conditional + string? schema/Str + keyword? schema/Keyword + vector? RegexPathElement)) + +(def RouteInfo + {:path [PathElement] + :request-method RequestMethod}) + +(def Handler + (schema/conditional + keyword? schema/Keyword + fn? (schema/pred fn?) + map? {RequestMethod (schema/recursive #'Handler)})) + +(def RouteMetadata + {:routes [RouteInfo] + :handlers {Handler RouteInfo}}) + +(def BidiPattern + (schema/conditional + keyword? schema/Keyword + string? schema/Str + sequential? [PathElement])) + +(def BidiRouteDestination + (schema/conditional + #(nil? (schema/check Handler %)) Handler + :else [[(schema/one BidiPattern "pattern") + (schema/one (schema/recursive #'BidiRouteDestination) "destination")]])) + +(def BidiRoute + [(schema/one BidiPattern "pattern") + (schema/one BidiRouteDestination "destination")]) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Private + +(defmacro handler-fn* + "Helper macro, used by the compojure-like macros (GET/POST/etc.) to generate + a function that provides compojure's destructuring and rendering support." + [bindings body] + `(fn [request#] + (compojure-response/render + (compojure/let-request [~bindings request#] ~@body) + request#))) + +(defn route-with-method* + "Helper function, used by the compojure-like macros (GET/POST/etc.) to generate + a bidi route that includes a wrapped handler function." + [method pattern bindings body] + `[~pattern {~method (handler-fn* ~bindings ~body)}]) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Private - route metadata computation + +(schema/defn ^:always-validate + update-route-info* :- RouteInfo + "Helper function, used to maintain a RouteInfo data structure that represents + the current path elements of a route as we traverse the Bidi route tree via + zipper." + [route-info :- RouteInfo + pattern :- BidiPattern] + (cond + (contains? http-methods pattern) + (assoc-in route-info [:request-method] pattern) + + (nil? (schema/check RegexPathElement pattern)) + (update-in route-info [:path] concat [pattern]) + + (sequential? pattern) + (if-let [next (first pattern)] + (update-route-info* + (update-in route-info [:path] conj next) + (rest pattern)) + route-info) + + :else + (update-in route-info [:path] conj pattern))) + +(declare breadth-route-metadata*) + +(schema/defn ^:always-validate + depth-route-metadata* :- RouteMetadata + "Helper function used to traverse branches of the Bidi route tree, depth-first." + [route-meta :- RouteMetadata + route-info :- RouteInfo + loc :- Zipper] + (let [[pattern matched] (zip/node loc)] + (cond + (map? matched) + (depth-route-metadata* + route-meta + route-info + (-> loc zip/down zip/right (zip/edit #(into [] %)) zip/up)) + + (vector? matched) + (breadth-route-metadata* + route-meta + (update-route-info* route-info pattern) + (-> loc zip/down zip/right zip/down)) + + :else + (let [route-info (update-route-info* route-info pattern)] + (-> route-meta + (update-in [:routes] conj route-info) + (assoc-in [:handlers matched] route-info)))))) + +(schema/defn ^:always-validate + breadth-route-metadata* :- RouteMetadata + "Helper function used to traverse branches of the Bidi route tree, breadth-first." + [route-meta :- RouteMetadata + route-info :- RouteInfo + loc :- Zipper] + (loop [route-meta route-meta + loc loc] + (let [routes (depth-route-metadata* route-meta route-info loc)] + (if-let [next (zip/right loc)] + (recur routes next) + routes)))) + +(schema/defn ^:always-validate + route-metadata :- RouteMetadata + "Traverses a Bidi route tree and returns route metadata, which includes a list + of RouteInfo objects (one per route), plus a mechanism to look up the + RouteInfo for a given handler." + [routes :- BidiRoute] + (let [route-info {:path [] + :request-method :any} + loc (-> [routes] zip/vector-zip zip/down)] + (breadth-route-metadata* {:routes [] + :handlers {}} route-info loc))) + +(schema/defn ^:always-validate + make-handler :- (schema/pred fn?) + "Create a Ring handler from the route definition data structure. (This code + is largely borrowed from bidi core.) Arguments: + + - route-meta: metadata about the routes; allows us to look up the route info + by handler. You can get this by calling `route-metadata`. + - routes: the Bidi route tree + - handler-fn: this fn will be called on all of the handlers found in the bidi + route tree; it is expected to return a ring handler fn for that + route. If you are using the compojure-like macros in this + namespace or have nested your ring handler functions in the bidi + tree by other means, you can just pass `identity` here, or pass + in some middleware fn to wrap around the nested ring handlers. + The handlers will have access to the `RouteInfo` of the matching + bidi route via the `:route-info` key in the request map." + [route-meta :- RouteMetadata + routes :- BidiRoute + handler-fn :- (schema/pred fn?)] + (let [compiled-routes (bidi/compile-route routes)] + (fn [{:keys [uri path-info] :as req}] + (let [path (or path-info uri) + {:keys [handler route-params] :as match-context} + (apply bidi/match-route compiled-routes path (apply concat (seq req)))] + (when handler + (let [req (-> req + (update-in [:params] merge route-params) + (update-in [:route-params] merge route-params) + (assoc-in [:route-info] (get-in route-meta + [:handlers handler])))] + (bidi-ring/request + (handler-fn handler) + req + (apply dissoc match-context :handler (keys req))))))))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; Public - core functions + +(schema/defn ^:always-validate + routes :- BidiRoute + "Combines multiple bidi routes into a single data structure; this is largely + just a convenience function for grouping several routes together as a single + object that can be passed around." + [& routes :- [BidiRoute]] + ["" (vec routes)]) + +(schema/defn ^:always-validate + context :- BidiRoute + "Combines multiple bidi routes together into a single data structure, but nests + them all under the given url-prefix. This is similar to compojure's `context` + macro, but does not accept a binding form. You can still destructure variables + by passing a bidi pattern for `url-prefix`, and the variables will be available + to all nested routes." + [url-prefix :- BidiPattern + & routes :- [BidiRoute]] + [url-prefix (vec routes)]) + +(schema/defn ^:always-validate + routes->handler :- (schema/pred fn?) + "Given a bidi route tree, converts into a ring request handler function. You + may pass an optional middleware function that will be wrapped around the + request handling; the middleware fn will have access to the `RouteInfo` of the + matching bidi route via the `:route-info` key in the request map." + ([routes :- BidiRoute + route-middleware-fn :- (schema/maybe (schema/pred fn?))] + (let [route-meta (route-metadata routes)] + (with-meta + (make-handler route-meta + routes + route-middleware-fn) + {:route-metadata route-meta}))) + ([routes] + (routes->handler routes identity))) + +(schema/defn ^:always-validate + context-handler :- (schema/pred fn?) + "Convenience function that effectively composes `context` and `routes->handler`." + [url-prefix :- BidiPattern + & routes :- [BidiRoute]] + (routes->handler + (apply context url-prefix routes))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Public - compojure-like convenience macros + +(defmacro ANY + [pattern bindings & body] + `[~pattern (handler-fn* ~bindings ~body)]) + +(defmacro GET + [pattern bindings & body] + (route-with-method* :get pattern bindings body)) + +(defmacro HEAD + [pattern bindings & body] + (route-with-method* :head pattern bindings body)) + +(defmacro PUT + [pattern bindings & body] + (route-with-method* :put pattern bindings body)) + +(defmacro POST + [pattern bindings & body] + (route-with-method* :post pattern bindings body)) + +(defmacro DELETE + [pattern bindings & body] + (route-with-method* :delete pattern bindings body)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Public - pre-built routes + +(defn not-found + [body] + [[[#".*" :rest]] (fn [request] + (-> (compojure-response/render body request) + (ring-response/status 404)))]) diff --git a/test/puppetlabs/bidi_test.clj b/test/puppetlabs/bidi_test.clj deleted file mode 100644 index 763dd4b..0000000 --- a/test/puppetlabs/bidi_test.clj +++ /dev/null @@ -1,284 +0,0 @@ -(ns puppetlabs.bidi-test - (require [clojure.test :refer :all] - [puppetlabs.bidi :as pl-bidi] - [schema.test :as schema-test] - [puppetlabs.bidi :refer :all] - [schema.core :as schema] - [clojure.zip :as zip])) - -(use-fixtures :once schema-test/validate-schemas) - -(defn replace-regexes-for-equality-check - [xs] - (loop [loc (zip/vector-zip xs)] - (if (zip/end? loc) - (zip/root loc) - (recur - (let [node (zip/node loc)] - (if (pattern? node) - (zip/edit loc #(str "REGEX: " (.pattern %))) - (zip/next loc))))))) - -(deftest handler-schema-test - (testing "handler schema" - (is (nil? (schema/check Handler :foo))) - (is (nil? (schema/check Handler (fn [] :foo)))) - (is (nil? (schema/check Handler {:get (fn [] :foo)}))) - (is (nil? (schema/check Handler {:post :foo}))))) - -(deftest pattern-schema-test - (testing "pattern schema" - (is (nil? (schema/check BidiPattern "/foo"))) - (is (nil? (schema/check BidiPattern :foo))) - (is (nil? (schema/check BidiPattern ["/foo/" :foo "/foo"]))) - (is (nil? (schema/check BidiPattern ["/foo/" [#".*" :rest]]))))) - -(deftest destination-schema-test - (testing "route destination schema" - (is (nil? (schema/check BidiRouteDestination :foo))) - (is (nil? (schema/check BidiRouteDestination (fn [] nil)))) - (is (nil? (schema/check BidiRouteDestination {:get (fn [] nil)}))) - (is (nil? (schema/check BidiRouteDestination {:get :my-handler}))) - (is (nil? (schema/check BidiRouteDestination [[["/foo/" :foo "/foo"] :foo]]))) - (is (not (nil? (schema/check BidiRouteDestination [["/foo/" :foo "/foo"] :foo])))) - (is (nil? (schema/check BidiRouteDestination [[["/foo/" :foo] - :foo-handler] - [["/bar/" :bar] - {:get :bar-handler}]]))))) - -(deftest route-schema-test - (testing "route schema" - (is (nil? (schema/check BidiRoute [:foo :foo]))) - (is (nil? (schema/check BidiRoute ["/foo" [[:foo :foo]]]))) - (is (not (nil? (schema/check BidiRoute ["/foo" [:foo :foo]])))) - (is (nil? (schema/check BidiRoute ["" [[["/foo/" :foo] - :foo-handler] - [["/bar/" :bar] - {:get :bar-handler}]]]))))) - -(deftest update-route-info-test - (let [orig-route-info {:path [] - :request-method :any}] - (testing "HTTP verb keyword causes request-method to be updated" - (doseq [verb [:get :post :put :delete :head]] - (is (= {:path [] - :request-method verb} - (update-route-info* orig-route-info verb))))) - (testing "string path elements get added to the path" - (is (= {:path ["/foo"] - :request-method :any} - (update-route-info* orig-route-info "/foo")))) - (testing "keyword path elements get added to the path" - (is (= {:path [:foo] - :request-method :any} - (update-route-info* orig-route-info :foo)))) - (testing "vector path elements get flattened and added to the path" - (is (= {:path ["/foo/" :foo] - :request-method :any} - (update-route-info* orig-route-info ["/foo/" :foo])))) - (testing "regex path element gets added to the path" - (is (= {:path ["/foo/" ["REGEX: .*" :foo]] - :request-method :any} - (-> (update-route-info* orig-route-info ["/foo/" [#".*" :foo]]) - (update-in [:path] replace-regexes-for-equality-check))))))) - -(deftest route-metadata-test - (testing "route metadata includes ordered list of routes and lookup by handler" - (let [routes ["" [[["/foo/" :foo] - :foo-handler] - [["/bar/" :bar] - [["/baz" {:get :baz-handler}] - ["/bam" {:put :bam-handler}] - ["/bap" {:any :bap-handler}]]] - ["/buzz" {:post :buzz-handler}]]] - expected-foo-meta {:path '("" "/foo/" :foo) - :request-method :any} - expected-baz-meta {:path '("" "/bar/" :bar "/baz") - :request-method :get} - expected-bam-meta {:path '("" "/bar/" :bar "/bam") - :request-method :put} - expected-bap-meta {:path '("" "/bar/" :bar "/bap") - :request-method :any} - expected-buzz-meta {:path '("" "/buzz") - :request-method :post}] - (is (= (pl-bidi/route-metadata routes) - {:routes [expected-foo-meta - expected-baz-meta - expected-bam-meta - expected-bap-meta - expected-buzz-meta] - :handlers {:foo-handler expected-foo-meta - :baz-handler expected-baz-meta - :bam-handler expected-bam-meta - :bap-handler expected-bap-meta - :buzz-handler expected-buzz-meta}}))))) - -(deftest routes-test - (is (= ["" [["/foo" :foo-handler] - [["/bar/" :bar] :bar-handler]]] - (routes ["/foo" :foo-handler] - [["/bar/" :bar] :bar-handler])))) - -(deftest context-test - (testing "simple context" - (is (= ["/foo" [["/bar" :bar-handler] - [["/baz" :baz] :baz-handler]]] - (context "/foo" - ["/bar" :bar-handler] - [["/baz" :baz] :baz-handler])))) - (testing "context with variable" - (is (= [["/foo" :foo] [["/bar" :bar-handler] - [["/baz" :baz] :baz-handler]]] - (context ["/foo" :foo] - ["/bar" :bar-handler] - [["/baz" :baz] :baz-handler]))))) - -(deftest routes->handler-test - (testing "routes are matched against a request properly, with route params" - (let [handler (routes->handler ["/foo" - [["" - [["/bar" - (fn [req] :bar)] - [["/baz/" :baz] - (fn [req] - {:endpoint :baz - :route-params (:route-params req)})]]]]])] - (is (= :bar (handler {:uri "/foo/bar"}))) - (is (= {:endpoint :baz - :route-params {:baz "howdy"}} - (handler {:uri "/foo/baz/howdy"}))))) - (testing "request-methods are honored" - (let [handler (routes->handler ["/foo" {:get (fn [req] :foo)}])] - (is (nil? (handler {:uri "/foo"}))) - (is (= :foo (handler {:uri "/foo" :request-method :get}))))) - (testing "contexts can bind route variables" - (let [handler (routes->handler - (context ["/foo/" :foo] - [["/bar/" :bar] - (fn [req] (:route-params req))]))] - (is (= {:foo "hi" - :bar "there"} - (handler {:uri "/foo/hi/bar/there"}))))) - (testing "route metadata is added to fn metadata" - (let [foo-handler (fn [req] :foo) - handler (routes->handler ["/foo" {:get foo-handler}])] - (let [route-meta (:route-metadata (meta handler))] - (is (= {:routes [{:path ["/foo"] - :request-method :get}] - :handlers {foo-handler {:path ["/foo"] - :request-method :get}}} - route-meta)))))) - -(deftest routes->handler-middleware-test - (let [handler (routes->handler - (context ["/foo/" :foo] - [["/bar/" :bar] - (fn [req] (:route-params req))]) - (fn [f] - (fn [req] - {:result (f req) - :route-info (:route-info req)})))] - (is (= {:result {:foo "hi" - :bar "there"} - :route-info {:path ["/foo/" :foo "/bar/" :bar] - :request-method :any}} - (handler {:uri "/foo/hi/bar/there"}))))) - -(deftest context-handler-test - (let [handler (context-handler ["/foo/" :foo] - [["/bar/" :bar] - (fn [req] (:route-params req))])] - (is (= {:foo "hi" - :bar "there"} - (handler {:uri "/foo/hi/bar/there"}))))) - - -(deftest compojure-macros-test - (let [routes (context ["/foo/" :foo] - (ANY ["/any/" :any] [foo any] - (str "foo: " foo " any: " any)) - (GET ["/get/" :get] [foo get] - (fn [req] {:foo foo - :get get})) - (HEAD ["/head/" :head] [foo head] - {:foo foo - :head head}) - (PUT "/put" [foo] - {:status 500 - :body foo}) - (POST ["/post/" :post] [post] - post) - (DELETE ["/delete/" :delete] [foo delete] - (atom {:foo foo - :delete delete}))) - handler (routes->handler routes)] - (is (nil? (handler {:uri "/foo/hi/get/there" :request-method :post}))) - (is (nil? (handler {:uri "/foo/hi/head/there" :request-method :get}))) - (is (nil? (handler {:uri "/foo/hi/put" :request-method :get}))) - (is (nil? (handler {:uri "/foo/hi/post/there" :request-method :get}))) - (is (nil? (handler {:uri "/foo/hi/delete/there" :request-method :get}))) - - (is (= "foo: hi any: there" (:body (handler {:uri "/foo/hi/any/there"})))) - (is (= {:foo "hi" - :get "there"} - (select-keys - (handler {:uri "/foo/hi/get/there" :request-method :get}) - [:foo :get]))) - (is (= {:foo "hi" - :head "there"} - (select-keys - (handler {:uri "/foo/hi/head/there" :request-method :head}) - [:foo :head]))) - (is (= {:status 500 - :body "hi"} - (select-keys - (handler {:uri "/foo/hi/put" :request-method :put}) - [:status :body]))) - (is (= {:status 200 - :body "there"} - (select-keys - (handler {:uri "/foo/hi/post/there" :request-method :post}) - [:status :body]))) - (is (= {:status 200 - :foo "hi" - :delete "there"} - (select-keys - (handler {:uri "/foo/hi/delete/there" :request-method :delete}) - [:status :foo :delete]))))) - -(deftest not-found-test - (testing "root not-found handler" - (let [handler (routes->handler (not-found "nobody's home, yo"))] - (is (= {:status 404 - :body "nobody's home, yo"} - (select-keys - (handler {:uri "/hi/there"}) - [:body :status]))))) - (testing "nested not-found handler" - (let [handler (routes->handler - (routes - ["/bar" [["" (fn [req] :bar)] - (not-found "nothing else under bar!")]] - (not-found "nothing else under root!")))] - (is (= :bar (handler {:uri "/bar"}))) - (is (= {:status 404 - :body "nothing else under bar!"} - (select-keys - (handler {:uri "/bar/baz"}) - [:status :body]))) - (is (= {:status 404 - :body "nothing else under root!"} - (select-keys - (handler {:uri "/yo/mang"}) - [:status :body])))))) - -(deftest regex-test - (let [handler (routes->handler - ["/foo" [[["/boo/" [#".*" :rest]] - (fn [req] (:rest (:route-params req)))]]])] - (is (= "hi/there" - (handler {:uri "/foo/boo/hi/there"}))))) - - - - diff --git a/test/puppetlabs/comidi_test.clj b/test/puppetlabs/comidi_test.clj new file mode 100644 index 0000000..ac1ca33 --- /dev/null +++ b/test/puppetlabs/comidi_test.clj @@ -0,0 +1,284 @@ +(ns puppetlabs.comidi-test + (require [clojure.test :refer :all] + [puppetlabs.comidi :as comidi] + [schema.test :as schema-test] + [puppetlabs.comidi :refer :all] + [schema.core :as schema] + [clojure.zip :as zip])) + +(use-fixtures :once schema-test/validate-schemas) + +(defn replace-regexes-for-equality-check + [xs] + (loop [loc (zip/vector-zip xs)] + (if (zip/end? loc) + (zip/root loc) + (recur + (let [node (zip/node loc)] + (if (pattern? node) + (zip/edit loc #(str "REGEX: " (.pattern %))) + (zip/next loc))))))) + +(deftest handler-schema-test + (testing "handler schema" + (is (nil? (schema/check Handler :foo))) + (is (nil? (schema/check Handler (fn [] :foo)))) + (is (nil? (schema/check Handler {:get (fn [] :foo)}))) + (is (nil? (schema/check Handler {:post :foo}))))) + +(deftest pattern-schema-test + (testing "pattern schema" + (is (nil? (schema/check BidiPattern "/foo"))) + (is (nil? (schema/check BidiPattern :foo))) + (is (nil? (schema/check BidiPattern ["/foo/" :foo "/foo"]))) + (is (nil? (schema/check BidiPattern ["/foo/" [#".*" :rest]]))))) + +(deftest destination-schema-test + (testing "route destination schema" + (is (nil? (schema/check BidiRouteDestination :foo))) + (is (nil? (schema/check BidiRouteDestination (fn [] nil)))) + (is (nil? (schema/check BidiRouteDestination {:get (fn [] nil)}))) + (is (nil? (schema/check BidiRouteDestination {:get :my-handler}))) + (is (nil? (schema/check BidiRouteDestination [[["/foo/" :foo "/foo"] :foo]]))) + (is (not (nil? (schema/check BidiRouteDestination [["/foo/" :foo "/foo"] :foo])))) + (is (nil? (schema/check BidiRouteDestination [[["/foo/" :foo] + :foo-handler] + [["/bar/" :bar] + {:get :bar-handler}]]))))) + +(deftest route-schema-test + (testing "route schema" + (is (nil? (schema/check BidiRoute [:foo :foo]))) + (is (nil? (schema/check BidiRoute ["/foo" [[:foo :foo]]]))) + (is (not (nil? (schema/check BidiRoute ["/foo" [:foo :foo]])))) + (is (nil? (schema/check BidiRoute ["" [[["/foo/" :foo] + :foo-handler] + [["/bar/" :bar] + {:get :bar-handler}]]]))))) + +(deftest update-route-info-test + (let [orig-route-info {:path [] + :request-method :any}] + (testing "HTTP verb keyword causes request-method to be updated" + (doseq [verb [:get :post :put :delete :head]] + (is (= {:path [] + :request-method verb} + (update-route-info* orig-route-info verb))))) + (testing "string path elements get added to the path" + (is (= {:path ["/foo"] + :request-method :any} + (update-route-info* orig-route-info "/foo")))) + (testing "keyword path elements get added to the path" + (is (= {:path [:foo] + :request-method :any} + (update-route-info* orig-route-info :foo)))) + (testing "vector path elements get flattened and added to the path" + (is (= {:path ["/foo/" :foo] + :request-method :any} + (update-route-info* orig-route-info ["/foo/" :foo])))) + (testing "regex path element gets added to the path" + (is (= {:path ["/foo/" ["REGEX: .*" :foo]] + :request-method :any} + (-> (update-route-info* orig-route-info ["/foo/" [#".*" :foo]]) + (update-in [:path] replace-regexes-for-equality-check))))))) + +(deftest route-metadata-test + (testing "route metadata includes ordered list of routes and lookup by handler" + (let [routes ["" [[["/foo/" :foo] + :foo-handler] + [["/bar/" :bar] + [["/baz" {:get :baz-handler}] + ["/bam" {:put :bam-handler}] + ["/bap" {:any :bap-handler}]]] + ["/buzz" {:post :buzz-handler}]]] + expected-foo-meta {:path '("" "/foo/" :foo) + :request-method :any} + expected-baz-meta {:path '("" "/bar/" :bar "/baz") + :request-method :get} + expected-bam-meta {:path '("" "/bar/" :bar "/bam") + :request-method :put} + expected-bap-meta {:path '("" "/bar/" :bar "/bap") + :request-method :any} + expected-buzz-meta {:path '("" "/buzz") + :request-method :post}] + (is (= (comidi/route-metadata routes) + {:routes [expected-foo-meta + expected-baz-meta + expected-bam-meta + expected-bap-meta + expected-buzz-meta] + :handlers {:foo-handler expected-foo-meta + :baz-handler expected-baz-meta + :bam-handler expected-bam-meta + :bap-handler expected-bap-meta + :buzz-handler expected-buzz-meta}}))))) + +(deftest routes-test + (is (= ["" [["/foo" :foo-handler] + [["/bar/" :bar] :bar-handler]]] + (routes ["/foo" :foo-handler] + [["/bar/" :bar] :bar-handler])))) + +(deftest context-test + (testing "simple context" + (is (= ["/foo" [["/bar" :bar-handler] + [["/baz" :baz] :baz-handler]]] + (context "/foo" + ["/bar" :bar-handler] + [["/baz" :baz] :baz-handler])))) + (testing "context with variable" + (is (= [["/foo" :foo] [["/bar" :bar-handler] + [["/baz" :baz] :baz-handler]]] + (context ["/foo" :foo] + ["/bar" :bar-handler] + [["/baz" :baz] :baz-handler]))))) + +(deftest routes->handler-test + (testing "routes are matched against a request properly, with route params" + (let [handler (routes->handler ["/foo" + [["" + [["/bar" + (fn [req] :bar)] + [["/baz/" :baz] + (fn [req] + {:endpoint :baz + :route-params (:route-params req)})]]]]])] + (is (= :bar (handler {:uri "/foo/bar"}))) + (is (= {:endpoint :baz + :route-params {:baz "howdy"}} + (handler {:uri "/foo/baz/howdy"}))))) + (testing "request-methods are honored" + (let [handler (routes->handler ["/foo" {:get (fn [req] :foo)}])] + (is (nil? (handler {:uri "/foo"}))) + (is (= :foo (handler {:uri "/foo" :request-method :get}))))) + (testing "contexts can bind route variables" + (let [handler (routes->handler + (context ["/foo/" :foo] + [["/bar/" :bar] + (fn [req] (:route-params req))]))] + (is (= {:foo "hi" + :bar "there"} + (handler {:uri "/foo/hi/bar/there"}))))) + (testing "route metadata is added to fn metadata" + (let [foo-handler (fn [req] :foo) + handler (routes->handler ["/foo" {:get foo-handler}])] + (let [route-meta (:route-metadata (meta handler))] + (is (= {:routes [{:path ["/foo"] + :request-method :get}] + :handlers {foo-handler {:path ["/foo"] + :request-method :get}}} + route-meta)))))) + +(deftest routes->handler-middleware-test + (let [handler (routes->handler + (context ["/foo/" :foo] + [["/bar/" :bar] + (fn [req] (:route-params req))]) + (fn [f] + (fn [req] + {:result (f req) + :route-info (:route-info req)})))] + (is (= {:result {:foo "hi" + :bar "there"} + :route-info {:path ["/foo/" :foo "/bar/" :bar] + :request-method :any}} + (handler {:uri "/foo/hi/bar/there"}))))) + +(deftest context-handler-test + (let [handler (context-handler ["/foo/" :foo] + [["/bar/" :bar] + (fn [req] (:route-params req))])] + (is (= {:foo "hi" + :bar "there"} + (handler {:uri "/foo/hi/bar/there"}))))) + + +(deftest compojure-macros-test + (let [routes (context ["/foo/" :foo] + (ANY ["/any/" :any] [foo any] + (str "foo: " foo " any: " any)) + (GET ["/get/" :get] [foo get] + (fn [req] {:foo foo + :get get})) + (HEAD ["/head/" :head] [foo head] + {:foo foo + :head head}) + (PUT "/put" [foo] + {:status 500 + :body foo}) + (POST ["/post/" :post] [post] + post) + (DELETE ["/delete/" :delete] [foo delete] + (atom {:foo foo + :delete delete}))) + handler (routes->handler routes)] + (is (nil? (handler {:uri "/foo/hi/get/there" :request-method :post}))) + (is (nil? (handler {:uri "/foo/hi/head/there" :request-method :get}))) + (is (nil? (handler {:uri "/foo/hi/put" :request-method :get}))) + (is (nil? (handler {:uri "/foo/hi/post/there" :request-method :get}))) + (is (nil? (handler {:uri "/foo/hi/delete/there" :request-method :get}))) + + (is (= "foo: hi any: there" (:body (handler {:uri "/foo/hi/any/there"})))) + (is (= {:foo "hi" + :get "there"} + (select-keys + (handler {:uri "/foo/hi/get/there" :request-method :get}) + [:foo :get]))) + (is (= {:foo "hi" + :head "there"} + (select-keys + (handler {:uri "/foo/hi/head/there" :request-method :head}) + [:foo :head]))) + (is (= {:status 500 + :body "hi"} + (select-keys + (handler {:uri "/foo/hi/put" :request-method :put}) + [:status :body]))) + (is (= {:status 200 + :body "there"} + (select-keys + (handler {:uri "/foo/hi/post/there" :request-method :post}) + [:status :body]))) + (is (= {:status 200 + :foo "hi" + :delete "there"} + (select-keys + (handler {:uri "/foo/hi/delete/there" :request-method :delete}) + [:status :foo :delete]))))) + +(deftest not-found-test + (testing "root not-found handler" + (let [handler (routes->handler (not-found "nobody's home, yo"))] + (is (= {:status 404 + :body "nobody's home, yo"} + (select-keys + (handler {:uri "/hi/there"}) + [:body :status]))))) + (testing "nested not-found handler" + (let [handler (routes->handler + (routes + ["/bar" [["" (fn [req] :bar)] + (not-found "nothing else under bar!")]] + (not-found "nothing else under root!")))] + (is (= :bar (handler {:uri "/bar"}))) + (is (= {:status 404 + :body "nothing else under bar!"} + (select-keys + (handler {:uri "/bar/baz"}) + [:status :body]))) + (is (= {:status 404 + :body "nothing else under root!"} + (select-keys + (handler {:uri "/yo/mang"}) + [:status :body])))))) + +(deftest regex-test + (let [handler (routes->handler + ["/foo" [[["/boo/" [#".*" :rest]] + (fn [req] (:rest (:route-params req)))]]])] + (is (= "hi/there" + (handler {:uri "/foo/boo/hi/there"}))))) + + + +