From 2b6625b22a32abbc81f0649ccab205c42fbc5402 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Sun, 27 Nov 2016 20:19:44 +0400 Subject: [PATCH] new (#1) 0.2.0-alpha --- .travis.yml | 1 + project.clj | 16 +- src/darkleaf/router.clj | 214 --------------------- src/darkleaf/router.cljc | 243 ++++++++++++++++++++++++ src/darkleaf/router/low_level.clj | 102 ---------- test/darkleaf/router/low_level_test.clj | 64 ------- test/darkleaf/router_test.clj | 191 ------------------- test/darkleaf/router_test.cljc | 168 ++++++++++++++++ test/darkleaf/test_runner.cljs | 5 + 9 files changed, 429 insertions(+), 575 deletions(-) delete mode 100644 src/darkleaf/router.clj create mode 100644 src/darkleaf/router.cljc delete mode 100644 src/darkleaf/router/low_level.clj delete mode 100644 test/darkleaf/router/low_level_test.clj delete mode 100644 test/darkleaf/router_test.clj create mode 100644 test/darkleaf/router_test.cljc create mode 100644 test/darkleaf/test_runner.cljs diff --git a/.travis.yml b/.travis.yml index 0791305..6379ef3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,2 @@ language: clojure +script: lein do test, doo node test once diff --git a/project.clj b/project.clj index cd09cd0..46090f0 100644 --- a/project.clj +++ b/project.clj @@ -1,8 +1,16 @@ -(defproject darkleaf/router "0.1.0-SNAPSHOT" +(defproject darkleaf/router "0.2.0-alpha" :description "Bidirectional Ring router. REST oriented." :url "https://github.com/darkleaf/router" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} - :dependencies [[org.clojure/clojure "1.8.0"] - [org.clojure/core.match "0.3.0-alpha4"] - [backtick "0.3.3"]]) + :dependencies [[org.clojure/clojure "1.8.0" :scope "provided"] + [org.clojure/clojurescript "1.9.293" :scope "provided"]] + :plugins [[lein-cljsbuild "1.1.4"] + [lein-doo "0.1.7"]] + :cljsbuild {:builds [{:id "test" + :source-paths ["test" "src"] + :compiler {:optimizations :none + :target :nodejs + :output-to "target/testable.js" + :output-dir "target" + :main darkleaf.test-runner}}]}) diff --git a/src/darkleaf/router.clj b/src/darkleaf/router.clj deleted file mode 100644 index 67f4118..0000000 --- a/src/darkleaf/router.clj +++ /dev/null @@ -1,214 +0,0 @@ -(ns darkleaf.router - (:require [darkleaf.router.low-level :refer :all, :as ll]) - (:require [clojure.string :refer [split join]])) - -(defn build-routes [& routes] - (combine-routes routes)) - -(defn action - ([action-name handler] - (action :get action-name handler)) - ([request-method action-name handler] - (route action-name - :pattern {:request-method request-method, ::ll/segments [(name action-name)]} - :template {:request-method request-method, ::ll/segments [(name action-name)]} - :handler handler))) - -(defn wildcard [request-method handler] - (route :wildcard - :vars '#{wildcard} - :pattern {:request-method request-method, ::ll/segments '[& wildcard]} - :template {:request-method request-method, ::ll/segments '[~@wildcard]} - :handler handler)) - -(defn root [handler] - (route :root - :pattern {:request-method :get, ::ll/segments []} - :template {:request-method :get, ::ll/segments []} - :handler handler)) - -(defn not-found [handler] - (route :not-found - :vars '#{requested-segments} - :pattern {::ll/segments '[& requested-segments]} - :template {::ll/segments '[~@requested-segments]} - :handler handler)) - -(defn section [s-name & routes] - (scope s-name - {:pattern {::ll/segments [(name s-name)]} - :template {::ll/segments [(name s-name)]}} - routes)) - -(defn guard [g-name predicate & routes] - (let [name-symbol (-> g-name name symbol)] - (scope g-name - {:vars #{name-symbol} - :pattern {::ll/segments [(list name-symbol :guard predicate)]} - :template {::ll/segments [(list 'clojure.core/unquote name-symbol)]}} - routes))) - -(defn wrap-handler [middleware & routes] - (map - #(update % :handler middleware) - (flatten routes))) - -(defn- collection-routes [rs-name controller additional-routes] - (wrap-handler - (get controller :middleware identity) - (scope rs-name - {:pattern {::ll/segments [(name rs-name)]} - :template {::ll/segments [(name rs-name)]}} - (cond-> [] - (contains? controller :index) - (conj (route :index - :pattern {:request-method :get, ::ll/segments []} - :template {:request-method :get, ::ll/segments []} - :handler (:index controller))) - - (contains? controller :new) - (conj (route :new - :pattern {:request-method :get, ::ll/segments ["new"]} - :template {:request-method :get, ::ll/segments ["new"]} - :handler (:new controller))) - - (contains? controller :create) - (conj (route :create - :pattern {:request-method :post, ::ll/segments []} - :template {:request-method :post, ::ll/segments []} - :handler (:create controller)))) - additional-routes))) - -(defn- member-routes [rs-name id-symbol controller additional-routes] - (wrap-handler - (comp (get controller :member-middleware identity) - (get controller :middleware identity)) - (scope rs-name - {:vars #{id-symbol} - :pattern {::ll/segments [(name rs-name) id-symbol]} - :template {::ll/segments [(name rs-name) (list 'clojure.core/unquote id-symbol)]}} - (cond-> [] - (contains? controller :show) - (conj (route :show - :pattern {:request-method :get, ::ll/segments []} - :template {:request-method :get, ::ll/segments []} - :handler (:show controller))) - - (contains? controller :edit) - (conj (route :edit - :pattern {:request-method :get, ::ll/segments ["edit"]} - :template {:request-method :get, ::ll/segments ["edit"]} - :handler (:edit controller))) - - (contains? controller :update) - (conj (route :update - :pattern {:request-method :patch, ::ll/segments []} - :template {:request-method :patch, ::ll/segments []} - :handler (:update controller))) - - (contains? controller :destroy) - (conj (route :destroy - :pattern {:request-method :delete, ::ll/segments []} - :template {:request-method :delete, ::ll/segments []} - :handler (:destroy controller)))) - additional-routes))) - -(defn resources [rs-name id-symbol controller - & {collection-rs :collection, member-rs :member - :or {collection-rs [], member-rs []}}] - [(collection-routes rs-name controller collection-rs) - (member-routes rs-name id-symbol controller member-rs)]) - -(defn resource [r-name controller & inner-routes] - (wrap-handler - (get controller :middleware identity) - (scope r-name - {:pattern {::ll/segments [(name r-name)]} - :template {::ll/segments [(name r-name)]}} - (cond-> '() - (contains? controller :new) - (conj (route :new - :pattern {:request-method :get, ::ll/segments ["new"]} - :template {:request-method :get, ::ll/segments ["new"]} - :handler (:new controller))) - - (contains? controller :create) - (conj (route :create - :pattern {:request-method :post} - :template {:request-method :post} - :handler (:create controller))) - - (contains? controller :show) - (conj (route :show - :pattern '{:request-method :get} - :template '{:request-method :get} - :handler (:show controller))) - - (contains? controller :edit) - (conj (route :edit - :pattern '{:request-method :get, ::ll/segments ["edit"]} - :template '{:request-method :get, ::ll/segments ["edit"]} - :handler (:edit controller))) - - (contains? controller :update) - (conj (route :update - :pattern '{:request-method :patch} - :template '{:request-method :patch} - :handler (:update controller))) - - (contains? controller :destroy) - (conj (route :destroy - :pattern '{:request-method :delete} - :template '{:request-method :delete} - :handler (:destroy controller)))) - inner-routes))) - -;; ---------- builders ---------- - -(defn- request-before-match [req] - (-> req - (assoc ::ll/segments (vec (rest (split (:uri req) #"/")))))) - -(defn- request-after-match [req & {:as stuff}] - (-> req - (dissoc ::ll/segments) - (merge stuff))) - -(defn matcher->handler [matcher] - (fn [original-req] - (let [req (request-before-match original-req) - [route params] (matcher req) - route-handler (:handler route) - result-req (request-after-match req - :route-params params - :matched-route route)] - (route-handler result-req)))) - -(defmacro build-handler [routes-var-name] - `(let [matcher# (build-matcher ~routes-var-name)] - (matcher->handler matcher#))) - -(defn- prepare-reverse-request [req] - (-> req - (assoc :uri (str "/" (join "/" (::ll/segments req)))) - (dissoc ::ll/segments))) - -(defn matchers->request-for [matcher reverse-matcher] - (letfn [(request-for - ([r-name r-scope] (request-for r-name r-scope {})) - ([r-name r-scope r-params] - (let [raw-request (reverse-matcher r-name r-scope r-params) - [matched-route _] (matcher raw-request) - request (prepare-reverse-request raw-request)] - (if (and (= r-name (:name matched-route)) - (= r-scope (:scope matched-route))) - request - (throw (java.lang.IllegalArgumentException. - (str "Can't match the same route for given params. " - "Matched " (:name matched-route) " in scope " (:scope matched-route) ".")))))))] - request-for)) - -(defmacro build-request-for [routes-var-name] - `(let [matcher# (build-matcher ~routes-var-name) - reverse-matcher# (build-reverse-matcher ~routes-var-name)] - (matchers->request-for matcher# reverse-matcher#))) diff --git a/src/darkleaf/router.cljc b/src/darkleaf/router.cljc new file mode 100644 index 0000000..724b753 --- /dev/null +++ b/src/darkleaf/router.cljc @@ -0,0 +1,243 @@ +(ns darkleaf.router + (:require [clojure.string :refer [split join]])) + +;; ~~~~~~~~~~ Model ~~~~~~~~~~ + +(defprotocol Item + (handle [this req]) + (fill [this req-template])) + +(defn some-handle [req xs] + (some #(handle % req) xs)) + +(defn some-fill [req xs] + (some #(fill % req) xs)) + +(defrecord Composite [children] + Item + (handle [_ req] + (some-handle req children)) + (fill [_ req] + (some-fill req children))) + +(defn- composite [& children] + (Composite. children)) + +(defrecord Scope [id handle-impl fill-impl children] + Item + (handle [_ req] + (some-> req + (handle-impl) + (update ::scope conj id) + (some-handle children))) + (fill [_ req] + (when (= id (peek (::scope req))) + (-> req + (update ::scope pop) + (fill-impl) + (some-fill children))))) + +(defn- scope [id handle-impl fill-impl children] + (let [children (remove nil? children)] + (Scope. id handle-impl fill-impl children))) + +(defrecord Action [id handle-impl fill-impl handler] + Item + (handle [_ req] + (some-> req + (handle-impl) + (dissoc ::segments) + (assoc ::action id) + (handler))) + (fill [_ req] + (when (= id (::action req)) + (-> req + (fill-impl))))) + +(defn- action + ([id request-method handler] + (let [handle-impl (fn [req] + (when (and (= request-method (:request-method req)) + (empty? (::segments req))) + req)) + fill-impl (fn [req] + (-> req + (assoc :request-method request-method)))] + (Action. id handle-impl fill-impl handler))) + ([id request-method segment handler] + (let [handle-impl (fn [req] + (when (and (= request-method (:request-method req)) + (= [segment] (::segments req))) + req)) + fill-impl (fn [req] + (-> req + (assoc :request-method request-method) + (update ::segments conj segment)))] + (Action. id handle-impl fill-impl handler)))) + +;; ~~~~~~~~~~~ Scopes ~~~~~~~~~~ + +(defn section [id & children] + {:pre [(keyword? id) + (every? #(or (nil? %) (satisfies? Item %)) children)]} + (let [segment (name id) + handle-impl (fn [req] + (when (= segment (peek (::segments req))) + (update req ::segments pop))) + fill-impl (fn [req] + (update req ::segments conj segment))] + (scope id handle-impl fill-impl children))) + +;; ~~~~~~~~~~ Resources ~~~~~~~~~~ + +(defn- resources-collection-scope [scope-id segment & children] + (let [handle-impl (if segment + (fn [req] + (when (= segment (peek (::segments req))) + (update req ::segments pop))) + identity) + fill-impl (if segment + (fn [req] + (update req ::segments conj segment)) + identity)] + (scope scope-id handle-impl fill-impl children))) + +(defn- resources-member-scope [plural-name singular-name segment & children] + (let [id-key (keyword (str (name singular-name) "-id")) + handle-impl (if segment + (fn [req] + (let [segments (::segments req) + given-segment (peek segments) + + segments (pop segments) + given-id (peek segments) + + segments (pop segments)] + (when (and (= segment given-segment) + (some? given-id)) + (-> req + (assoc ::segments segments) + (assoc-in [::params id-key] given-id))))) + (fn [req] + (let [segments (::segments req) + given-id (peek segments) + segments (pop segments)] + (when (some? given-id) + (-> req + (assoc ::segments segments) + (assoc-in [::params id-key] given-id)))))) + fill-impl (if segment + (fn [req] + (let [id (get-in req [::params id-key])] + (update req ::segments conj segment id))) + (fn [req] + (let [id (get-in req [::params id-key])] + (update req ::segments conj id))))] + (scope singular-name handle-impl fill-impl children))) + +(defn resources [plural-name singular-name controller + & {:keys [segment nested] + :or {segment (name plural-name) + nested []}}] + (let [index-action (when-let [handler (:index controller)] + (action :index :get handler)) + new-action (when-let [handler (:new controller)] + (action :new :get "new" handler)) + create-action (when-let [handler (:create controller)] + (action :create :post handler)) + show-action (when-let [handler (:show controller)] + (action :show :get handler)) + edit-action (when-let [handler (:edit controller)] + (action :edit :get "edit" handler)) + update-action (when-let [handler (:update controller)] + (action :update :patch handler)) + put-action (when-let [handler (:put controller)] + (action :put :put handler)) + destroy-action (when-let [handler (:destroy controller)] + (action :destroy :delete handler))] + (composite + (resources-collection-scope plural-name segment + index-action) + (resources-collection-scope singular-name segment + new-action + create-action) + (apply resources-member-scope plural-name singular-name segment + (into nested + [show-action + edit-action + update-action + put-action + destroy-action]))))) + +;; ~~~~~~~~~~ Resource ~~~~~~~~~~ + +(defn- resource-scope [scope-id segment & children] + (let [handle-impl (if segment + (fn [req] + (when (= segment (peek (::segments req))) + (update req ::segments pop))) + identity) + fill-impl (if segment + (fn [req] + (update req ::segments conj segment)) + identity)] + (scope scope-id handle-impl fill-impl children))) + +(defn resource [singular-name controller & {:keys [segment nested] + :or {segment (name singular-name) + nested []}}] + (let [new-action (when-let [handler (:new controller)] + (action :new :get "new" handler)) + create-action (when-let [handler (:create controller)] + (action :create :post handler)) + show-action (when-let [handler (:show controller)] + (action :show :get handler)) + edit-action (when-let [handler (:edit controller)] + (action :edit :get "edit" handler)) + update-action (when-let [handler (:update controller)] + (action :update :patch handler)) + put-action (when-let [handler (:put controller)] + (action :put :put handler)) + destroy-action (when-let [handler (:destroy controller)] + (action :destroy :delete handler))] + (apply resource-scope singular-name segment + new-action + create-action + show-action + edit-action + update-action + put-action + destroy-action + nested))) + +;; ~~~~~~~~~~ Helpers ~~~~~~~~~~ + +(def ^:private empty-segments #?(:clj clojure.lang.PersistentQueue/EMPTY + :cljs cljs.core/PersistentQueue.EMPTY)) +(def ^:private empty-scope #?(:clj clojure.lang.PersistentQueue/EMPTY + :cljs cljs.core/PersistentQueue.EMPTY)) + +(defn- uri->segments [uri] + (into empty-segments + (map second (re-seq #"/([^/]+)" uri)))) + +(defn- segments->uri [segments] + (->> segments + (map #(str "/" %)) + (join))) + +(defn make-handler [item] + (fn [req] + (as-> req r + (assoc r ::scope empty-scope) + (assoc r ::params {}) + (assoc r ::segments (uri->segments (:uri r))) + (handle item r)))) + +(defn make-request-for [item] + (fn [action scope params] + (let [scope (into empty-scope scope)] + (as-> {::action action, ::scope scope, ::params params, ::segments empty-segments} r + (fill item r) + (assoc r :uri (segments->uri (::segments r))) + (dissoc r ::action ::scope ::params ::segments))))) diff --git a/src/darkleaf/router/low_level.clj b/src/darkleaf/router/low_level.clj deleted file mode 100644 index 927e257..0000000 --- a/src/darkleaf/router/low_level.clj +++ /dev/null @@ -1,102 +0,0 @@ -(ns darkleaf.router.low-level - (:require [clojure.core.match :refer [match]] - [backtick :refer [syntax-quote-fn]])) - -(defrecord Route [name - vars pattern template - handler scope]) - -(defn route - [name & {:keys [vars pattern template handler] - :or {vars #{}}}] - {:pre [(keyword? name) - (set? vars) - (map? pattern) - (map? template) - (ifn? handler)]} - (Route. name - vars pattern template - handler '())) - -(defn- merge-segments-shapes [parent-segments child-segments] - (into parent-segments child-segments)) - -(defn- merge-map-shapes [parent-map-pattern child-map-pattern] - (reduce-kv (fn [result k v] - (assoc result k - (cond - (and (map? (k result)) (map? v)) (merge-map-shapes (k result) v) - (not (contains? result k)) v - :else (throw (java.lang.IllegalArgumentException. "can't merge patterns: parent conrain child key"))))) - parent-map-pattern - child-map-pattern)) - -(defn- merge-request-shapes [parent child] - (let [parent-segments (get parent ::segments []) - child-segments (get child ::segments []) - - parent-map (dissoc parent ::segments) - child-map (dissoc child ::segments) - - result-segments (merge-segments-shapes parent-segments child-segments) - result-map-pattern (merge-map-shapes parent-map child-map)] - (assoc result-map-pattern ::segments result-segments))) - -(defn scope [s-name - {:keys [vars pattern template] - :or {vars #{}}} - & routes] - {:pre [(keyword? s-name) - (set? vars) - (map? pattern) - (map? template)]} - (map - (fn [route] - (-> route - (update :scope conj s-name) - (update :vars into vars) - (update :pattern #(merge-request-shapes pattern %)) - (update :template #(merge-request-shapes template %)))) - (flatten routes))) - -(defn- build-params-map [route] - (let [symbols (:vars route) - m-keys (vec (map keyword symbols)) - m-vals (vec symbols)] - `(zipmap ~m-keys ~m-vals))) - -(defn- build-pattern-row [route] - [(:pattern route)]) - -(defn- route->match-clause [idx route routes-var-name] - (let [pattern-row (build-pattern-row route) - action `[(nth ~routes-var-name ~idx) ~(build-params-map route)]] - (list pattern-row action))) - -(defmacro build-matcher [routes-var-name] - `(fn [req#] - (match - [req#] - ~@(let [routes (var-get (resolve routes-var-name))] - (apply concat - (map-indexed - (fn [idx route] (route->match-clause idx route routes-var-name)) - routes)))))) - -(defmacro build-reverse-matcher [routes-var-name] - (let [r-params-symbol (gensym 'r-params)] - `(fn [r-name# r-scope# ~r-params-symbol] - (case [r-name# r-scope#] - ~@(let [routes (var-get (resolve routes-var-name))] - (mapcat - (fn [route] - (list - [(:name route) (:scope route)] - `(let [{:keys [~@(:vars route)]} ~r-params-symbol] - ~(syntax-quote-fn (:template route))))) - routes)))))) - -(defn combine-routes [& routes] - (-> routes - flatten - vec)) diff --git a/test/darkleaf/router/low_level_test.clj b/test/darkleaf/router/low_level_test.clj deleted file mode 100644 index 41ce046..0000000 --- a/test/darkleaf/router/low_level_test.clj +++ /dev/null @@ -1,64 +0,0 @@ -(ns darkleaf.router.low-level-test - (:require [clojure.test :refer :all] - [clojure.template :refer [do-template]] - [darkleaf.router.low-level :refer :all, :as ll])) - -(def routes - (combine-routes - (route :main-page - :pattern '{::ll/segments [], :request-method :get} - :template '{::ll/segments [], :request-method :get} - :handler identity) - (route :legacy-page - :vars '#{slug} - :pattern '{::ll/segments ["pages" (slug :guard #{"old-page"})], :request-method :get} - :template '{::ll/segments ["pages" ~slug], :request-method :get} - :handler identity) - (route :page - :vars '#{slug} - :pattern '{::ll/segments ["pages" slug], :request-method :get} - :template '{::ll/segments ["pages" ~slug], :request-method :get} - :handler identity) - (scope :api - {:vars '#{api-token} - :pattern '{::ll/segments ["api"], :headers {"token" api-token}} - :template '{::ll/segments ["api"], :headers {"token" ~api-token}}} - (route :create-page - :pattern '{::ll/segments ["pages"], :request-method :post} - :template '{::ll/segments ["pages"], :request-method :post} - :handler identity) - (route :update-page - :vars '#{slug} - :pattern '{::ll/segments ["pages" slug], :request-method :patch} - :template '{::ll/segments ["pages" ~slug], :request-method :patch} - :handler identity)) - (route :not-found - :pattern {} - :template {} - :handler identity))) - -(deftest bidirectional-matching - (let [matcher (build-matcher routes) - reverse-matcher (build-reverse-matcher routes)] - (do-template [req-name req-scope req-params request] - (testing req-name - (is (= (reverse-matcher req-name req-scope req-params) - request)) - (is (= (let [[matched-route route-params] (matcher request)] - [(:name matched-route) (:scope matched-route) route-params] - [req-name req-scope req-params])))) - - :main-page [] {} - {::ll/segments [], :request-method :get} - - :page [] {:slug "about"} - {::ll/segments ["pages" "about"], :request-method :get} - - :legacy-page [] {:slug "old-page"} - {::ll/segments ["pages" "old-page"], :request-method :get} - - :create-page [:api] {:api-token "secret"} - {::ll/segments ["api" "pages"], :request-method :post, :headers {"token" "secret"}} - - :update-page [:api] {:slug "contacts", :api-token "secret"} - {::ll/segments ["api" "pages" "contacts"], :request-method :patch, :headers {"token" "secret"}}))) diff --git a/test/darkleaf/router_test.clj b/test/darkleaf/router_test.clj deleted file mode 100644 index d408183..0000000 --- a/test/darkleaf/router_test.clj +++ /dev/null @@ -1,191 +0,0 @@ -(ns darkleaf.router-test - (:require [clojure.test :refer :all] - [clojure.template :refer [do-template]] - [darkleaf.router :refer :all])) - -(def routes - (build-routes - (root identity) - (action :get :about identity) - (section :taxonomy - (wildcard :get identity)) - (resources :pages 'page-id {:index identity - :new identity - :create identity - :show identity - :edit identity - :update identity - :destroy identity} - :collection - [(action :archived identity)] - :member - [(resources :comments 'comment-id {:index identity})]) - (resource :account {:new identity - :create identity - :show identity - :edit identity - :update identity - :destroy identity} - (resources :pages 'page-id {:index identity})) - (guard :locale #{"ru" "en"} - (action :localized-page identity) - (not-found identity)) - (not-found identity))) - -(deftest test-routes - (let [handler (build-handler routes) - request-for (build-request-for routes)] - (do-template [req-name req-scope req-params request] - (testing (:uri request) - (testing "direct" - (let [response (handler request)] - (is (= req-name (get-in response [:matched-route :name]))) - (is (= req-params (:route-params response))))) - (testing "reverse" - (let [computed-request (request-for req-name req-scope req-params)] - (is (= request computed-request))))) - :root [] {} - {:uri "/", :request-method :get} - - :about [] {} - {:uri "/about", :request-method :get} - - :wildcard [:taxonomy] {:wildcard ["animal" "cat"]} - {:uri "/taxonomy/animal/cat", :request-method :get} - - ;; pages routes - :index [:pages] {} - {:uri "/pages", :request-method :get} - - :new [:pages] {} - {:uri "/pages/new", :request-method :get} - - :create [:pages] {} - {:uri "/pages", :request-method :post} - - :show [:pages] {:page-id "some-id"} - {:uri "/pages/some-id", :request-method :get} - - :edit [:pages] {:page-id "about"} - {:uri "/pages/about/edit", :request-method :get} - - :update [:pages] {:page-id "contacts"} - {:uri "/pages/contacts", :request-method :patch} - - :destroy [:pages] {:page-id "wrong"} - {:uri "/pages/wrong", :request-method :delete} - - ;; pages collection routes - :archived [:pages] {} - {:uri "/pages/archived", :request-method :get} - - ;; pages member routes - :index [:pages :comments] {:page-id "some-id"} - {:uri "/pages/some-id/comments", :request-method :get} - - ;; account routes - :new [:account] {} - {:uri "/account/new", :request-method :get} - - :create [:account] {} - {:uri "/account", :request-method :post} - - :show [:account] {} - {:uri "/account", :request-method :get} - - :edit [:account] {} - {:uri "/account/edit", :request-method :get} - - :update [:account] {} - {:uri "/account", :request-method :patch} - - :destroy [:account] {} - {:uri "/account", :request-method :delete} - - ;; inner account routes - :index [:account :pages] {} - {:uri "/account/pages", :request-method :get} - - ;; guard locale - :localized-page [:locale] {:locale "en"} - {:uri "/en/localized-page", :request-method :get} - - :localized-page [:locale] {:locale "ru"} - {:uri "/ru/localized-page", :request-method :get} - - ;; not found - :not-found [] {:requested-segments ["not-found" "page"]} - {:uri "/not-found/page"} - - :not-found [:locale] {:requested-segments ["wrong" "path"], :locale "en"} - {:uri "/en/wrong/path"}) - (testing "wrong guard cases" - (let [request {:uri "/it/localized-page", :request-method :get} - response (handler request)] - (is (not= :localized-page (get-in response [:matched-route :name])))) - (is (thrown-with-msg? java.lang.IllegalArgumentException - #"Can't match the same route for given params\. Matched :not-found in scope \(\)\." - (request-for :localized-page [:locale] {:locale "wrong-locale"})))))) - -;; ---------- wrap-handler testing ---------- - -(defn find-page [slug] - (get - {"about" {:id 1, :slug "about"} - "contacts" {:id 2, :slug "contacts"}} - slug)) - -(defn find-page-middleware [handler] - (fn [req] - (-> req - (assoc-in [:models :page] (find-page (get-in req [:route-params :page-slug]))) - handler))) - -(defn test-middleware [handler] - (fn [req] - (-> req - (assoc :test-key :test-value) - handler))) - -(def routes-with-middleware - (build-routes - (wrap-handler test-middleware - (action :some-action identity)) - (resources :pages 'page-slug {:middleware test-middleware - :member-middleware find-page-middleware - :index identity - :show identity} - :member - [(action :member-action identity)] - :collection - [(action :collection-action identity)]) - (resource :account {:middleware test-middleware - :show identity} - (action :additional-action identity)))) - -(deftest test-routes-with-middleware - (let [handler (build-handler routes-with-middleware)] - (testing "wrap" - (let [request {:uri "/some-action", :request-method :get} - response (handler request)] - (is (= :test-value (:test-key response))))) - (testing "resoure(s) middleware" - (do-template [request] - (testing request - (let [response (handler request)] - (is (= :test-value (:test-key response))))) - {:uri "/pages/about", :request-method :get} - {:uri "/pages/contacts/member-action", :request-method :get} - {:uri "/pages", :request-method :get} - {:uri "/pages/collection-action", :request-method :get} - - {:uri "/account", :request-method :get} - {:uri "/account/additional-action", :request-method :get})) - - (testing "resources member middleware" - (do-template [request model] - (testing request - (let [response (handler request)] - (is (= model (get-in response [:models :page]))))) - {:uri "/pages/about", :request-method :get} {:id 1, :slug "about"} - {:uri "/pages/contacts/member-action", :request-method :get} {:id 2, :slug "contacts"})))) diff --git a/test/darkleaf/router_test.cljc b/test/darkleaf/router_test.cljc new file mode 100644 index 0000000..a9ad684 --- /dev/null +++ b/test/darkleaf/router_test.cljc @@ -0,0 +1,168 @@ +(ns darkleaf.router-test + (:require [clojure.test :refer [deftest testing is]] + [darkleaf.router :as r])) + +(defn route-testing [routes action-id scope params request] + (testing (str "action " action-id " " + "in scope " scope " " + "with params " params) + (testing "direct matching" + (let [handler (r/make-handler routes) + response (handler request)] + (is (= action-id (::r/action response))) + (is (= scope (::r/scope response))) + (is (= params (::r/params response))))) + (testing "reverse matching" + (let [request-for (r/make-request-for routes) + calculated-request (request-for action-id scope params)] + (is (= request calculated-request)))))) + +;; ~~~~~~~~~~ Resources ~~~~~~~~~~ + +(deftest resources + (let [pages-controller {:index (fn [req] req) + :show (fn [req] req) + :new (fn [req] req) + :create (fn [req] req) + :edit (fn [req] req) + :update (fn [req] req) + :put (fn [req] req) + :destroy (fn [req] req)} + pages (r/resources :pages :page pages-controller) + pages-testing (partial route-testing pages)] + (pages-testing :index [:pages] {} + {:uri "/pages", :request-method :get}) + (pages-testing :new [:page] {} + {:uri "/pages/new", :request-method :get}) + (pages-testing :create [:page] {} + {:uri "/pages", :request-method :post}) + (pages-testing :show [:page] {:page-id "some-id"} + {:uri "/pages/some-id", :request-method :get}) + (pages-testing :edit [:page] {:page-id "some-id"} + {:uri "/pages/some-id/edit", :request-method :get}) + (pages-testing :update [:page] {:page-id "some-id"} + {:uri "/pages/some-id", :request-method :patch}) + (pages-testing :put [:page] {:page-id "some-id"} + {:uri "/pages/some-id", :request-method :put}) + (pages-testing :destroy [:page] {:page-id "some-id"} + {:uri "/pages/some-id", :request-method :delete}))) + +(deftest resources-without-segment + (let [pages-controller {:index (fn [req] req) + :show (fn [req] req) + :new (fn [req] req) + :create (fn [req] req) + :edit (fn [req] req) + :update (fn [req] req) + :put (fn [req] req) + :destroy (fn [req] req)} + pages (r/resources :pages :page pages-controller + :segment false) + pages-testing (partial route-testing pages)] + (pages-testing :index [:pages] {} + {:uri "", :request-method :get}) + (pages-testing :new [:page] {} + {:uri "/new", :request-method :get}) + (pages-testing :create [:page] {} + {:uri "", :request-method :post}) + (pages-testing :show [:page] {:page-id "some-id"} + {:uri "/some-id", :request-method :get}) + (pages-testing :edit [:page] {:page-id "some-id"} + {:uri "/some-id/edit", :request-method :get}) + (pages-testing :update [:page] {:page-id "some-id"} + {:uri "/some-id", :request-method :patch}) + (pages-testing :put [:page] {:page-id "some-id"} + {:uri "/some-id", :request-method :put}) + (pages-testing :destroy [:page] {:page-id "some-id"} + {:uri "/some-id", :request-method :delete}))) + +;; ~~~~~~~~~~ Resource ~~~~~~~~~~ + +(deftest resource + (let [star-controller {:show (fn [req] req) + :new (fn [req] req) + :create (fn [req] req) + :edit (fn [req] req) + :update (fn [req] req) + :put (fn [req] req) + :destroy (fn [req] req)} + star (r/resource :star star-controller) + star-testing (partial route-testing star)] + (star-testing :new [:star] {} + {:uri "/star/new", :request-method :get}) + (star-testing :create [:star] {} + {:uri "/star", :request-method :post}) + (star-testing :show [:star] {} + {:uri "/star", :request-method :get}) + (star-testing :edit [:star] {} + {:uri "/star/edit", :request-method :get}) + (star-testing :update [:star] {} + {:uri "/star", :request-method :patch}) + (star-testing :put [:star] {} + {:uri "/star", :request-method :put}) + (star-testing :destroy [:star] {} + {:uri "/star", :request-method :delete}))) + +(deftest resource-wihout-segment + (let [star-controller {:show (fn [req] req) + :new (fn [req] req) + :create (fn [req] req) + :edit (fn [req] req) + :update (fn [req] req) + :put (fn [req] req) + :destroy (fn [req] req)} + star (r/resource :star star-controller + :segment false) + star-testing (partial route-testing star)] + (star-testing :new [:star] {} + {:uri "/new", :request-method :get}) + (star-testing :create [:star] {} + {:uri "", :request-method :post}) + (star-testing :show [:star] {} + {:uri "", :request-method :get}) + (star-testing :edit [:star] {} + {:uri "/edit", :request-method :get}) + (star-testing :update [:star] {} + {:uri "", :request-method :patch}) + (star-testing :put [:star] {} + {:uri "", :request-method :put}) + (star-testing :destroy [:star] {} + {:uri "", :request-method :delete}))) + +;; ~~~~~~~~~~ Nested ~~~~~~~~~~ + +(deftest resources-with-nested + (let [comments-controller {:show (fn [req] req)} + comments (r/resources :comments :comment comments-controller) + + pages-controller {} + pages (r/resources :pages :page pages-controller + :nested [comments]) + pages-testing (partial route-testing pages)] + (pages-testing :show [:page :comment] {:page-id "some-page-id" + :comment-id "some-comment-id"} + {:uri "/pages/some-page-id/comments/some-comment-id" + :request-method :get}))) + +(deftest resource-with-nested + (let [comments-controller {:show (fn [req] req)} + comments (r/resources :comments :comment comments-controller) + + star-controller {} + star (r/resource :star star-controller + :nested [comments]) + star-testing (partial route-testing star)] + (star-testing :show [:star :comment] {:comment-id "some-comment-id"} + {:uri "/star/comments/some-comment-id" + :request-method :get}))) + +;; ~~~~~~~~~~ Scopes ~~~~~~~~~~ + +(deftest section + (let [pages-controller {:index (fn [req] req)} + pages (r/resources :pages :page pages-controller) + admin (r/section :admin + pages) + admin-testing (partial route-testing admin)] + (admin-testing :index [:admin :pages] {} + {:uri "/admin/pages", :request-method :get}))) diff --git a/test/darkleaf/test_runner.cljs b/test/darkleaf/test_runner.cljs new file mode 100644 index 0000000..3045207 --- /dev/null +++ b/test/darkleaf/test_runner.cljs @@ -0,0 +1,5 @@ +(ns darkleaf.test-runner + (:require [doo.runner :refer-macros [doo-tests]] + [darkleaf.router-test])) + +(doo-tests 'darkleaf.router-test)