├── Dockerfile ├── .travis.yml ├── doc └── intro.md ├── .gitignore ├── src └── darkleaf │ ├── router │ ├── item.cljc │ ├── keywords.cljc │ ├── url.cljc │ ├── group_impl.cljc │ ├── args.cljc │ ├── section_impl.cljc │ ├── html │ │ └── method_override.cljc │ ├── mount_impl.cljc │ ├── action.cljc │ ├── resource_impl.cljc │ ├── guard_impl.cljc │ ├── pass_impl.cljc │ ├── item_wrappers.cljc │ ├── helpers.cljc │ └── resources_impl.cljc │ └── router.cljc ├── docker-compose.yml ├── test └── darkleaf │ ├── router │ ├── args_test.cljc │ ├── async_test.cljs │ ├── additional_request_keys_test.cljc │ ├── async_test.clj │ ├── html │ │ └── method_override_test.cljc │ ├── group_test.cljc │ ├── section_test.cljc │ ├── guard_test.cljc │ ├── controller_test.cljc │ ├── use_cases │ │ ├── resource_composition_test.cljc │ │ ├── member_middleware_test.cljc │ │ └── domain_constraint_test.cljc │ ├── test_helpers.cljc │ ├── pass_test.cljc │ ├── mount_test.cljc │ ├── resource_test.cljc │ └── resources_test.cljc │ └── test_runner.cljs ├── CHANGELOG.md ├── project.clj ├── LICENSE └── README.md /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM clojure:lein-2.7.1-alpine-onbuild 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | lein: 2.6.1 3 | script: lein do test, doo node test once, doo node test-advanced once 4 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to router 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /src/darkleaf/router/item.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.item 2 | (:require [darkleaf.router.keywords :as k])) 3 | 4 | (defprotocol Item 5 | (process [this req] 6 | "return [handler req]") 7 | (fill [this req-template] 8 | "return req") 9 | (explain [this init] 10 | "return [explanation]")) 11 | -------------------------------------------------------------------------------- /src/darkleaf/router/keywords.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.keywords) 2 | 3 | (def request-for :darkleaf.router/request-for) 4 | (def scope :darkleaf.router/scope) 5 | (def params :darkleaf.router/params) 6 | (def action :darkleaf.router/action) 7 | (def segments :darkleaf.router/segments) 8 | (def middlewares :darkleaf.router/middlewares) 9 | -------------------------------------------------------------------------------- /src/darkleaf/router/url.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.url 2 | (:require [clojure.string :as str]) 3 | #?(:clj (:import (java.net URLEncoder)))) 4 | 5 | (defn- encode-impl [string] 6 | #?(:clj (URLEncoder/encode string "UTF-8") 7 | :cljc (js/encodeURIComponent string))) 8 | 9 | (defn encode [string] 10 | (some-> string 11 | (str) 12 | (encode-impl) 13 | (str/replace "+" "%20"))) 14 | -------------------------------------------------------------------------------- /src/darkleaf/router/group_impl.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.group-impl 2 | (:require [darkleaf.router.args :as args] 3 | [darkleaf.router.item-wrappers :as wrappers])) 4 | 5 | (defn ^{:style/indent :defn} group [& args] 6 | (let [[{:keys [middleware]} 7 | children] 8 | (args/parse 0 args)] 9 | (cond-> (wrappers/composite children) 10 | middleware (wrappers/wrap-middleware middleware)))) 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: . 5 | command: > 6 | lein update-in :plugins into 7 | "[[cider/cider-nrepl \"0.15.0-SNAPSHOT\"] 8 | [refactor-nrepl \"2.3.0-SNAPSHOT\"]]" 9 | -- 10 | repl :headless 11 | environment: 12 | - LEIN_REPL_HOST=0.0.0.0 13 | - LEIN_REPL_PORT=40000 14 | ports: 15 | - "40000:40000" 16 | volumes: 17 | - .:/usr/src/app 18 | -------------------------------------------------------------------------------- /src/darkleaf/router/args.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.args) 2 | 3 | (defn parse [ordinal-args-count xs] 4 | {:pre (< ordinal-args-count (count xs))} 5 | (let [ordinal-args (take ordinal-args-count xs) 6 | xs (drop ordinal-args-count xs)] 7 | (loop [opts {} 8 | xs xs] 9 | (if (keyword? (first xs)) 10 | (do 11 | (assert (>= (count xs) 2)) 12 | (recur (assoc opts (first xs) (second xs)) 13 | (drop 2 xs))) 14 | (-> ordinal-args 15 | (vec) 16 | (conj opts) 17 | (conj xs)))))) 18 | -------------------------------------------------------------------------------- /src/darkleaf/router/section_impl.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.section-impl 2 | (:require [darkleaf.router.args :as args] 3 | [darkleaf.router.item-wrappers :as wrappers])) 4 | 5 | (defn ^{:style/indent :defn} section [& args] 6 | (let [[id 7 | {:keys [middleware segment] 8 | :or {segment (name id)}} 9 | children] 10 | (args/parse 1 args)] 11 | (cond-> (wrappers/composite children) 12 | middleware (wrappers/wrap-middleware middleware) 13 | segment (wrappers/wrap-segment segment) 14 | :always (wrappers/wrap-scope id)))) 15 | -------------------------------------------------------------------------------- /test/darkleaf/router/args_test.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.args-test 2 | (:require [darkleaf.router.args :as sut] 3 | [clojure.test :refer [deftest is]])) 4 | 5 | (deftest parse 6 | (is (= 7 | [{:a 5, :b 6} [7 8 9]] 8 | (sut/parse 0 [:a 5 :b 6 7 8 9]))) 9 | (is (= 10 | [{} [7 8 9]] 11 | (sut/parse 0 [7 8 9]))) 12 | (is (= 13 | [1 2 3 4 {:a 5, :b 6} [7 8 9]] 14 | (sut/parse 4 [1 2 3 4 :a 5 :b 6 7 8 9]))) 15 | (is (= 16 | [1 2 3 4 {} [7 8 9]] 17 | (sut/parse 4 [1 2 3 4 7 8 9]))) 18 | (is (= 19 | [1 2 3 4 {:a 5} []] 20 | (sut/parse 4 [1 2 3 4 :a 5])))) 21 | -------------------------------------------------------------------------------- /test/darkleaf/router/async_test.cljs: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.async-test 2 | (:require [darkleaf.router :as r]) 3 | (:import [goog.async nextTick]) 4 | (:require-macros [cljs.test :refer [deftest is async]])) 5 | 6 | (deftest async-handler 7 | (async done 8 | (let [pages-controller (r/controller 9 | (index [req resp raise] 10 | (nextTick #(resp "index resp")) 11 | :something)) 12 | pages (r/resources :pages :page pages-controller) 13 | handler (r/make-handler pages) 14 | test-req {:uri "/pages", :request-method :get} 15 | check (fn [val] 16 | (is (= "index resp" val)) 17 | (done))] 18 | (handler test-req check check)))) 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [0.1.1] - 2016-09-08 9 | ### Changed 10 | - Documentation on how to make the widgets. 11 | 12 | ### Removed 13 | - `make-widget-sync` - we're all async, all the time. 14 | 15 | ### Fixed 16 | - Fixed widget maker to keep working when daylight savings switches over. 17 | 18 | ## 0.1.0 - 2016-09-08 19 | ### Added 20 | - Files from the new template. 21 | - Widget maker public API - `make-widget-sync`. 22 | 23 | [Unreleased]: https://github.com/your-name/router/compare/0.1.1...HEAD 24 | [0.1.1]: https://github.com/your-name/router/compare/0.1.0...0.1.1 25 | -------------------------------------------------------------------------------- /src/darkleaf/router/html/method_override.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.html.method-override 2 | (:require [clojure.string :refer [lower-case]])) 3 | 4 | (defn- method-override-request [request] 5 | (let [orig-method (:request-method request) 6 | new-method (some-> request 7 | :params 8 | :_method 9 | (lower-case) 10 | (keyword))] 11 | (if (and (= :post orig-method) 12 | (some? new-method)) 13 | (assoc request :request-method new-method) 14 | request))) 15 | 16 | (defn wrap-method-override [handler] 17 | "Use it with ring.middleware.params/wrap-params 18 | and ring.middleware.keyword-params/wrap-keyword-params" 19 | (fn 20 | ([request] 21 | (handler (method-override-request request))) 22 | ([request respond raise] 23 | (handler (method-override-request request) respond raise)))) 24 | -------------------------------------------------------------------------------- /src/darkleaf/router/mount_impl.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.mount-impl 2 | (:require [darkleaf.router.keywords :as k] 3 | [darkleaf.router.item :as i] 4 | [darkleaf.router.item-wrappers :as wrappers])) 5 | 6 | (deftype App [item] 7 | i/Item 8 | (process [_ req] 9 | (as-> req <> 10 | (update <> k/request-for (fn [request-for] 11 | (fn [action scope params] 12 | (request-for action 13 | (into (k/scope req) scope) 14 | (merge (k/params req) params))))) 15 | (update <> k/scope empty) 16 | (i/process item <>))) 17 | (fill [_ req] 18 | (i/fill item req)) 19 | (explain [_ init] 20 | (i/explain item init))) 21 | 22 | (defn mount [item & {:keys [segment middleware]}] 23 | (cond-> (App. item) 24 | middleware (wrappers/wrap-middleware middleware) 25 | segment (wrappers/wrap-segment segment))) 26 | -------------------------------------------------------------------------------- /test/darkleaf/router/additional_request_keys_test.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.additional-request-keys-test 2 | (:require [clojure.test :refer [deftest testing is]] 3 | [darkleaf.router :as r])) 4 | 5 | (deftest request-keys 6 | (let [pages-controller (r/controller 7 | (index [req] "index resp") 8 | (show [req] req)) 9 | pages (r/resources :pages :page pages-controller) 10 | handler (r/make-handler pages) 11 | returned-req (handler {:uri "/pages/1", :request-method :get})] 12 | (testing ::r/request-for 13 | (let [request-for (::r/request-for returned-req)] 14 | (is (= {:uri "/pages", :request-method :get} 15 | (request-for :index [:pages] {}))))) 16 | (testing ::r/action 17 | (is (= :show (::r/action returned-req)))) 18 | (testing ::r/scope 19 | (is (= [:page] (::r/scope returned-req)))) 20 | (testing ::r/params 21 | (is (= {:page "1"} (::r/params returned-req)))))) 22 | -------------------------------------------------------------------------------- /src/darkleaf/router/action.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.action 2 | (:require [clojure.string :refer [join]] 3 | [darkleaf.router.keywords :as k] 4 | [darkleaf.router.item :as i])) 5 | 6 | ;; todo: double from helpers ns 7 | (defn- segments->uri [segments] 8 | (->> segments 9 | (map #(str "/" %)) 10 | (join))) 11 | 12 | (deftype Action [id request-method segments handler] 13 | i/Item 14 | (process [_ req] 15 | (when (and (= request-method (:request-method req)) 16 | (= segments (k/segments req))) 17 | (let [req (-> req 18 | (assoc k/action id) 19 | (dissoc k/segments))] 20 | [handler req]))) 21 | (fill [_ req] 22 | (when (and (= id (k/action req)) 23 | (empty? (k/scope req))) 24 | (-> req 25 | (assoc :request-method request-method) 26 | (update k/segments into segments)))) 27 | (explain [_ init] 28 | [(-> init 29 | (assoc :action id) 30 | (assoc-in [:req :request-method] request-method) 31 | (update-in [:req :uri] str (segments->uri segments)))])) 32 | 33 | (defn action [id request-method segments handler] 34 | (Action. id request-method segments handler)) 35 | -------------------------------------------------------------------------------- /test/darkleaf/router/async_test.clj: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.async-test 2 | (:require [darkleaf.router :as r] 3 | [clojure.test :refer [deftest testing is]])) 4 | 5 | (defn testing-handler [msg handler test-req test-resp] 6 | (testing msg 7 | (let [respond (promise) 8 | exception (promise)] 9 | (handler test-req respond exception) 10 | (is (= test-resp 11 | (deref respond 100 nil))) 12 | (is (not (realized? exception)))))) 13 | 14 | (deftest async-handler 15 | (let [response {:status 200 16 | :headers {} 17 | :body "index resp"} 18 | pages-controller (r/controller 19 | (index [req resp raise] 20 | (future 21 | (resp response)))) 22 | pages (r/resources :pages :page pages-controller) 23 | handler (r/make-handler pages)] 24 | (testing-handler "found" 25 | handler 26 | {:uri "/pages", :request-method :get} 27 | response) 28 | (testing-handler "not found" 29 | handler 30 | {:uri "/wrong/url", :request-method :get} 31 | {:status 404, :headers {}, :body "404 error"}))) 32 | -------------------------------------------------------------------------------- /src/darkleaf/router/resource_impl.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.resource-impl 2 | (:require [darkleaf.router.keywords :as k] 3 | [darkleaf.router.item :as i] 4 | [darkleaf.router.item-wrappers :as wrappers] 5 | [darkleaf.router.action :refer [action]] 6 | [darkleaf.router.args :as args])) 7 | 8 | (defn ^{:style/indent :defn} resource [& args] 9 | (let [[singular-name controller 10 | {:keys [segment], :or {segment (name singular-name)}} 11 | nested] 12 | (args/parse 2 args)] 13 | (let [{:keys [middleware new create show edit update put destroy]} controller] 14 | (cond-> [] 15 | new (conj (action :new :get ["new"] new)) 16 | create (conj (action :create :post [] create)) 17 | show (conj (action :show :get [] show)) 18 | edit (conj (action :edit :get ["edit"] edit)) 19 | update (conj (action :update :patch [] update)) 20 | put (conj (action :put :put [] put)) 21 | destroy (conj (action :destroy :delete [] destroy)) 22 | :always (into nested) 23 | :always (wrappers/composite) 24 | middleware (wrappers/wrap-middleware middleware) 25 | segment (wrappers/wrap-segment segment) 26 | :always (wrappers/wrap-scope singular-name))))) 27 | -------------------------------------------------------------------------------- /test/darkleaf/router/html/method_override_test.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.html.method-override-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [darkleaf.router :as r] 4 | [darkleaf.router.html.method-override :as sut])) 5 | 6 | (deftest wrap-method-override 7 | (let [controller {:put (fn [req] "success put")} 8 | routes (r/resource :star controller) 9 | handler (-> (r/make-handler routes) 10 | (sut/wrap-method-override))] 11 | (is (= "success put" 12 | (handler {:request-method :post 13 | :uri "/star" 14 | :params {:_method "put"}}))))) 15 | 16 | #?(:clj 17 | (deftest wrap-method-override-async 18 | (let [controller {:put (fn [req respond raise] (respond "success put"))} 19 | routes (r/resource :star controller) 20 | handler (-> (r/make-handler routes) 21 | (sut/wrap-method-override)) 22 | respond (promise) 23 | exception (promise)] 24 | (handler {:request-method :post 25 | :uri "/star" 26 | :params {:_method "put"}} 27 | respond 28 | exception) 29 | (is (= "success put" 30 | (deref respond 100 nil))) 31 | (is (not (realized? exception)))))) 32 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject darkleaf/router "0.3.3" 2 | :description "Bidirectional Ring router. REST oriented." 3 | :url "https://github.com/darkleaf/router" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.8.0" :scope "provided"] 7 | [org.clojure/clojurescript "1.9.293" :scope "provided"] 8 | [uritemplate-clj "1.1.1" :scope "test"]] 9 | :plugins [[lein-cljsbuild "1.1.4"] 10 | [lein-doo "0.1.7"]] 11 | :cljsbuild {:builds [{:id "test" 12 | :source-paths ["test" "src"] 13 | :compiler {:optimizations :none 14 | :target :nodejs 15 | :output-to "target/testable.js" 16 | :output-dir "target" 17 | :main darkleaf.test-runner}} 18 | {:id "test-advanced" 19 | :source-paths ["test" "src"] 20 | :compiler {:optimizations :advanced 21 | :target :nodejs 22 | :output-to "target/testable.js" 23 | :output-dir "target" 24 | :main darkleaf.test-runner}}]}) 25 | -------------------------------------------------------------------------------- /test/darkleaf/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns darkleaf.test-runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [darkleaf.router.additional-request-keys-test] 4 | [darkleaf.router.args-test] 5 | [darkleaf.router.async-test] 6 | [darkleaf.router.group-test] 7 | [darkleaf.router.guard-test] 8 | [darkleaf.router.mount-test] 9 | [darkleaf.router.pass-test] 10 | [darkleaf.router.resource-test] 11 | [darkleaf.router.resources-test] 12 | [darkleaf.router.section-test] 13 | [darkleaf.router.html.method-override-test] 14 | [darkleaf.router.use-cases.resource-composition-test] 15 | [darkleaf.router.use-cases.member-middleware-test] 16 | [darkleaf.router.use-cases.domain-constraint-test])) 17 | 18 | (doo-tests 19 | 'darkleaf.router.additional-request-keys-test 20 | 'darkleaf.router.args-test 21 | 'darkleaf.router.async-test 22 | 'darkleaf.router.group-test 23 | 'darkleaf.router.guard-test 24 | 'darkleaf.router.mount-test 25 | 'darkleaf.router.pass-test 26 | 'darkleaf.router.resource-test 27 | 'darkleaf.router.resources-test 28 | 'darkleaf.router.section-test 29 | 'darkleaf.router.html.method-override-test 30 | 'darkleaf.router.use-cases.resource-composition-test 31 | 'darkleaf.router.use-cases.member-middleware-test 32 | 'darkleaf.router.use-cases.domain-constraint-test) 33 | -------------------------------------------------------------------------------- /src/darkleaf/router/guard_impl.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.guard-impl 2 | (:require [darkleaf.router.item :as i] 3 | [darkleaf.router.keywords :as k] 4 | [darkleaf.router.args :as args] 5 | [darkleaf.router.url :as url] 6 | [darkleaf.router.item-wrappers :as wrappers])) 7 | 8 | (deftype Guard [item id predicate] 9 | i/Item 10 | (process [_ req] 11 | (let [segment (-> req k/segments peek)] 12 | (when (predicate segment) 13 | (as-> req <> 14 | (update <> k/segments pop) 15 | (update <> k/params assoc id segment) 16 | (i/process item <>))))) 17 | (fill [_ req] 18 | (let [segment (-> req k/params id)] 19 | (when (predicate segment) 20 | (as-> req <> 21 | (update <> k/segments conj segment) 22 | (i/fill item <>))))) 23 | (explain [_ init] 24 | (let [encoded-id (url/encode id)] 25 | (as-> init <> 26 | (assoc-in <> [:params-kmap id] encoded-id) 27 | (update-in <> [:req :uri] str "{/" encoded-id "}") 28 | (i/explain item <>))))) 29 | 30 | (defn ^{:style/indent :defn} guard [& args] 31 | (let [[id predicate 32 | {:keys [middleware]} 33 | children] 34 | (args/parse 2 args)] 35 | (cond-> (wrappers/composite children) 36 | middleware (wrappers/wrap-middleware middleware) 37 | :always (Guard. id predicate) 38 | :always (wrappers/wrap-scope id)))) 39 | -------------------------------------------------------------------------------- /test/darkleaf/router/group_test.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.group-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [darkleaf.router :as r] 4 | [darkleaf.router.test-helpers :refer [route-testing make-middleware]])) 5 | 6 | (deftest defaults 7 | (let [posts-controller (r/controller 8 | (show [req] "show post resp")) 9 | news-controller (r/controller 10 | (show [req] "show news resp")) 11 | routes (r/group 12 | (r/resources :posts :post posts-controller) 13 | (r/resources :news :news news-controller))] 14 | (route-testing routes 15 | :description [:show [:post] {:post "some-post"}] 16 | :request {:uri "/posts/some-post", :request-method :get} 17 | :response "show post resp") 18 | (route-testing routes 19 | :description [:show [:news] {:news "some-news"}] 20 | :request {:uri "/news/some-news", :request-method :get} 21 | :response "show news resp"))) 22 | 23 | (deftest middleware 24 | (let [pages-controller (r/controller 25 | (index [req] "index resp")) 26 | routes (r/group :middleware (make-middleware "wrapper") 27 | (r/resources :pages :page pages-controller)) 28 | handler (r/make-handler routes) 29 | req {:uri "/pages", :request-method :get}] 30 | (is (= "wrapper // index resp" (handler req))))) 31 | -------------------------------------------------------------------------------- /test/darkleaf/router/section_test.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.section-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [darkleaf.router :as r] 4 | [darkleaf.router.test-helpers :refer [route-testing make-middleware]])) 5 | 6 | (deftest defaults 7 | (let [pages-controller (r/controller 8 | (index [req] "index resp")) 9 | admin (r/section :admin 10 | (r/resources :pages :page pages-controller))] 11 | (route-testing admin 12 | :description [:index [:admin :pages] {}] 13 | :request {:uri "/admin/pages", :request-method :get} 14 | :response "index resp"))) 15 | 16 | (deftest with-segment 17 | (let [pages-controller (r/controller 18 | (index [req] "index resp")) 19 | admin (r/section :admin, :segment "private" 20 | (r/resources :pages :page pages-controller))] 21 | (route-testing admin 22 | :description [:index [:admin :pages] {}] 23 | :request {:uri "/private/pages", :request-method :get} 24 | :response "index resp"))) 25 | 26 | (deftest middleware 27 | (let [pages-controller (r/controller 28 | (index [req] "index resp")) 29 | routes (r/section :admin :middleware (make-middleware "admin") 30 | (r/resources :pages :page pages-controller)) 31 | handler (r/make-handler routes) 32 | req {:uri "/admin/pages", :request-method :get}] 33 | (is (= "admin // index resp" (handler req))))) 34 | -------------------------------------------------------------------------------- /test/darkleaf/router/guard_test.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.guard-test 2 | (:require [clojure.test :refer [deftest testing is]] 3 | [darkleaf.router :as r] 4 | [darkleaf.router.test-helpers :refer [route-testing make-middleware]])) 5 | 6 | (deftest defaults 7 | (let [pages-controller (r/controller 8 | (index [req] 9 | (str "locale: " 10 | (-> req ::r/params :locale)))) 11 | routes (r/guard :locale #{"ru" "en"} 12 | (r/resources :pages :page pages-controller))] 13 | (testing "correct" 14 | (let [routes-testing (partial route-testing routes)] 15 | (route-testing routes 16 | :description [:index [:locale :pages] {:locale "ru"}] 17 | :request {:uri "/ru/pages", :request-method :get} 18 | :response "locale: ru"))) 19 | (testing "wrong" 20 | (let [handler (r/make-handler routes)] 21 | (is (= 404 22 | (:status (handler {:uri "/wrong/pages", :request-method :get})))))))) 23 | 24 | (deftest middleware 25 | (let [pages-controller (r/controller 26 | (index [req] 27 | (str "locale: " 28 | (-> req ::r/params :locale)))) 29 | routes (r/guard :locale #{"ru" "en"} :middleware (make-middleware "guard") 30 | (r/resources :pages :page pages-controller)) 31 | handler (r/make-handler routes)] 32 | (is (= "guard // locale: ru" 33 | (handler {:uri "/ru/pages", :request-method :get}))))) 34 | -------------------------------------------------------------------------------- /src/darkleaf/router/pass_impl.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.pass-impl 2 | (:require [darkleaf.router.keywords :as k] 3 | [darkleaf.router.item :as i] 4 | [darkleaf.router.url :as url] 5 | [darkleaf.router.item-wrappers :as wrappers])) 6 | 7 | (def ^:private actions #{:get :post :patch :put :delete :head :options}) 8 | 9 | (deftype Pass [id handler] 10 | i/Item 11 | (process [_ req] 12 | (when (actions (-> req :request-method)) 13 | (let [segments (-> req k/segments) 14 | req (-> req 15 | (assoc k/action (:request-method req)) 16 | (assoc-in [k/params :segments] (vec segments)) 17 | (dissoc k/segments))] 18 | [handler req]))) 19 | (fill [_ req] 20 | (let [segments (get-in req [k/params :segments] [])] 21 | (when (and (empty? (k/scope req)) 22 | (actions (k/action req))) 23 | (-> req 24 | (assoc :request-method (k/action req)) 25 | (update k/segments into segments) 26 | (dissoc k/scope))))) 27 | (explain [_ init] 28 | (let [encoded-segments (url/encode :segments)] 29 | (for [action actions] 30 | (-> init 31 | (assoc :action action) 32 | (assoc-in [:params-kmap :segments] encoded-segments) 33 | (assoc-in [:req :request-method] action) 34 | (update-in [:req :uri] str "{/" encoded-segments "*}")))))) 35 | 36 | (defn pass [id handler & {:keys [segment middleware] 37 | :or {segment (name id)}}] 38 | (cond-> (Pass. id handler) 39 | middleware (wrappers/wrap-middleware middleware) 40 | segment (wrappers/wrap-segment segment) 41 | :always (wrappers/wrap-scope id))) 42 | -------------------------------------------------------------------------------- /test/darkleaf/router/controller_test.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.controller-test 2 | (:require [clojure.test :refer [deftest are]] 3 | [darkleaf.router :as r])) 4 | 5 | (r/defcontroller pages-controller 6 | (middleware [handler] 7 | #(str "pages // " (handler %))) 8 | (collection-middleware [handler] 9 | #(str "collection // " (handler %))) 10 | (member-middleware [handler] 11 | #(str "member // " (handler %))) 12 | (index [req] 13 | "index resp") 14 | (new [req] 15 | "new resp") 16 | (create [req] 17 | "create resp") 18 | (show [req] 19 | (str "show resp")) 20 | (edit [req] 21 | (str "edit resp")) 22 | (update [req] 23 | (str "update resp")) 24 | (put [req] 25 | (str "put resp")) 26 | (destroy [req] 27 | (str "destroy resp"))) 28 | 29 | (deftest default 30 | (let [routes (r/resources :pages :page pages-controller) 31 | handler (r/make-handler routes)] 32 | (are [req resp] (= resp (handler req)) 33 | {:uri "/pages", :request-method :get} 34 | "pages // collection // index resp" 35 | 36 | {:uri "/pages/1", :request-method :get} 37 | "pages // member // show resp" 38 | 39 | {:uri "/pages/new", :request-method :get} 40 | "pages // collection // new resp" 41 | 42 | {:uri "/pages", :request-method :post} 43 | "pages // collection // create resp" 44 | 45 | {:uri "/pages/1/edit", :request-method :get} 46 | "pages // member // edit resp" 47 | 48 | {:uri "/pages/1", :request-method :patch} 49 | "pages // member // update resp" 50 | 51 | {:uri "/pages/1", :request-method :put} 52 | "pages // member // put resp" 53 | 54 | {:uri "/pages/1", :request-method :delete} 55 | "pages // member // destroy resp"))) 56 | -------------------------------------------------------------------------------- /src/darkleaf/router.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router 2 | (:require [darkleaf.router.group-impl :as group-impl] 3 | [darkleaf.router.section-impl :as section-impl] 4 | [darkleaf.router.guard-impl :as guard-impl] 5 | [darkleaf.router.resource-impl :as resource-impl] 6 | [darkleaf.router.resources-impl :as resources-impl] 7 | [darkleaf.router.mount-impl :as mount-impl] 8 | [darkleaf.router.pass-impl :as pass-impl] 9 | [darkleaf.router.helpers :as helpers]) 10 | #?(:cljs (:require-macros [darkleaf.router :refer [defalias]]))) 11 | 12 | #?(:clj 13 | (defmacro ^:private defalias [name orig] 14 | `(do 15 | (def ~name ~orig) 16 | (let [new-var# (var ~name) 17 | orig-var# (var ~orig)] 18 | ;; for cljs compiler with advanced optimizations 19 | (when (and (some? new-var#) (some? orig-var#)) 20 | (alter-meta! new-var# merge (meta orig-var#))) 21 | new-var#)))) 22 | 23 | (defmacro controller 24 | {:style/indent [:defn [1]]} 25 | [& actions] 26 | (reduce (fn [acc [fn-name & fn-args]] 27 | (assoc acc (keyword fn-name) `(fn ~fn-name ~@fn-args))) 28 | {} 29 | actions)) 30 | 31 | (defmacro defcontroller 32 | {:style/indent [1 :defn [1]]} 33 | [controller-name & actions] 34 | `(def ~controller-name (controller ~@actions))) 35 | 36 | (defalias group group-impl/group) 37 | (defalias section section-impl/section) 38 | (defalias guard guard-impl/guard) 39 | (defalias resource resource-impl/resource) 40 | (defalias resources resources-impl/resources) 41 | (defalias mount mount-impl/mount) 42 | (defalias pass pass-impl/pass) 43 | 44 | (defalias make-request-for helpers/make-request-for) 45 | (defalias make-handler helpers/make-handler) 46 | (defalias explain helpers/explain) 47 | -------------------------------------------------------------------------------- /src/darkleaf/router/item_wrappers.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.item-wrappers 2 | (:require [darkleaf.router.keywords :as k] 3 | [darkleaf.router.item :as i])) 4 | 5 | (deftype Segment [item segment] 6 | i/Item 7 | (process [_ req] 8 | (when (= segment (-> req k/segments peek)) 9 | (i/process item 10 | (update req k/segments pop)))) 11 | (fill [_ req] 12 | (i/fill item 13 | (update req k/segments conj segment))) 14 | (explain [_ init] 15 | (i/explain item 16 | (update-in init [:req :uri] str "/" segment)))) 17 | 18 | (defn wrap-segment [item segment] 19 | (Segment. item segment)) 20 | 21 | (deftype Scope [item id] 22 | i/Item 23 | (process [_ req] 24 | (i/process item 25 | (update req k/scope conj id))) 26 | (fill [_ req] 27 | (when (= id (-> req k/scope peek)) 28 | (i/fill item 29 | (update req k/scope pop)))) 30 | (explain [_ init] 31 | (i/explain item 32 | (update init :scope conj id)))) 33 | 34 | (defn wrap-scope [item id] 35 | (Scope. item id)) 36 | 37 | (deftype Middleware [item middleware] 38 | i/Item 39 | (process [_ req] 40 | (i/process item 41 | (update req k/middlewares conj middleware))) 42 | (fill [_ req] 43 | (i/fill item req)) 44 | (explain [_ init] 45 | (i/explain item init))) 46 | 47 | (defn wrap-middleware [item middleware] 48 | (Middleware. item middleware)) 49 | 50 | (deftype Composite [items] 51 | i/Item 52 | (process [_ req] 53 | (some #(i/process % req) items)) 54 | (fill [_ req] 55 | (some #(i/fill % req) items)) 56 | (explain [_ init] 57 | (reduce (fn [acc item] 58 | (into acc (i/explain item init))) 59 | [] 60 | items))) 61 | 62 | (defn composite [items] 63 | (Composite. items)) 64 | -------------------------------------------------------------------------------- /test/darkleaf/router/use_cases/resource_composition_test.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.use-cases.resource-composition-test 2 | (:require [clojure.test :refer [deftest testing is]] 3 | [darkleaf.router :as r])) 4 | 5 | ;; There is a project resource and it needs to be completed. 6 | ;; Supposed project controller should have the complete action. 7 | ;; Some time later a new requirement is obtained: 8 | ;; there must be the form for data specifying while project completes. 9 | ;; In this case it is necessary to add actions for completed form. 10 | ;; Controller grows and becomes complicated fast with this approach. 11 | 12 | ;; In cases like that it is recommended to use nested resources 13 | ;; instead of adding extra actions to controller. 14 | 15 | ;; In this library there is only the one way to implement this requirement: 16 | ;; project resource must contains nested completion resource. 17 | 18 | (deftest usage 19 | (let [projects-controller (r/controller 20 | (index [req] "projects list") 21 | (show [req] "project page")) 22 | project-completion-controller (r/controller 23 | (new [req] "completion form") 24 | (create [req] "successfully completed")) 25 | routes (r/resources :projects :project projects-controller 26 | (r/resource :completion project-completion-controller))] 27 | (testing "handler" 28 | (let [handler (r/make-handler routes)] 29 | (is (= "completion form" 30 | (handler {:request-method :get 31 | :uri "/projects/1/completion/new"}))))) 32 | (testing "request-for" 33 | (let [request-for (r/make-request-for routes)] 34 | (is (= {:request-method :post 35 | :uri "/projects/1/completion"} 36 | (request-for :create [:project :completion] {:project 1}))))))) 37 | -------------------------------------------------------------------------------- /test/darkleaf/router/test_helpers.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.test-helpers 2 | (:require [clojure.test :refer [testing is]] 3 | [clojure.set :as set] 4 | [darkleaf.router :as r] 5 | #?(:clj [uritemplate-clj.core :as templ]) 6 | #?(:clj [clojure.walk :as walk]))) 7 | 8 | (defn make-middleware [name] 9 | (fn [handler] 10 | (fn [req] 11 | (str name " // " (handler req))))) 12 | 13 | #?(:clj 14 | (defn- transform-kv [m t-key t-val] 15 | (let [f (fn [[k v]] [(t-key k) (t-val v)])] 16 | (walk/postwalk (fn [x] (if (map? x) (into {} (map f x)) x)) m)))) 17 | 18 | (defn route-testing [routes & {[action scope params] :description 19 | :keys [request response]}] 20 | (testing (str "action " action " " 21 | "in scope " scope " " 22 | "with params " params) 23 | (testing "direct matching" 24 | (let [handler (r/make-handler routes)] 25 | (is (= response (handler request))))) 26 | (testing "reverse matching" 27 | (let [request-for (r/make-request-for routes)] 28 | (is (= request (request-for action scope params))))) 29 | #?(:clj 30 | (testing "explanation" 31 | (let [explanations (r/explain routes) 32 | explanation (first (filter (fn [i] 33 | (and (= action (:action i)) 34 | (= scope (:scope i)))) 35 | explanations)) 36 | _ (is (some? explanation)) 37 | params (set/rename-keys params (:params-kmap explanation)) 38 | request-template (:req explanation) 39 | calculated-request (transform-kv request-template 40 | identity 41 | (fn [val] 42 | (if (string? val) 43 | (templ/uritemplate val params) 44 | val)))] 45 | (is (= request calculated-request))))))) 46 | -------------------------------------------------------------------------------- /test/darkleaf/router/pass_test.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.pass-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [darkleaf.router :as r] 4 | [darkleaf.router.test-helpers :refer [route-testing make-middleware]])) 5 | 6 | (deftest defaults 7 | (let [handler (fn [req] "dashboard") 8 | routes (r/section :admin 9 | (r/pass :dashboard handler))] 10 | (route-testing routes 11 | :description [:post [:admin :dashboard] {}] 12 | :request {:uri "/admin/dashboard", :request-method :post} 13 | :response "dashboard") 14 | (route-testing routes 15 | :description [:get [:admin :dashboard] {:segments ["private" "users"]}] 16 | :request {:uri "/admin/dashboard/private/users", :request-method :get} 17 | :response "dashboard"))) 18 | 19 | (deftest with-segment 20 | (let [handler (fn [req] "dashboard") 21 | routes (r/section :admin 22 | (r/pass :dashboard handler :segment "monitoring"))] 23 | (route-testing routes 24 | :description [:post [:admin :dashboard] {}] 25 | :request {:uri "/admin/monitoring", :request-method :post} 26 | :response "dashboard"))) 27 | 28 | (deftest without-segment 29 | (let [main-controller {:show (fn [_] "main")} 30 | not-found-handler (fn [req] "custom 404 error") 31 | routes (r/group 32 | (r/resource :main main-controller :segment false) 33 | (r/pass :not-found not-found-handler :segment false))] 34 | (route-testing routes 35 | :description [:show [:main] {}] 36 | :request {:uri "", :request-method :get} 37 | :response "main") 38 | (route-testing routes 39 | :description [:get [:not-found] {:segments ["foo" "bar"]}] 40 | :request {:uri "/foo/bar", :request-method :get} 41 | :response "custom 404 error"))) 42 | 43 | (deftest middleware 44 | (let [handler (fn [req] "dashboard") 45 | middleware (make-middleware "m") 46 | routes (r/pass :dashboard handler :middleware middleware) 47 | handler (r/make-handler routes)] 48 | (is (= "m // dashboard") 49 | (handler {:uri "/dashboard", :request-method :get})))) 50 | -------------------------------------------------------------------------------- /test/darkleaf/router/use_cases/member_middleware_test.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.use-cases.member-middleware-test 2 | (:require [clojure.test :refer [deftest testing is are]] 3 | [darkleaf.router :as r])) 4 | 5 | (deftest usage 6 | (let [db {:users {"1" {:id "1", :name "Alice" 7 | :projects {"1" {:id "1", :name "e-shop"} 8 | "2" {:id "2", :name "web-site"}}}}} 9 | find-user (fn [id] (get-in db [:users id])) 10 | find-project (fn [user-id id] (get-in db [:users user-id :projects id])) 11 | users-controller (r/controller 12 | (member-middleware [h] 13 | (fn [req] 14 | (-> req 15 | (assoc-in [:models :user] 16 | (find-user (-> req ::r/params :user))) 17 | (h)))) 18 | (show [req] (-> req :models :user :name))) 19 | projects-controller (r/controller 20 | (member-middleware [h] 21 | (fn [req] 22 | (-> req 23 | (assoc-in [:models :project] 24 | (find-project (-> req :models :user :id) 25 | (-> req ::r/params :project))) 26 | (h)))) 27 | (index [req] 28 | (str "user name: " 29 | (-> req :models :user :name) 30 | "; " 31 | "projects list")) 32 | (show [req] 33 | (str "user name: " 34 | (-> req :models :user :name) 35 | "; project: " 36 | (-> req :models :project :name)))) 37 | routes (r/resources :users :user users-controller 38 | (r/resources :projects :project projects-controller))] 39 | (testing "response" 40 | (let [handler (r/make-handler routes)] 41 | (are [req resp] (= resp (handler req)) 42 | {:uri "/users/1", :request-method :get} 43 | "Alice" 44 | 45 | {:uri "/users/1/projects", :request-method :get} 46 | "user name: Alice; projects list" 47 | 48 | {:uri "/users/1/projects/2", :request-method :get} 49 | "user name: Alice; project: web-site"))))) 50 | -------------------------------------------------------------------------------- /test/darkleaf/router/use_cases/domain_constraint_test.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.use-cases.domain-constraint-test 2 | (:require [clojure.test :refer [deftest testing is]] 3 | [darkleaf.router :as r] 4 | [darkleaf.router.item :as i] 5 | [darkleaf.router.item-wrappers :as wrappers])) 6 | 7 | (deftype DomainConstraint [item name] 8 | i/Item 9 | (process [_ req] 10 | (when (= name (:server-name req)) 11 | (i/process item req))) 12 | (fill [_ req] 13 | (i/fill item (assoc req :server-name name))) 14 | (explain [_ init] 15 | (i/explain item (assoc-in init [:req :server-name] name)))) 16 | 17 | (defn ^{:style/indent :defn} domain [id name & children] 18 | (-> (wrappers/composite children) 19 | (wrappers/wrap-scope id) 20 | (DomainConstraint. name))) 21 | 22 | (deftest usage 23 | (let [main-pages-controller (r/controller 24 | (index [req] "main pages")) 25 | shop-pages-controller (r/controller 26 | (index [req] "shop pages")) 27 | routes (r/group 28 | (domain :main "cool-site.com" 29 | (r/resources :pages :page main-pages-controller)) 30 | (domain :shop "shop.cool-site.com" 31 | (r/resources :pages :page shop-pages-controller)))] 32 | (testing "handler" 33 | (let [handler (r/make-handler routes)] 34 | (is (= "main pages" 35 | (handler {:request-method :get 36 | :uri "/pages" 37 | :server-name "cool-site.com"}))) 38 | (is (= "shop pages" 39 | (handler {:request-method :get 40 | :uri "/pages" 41 | :server-name "shop.cool-site.com"}))))) 42 | (testing "request-for" 43 | (let [request-for (r/make-request-for routes)] 44 | (is (= {:request-method :get 45 | :uri "/pages" 46 | :server-name "cool-site.com"} 47 | (request-for :index [:main :pages] {}))) 48 | (is (= {:request-method :get 49 | :uri "/pages" 50 | :server-name "shop.cool-site.com"} 51 | (request-for :index [:shop :pages] {}))))) 52 | (testing "explain" 53 | (is (= [{:action :index, 54 | :scope [:main :pages], 55 | :params-kmap {}, 56 | :req {:uri "/pages", 57 | :server-name "cool-site.com", 58 | :request-method :get}} 59 | {:action :index, 60 | :scope [:shop :pages], 61 | :params-kmap {}, 62 | :req {:uri "/pages", 63 | :server-name "shop.cool-site.com", 64 | :request-method :get}}] 65 | (r/explain routes)))))) 66 | -------------------------------------------------------------------------------- /src/darkleaf/router/helpers.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.helpers 2 | (:require [clojure.string :refer [split join]] 3 | [darkleaf.router.keywords :as k] 4 | [darkleaf.router.item :as i])) 5 | 6 | (def ^:private empty-segments #?(:clj clojure.lang.PersistentQueue/EMPTY 7 | :cljs cljs.core/PersistentQueue.EMPTY)) 8 | (def ^:private empty-scope #?(:clj clojure.lang.PersistentQueue/EMPTY 9 | :cljs cljs.core/PersistentQueue.EMPTY)) 10 | (def ^:private empty-middlewares []) 11 | 12 | (defn- uri->segments [uri] 13 | (into empty-segments 14 | (map second (re-seq #"/([^/]+)" uri)))) 15 | 16 | (defn- segments->uri [segments] 17 | (->> segments 18 | (map #(str "/" %)) 19 | (join))) 20 | 21 | (defn make-request-for [item] 22 | (fn [action scope params] 23 | (let [scope (into empty-scope scope) 24 | initial-req {k/action action 25 | k/scope scope 26 | k/params params 27 | k/segments empty-segments}] 28 | (when-let [req (i/fill item initial-req)] 29 | (assert (-> req k/scope empty?)) 30 | (as-> req r 31 | (assoc r :uri (segments->uri (k/segments r))) 32 | (dissoc r k/action k/scope k/params k/segments)))))) 33 | 34 | (defn- process [item req] 35 | (let [req (assoc req 36 | k/scope empty-scope 37 | k/params {} 38 | k/segments (uri->segments (:uri req)) 39 | k/middlewares empty-middlewares)] 40 | (when-let [[handler req] (i/process item req)] 41 | (assert (-> req k/segments empty?)) 42 | (assert (-> req k/action keyword?)) 43 | (let [middleware (apply comp (k/middlewares req)) 44 | handler (middleware handler) 45 | req (dissoc req 46 | k/middlewares 47 | k/segments)] 48 | [handler req])))) 49 | 50 | (def not-found 51 | {:status 404 52 | :headers {} 53 | :body "404 error"}) 54 | 55 | (defn make-handler [item] 56 | (let [request-for (make-request-for item) 57 | pre-process (fn [req] 58 | (assoc req k/request-for request-for))] 59 | (fn 60 | ([req] 61 | (let [req (pre-process req)] 62 | (if-let [[handler req] (process item req)] 63 | (handler req) 64 | not-found))) 65 | ([req resp raise] 66 | (let [req (pre-process req)] 67 | (if-let [[handler req] (process item req)] 68 | (handler req resp raise) 69 | (resp not-found))))))) 70 | 71 | (defn explain [item] 72 | (let [init {:action nil 73 | :scope [] 74 | :params-kmap {} 75 | :req {:uri ""}} 76 | explanations (i/explain item init)] 77 | explanations)) 78 | -------------------------------------------------------------------------------- /src/darkleaf/router/resources_impl.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.resources-impl 2 | (:require [darkleaf.router.keywords :as k] 3 | [darkleaf.router.item :as i] 4 | [darkleaf.router.item-wrappers :as wrappers] 5 | [darkleaf.router.action :refer [action]] 6 | [darkleaf.router.args :as args] 7 | [darkleaf.router.url :as url])) 8 | 9 | (deftype MemberScope [item id] 10 | i/Item 11 | (process [_ req] 12 | (when-let [key-segment (-> req k/segments peek)] 13 | (as-> req <> 14 | (update <> k/segments pop) 15 | (assoc-in <> [k/params id] key-segment) 16 | (i/process item <>)))) 17 | (fill [_ req] 18 | (when-let [key-segment (get-in req [k/params id])] 19 | (as-> req <> 20 | (update <> k/segments conj key-segment) 21 | (i/fill item <>)))) 22 | (explain [_ init] 23 | (let [encoded-id (url/encode id)] 24 | (as-> init <> 25 | (assoc-in <> [:params-kmap id] encoded-id) 26 | (update-in <> [:req :uri] str "{/" encoded-id "}") 27 | (i/explain item <>))))) 28 | 29 | (defn ^{:style/indent :defn} resources [& args] 30 | (let [[plural-name singular-name controller 31 | {:keys [segment nested] 32 | :or {segment (name plural-name)}} 33 | nested] 34 | (args/parse 3 args)] 35 | (let [{:keys [middleware member-middleware collection-middleware 36 | index new create show edit update put destroy]} controller] 37 | (cond-> (wrappers/composite 38 | [(cond-> [] 39 | index (conj (action :index :get [] index)) 40 | :always (wrappers/composite) 41 | collection-middleware (wrappers/wrap-middleware collection-middleware) 42 | :always (wrappers/wrap-scope plural-name)) 43 | (cond-> [] 44 | new (conj (action :new :get ["new"] new)) 45 | create (conj (action :create :post [] create)) 46 | :always (wrappers/composite) 47 | collection-middleware (wrappers/wrap-middleware collection-middleware) 48 | :always (wrappers/wrap-scope singular-name)) 49 | (cond-> [] 50 | show (conj (action :show :get [] show)) 51 | edit (conj (action :edit :get ["edit"] edit)) 52 | update (conj (action :update :patch [] update)) 53 | destroy (conj (action :destroy :delete [] destroy)) 54 | put (conj (action :put :put [] put)) 55 | :always (into nested) 56 | :always (wrappers/composite) 57 | member-middleware (wrappers/wrap-middleware member-middleware) 58 | :always (MemberScope. singular-name) 59 | :always (wrappers/wrap-scope singular-name))]) 60 | middleware (wrappers/wrap-middleware middleware) 61 | segment (wrappers/wrap-segment segment))))) 62 | -------------------------------------------------------------------------------- /test/darkleaf/router/mount_test.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.mount-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [darkleaf.router :as r] 4 | [darkleaf.router.test-helpers :refer [route-testing make-middleware]])) 5 | 6 | (deftest with-segment 7 | (let [dashboard-controller (r/controller 8 | (show [req] 9 | (let [request-for (::r/request-for req)] 10 | (str "dashboard url: " 11 | (:uri (request-for :show [:dashboard/main] {})))))) 12 | dashboard (r/resource :dashboard/main dashboard-controller :segment false) 13 | routes (r/group 14 | (r/section :admin 15 | (r/mount dashboard :segment "dashboard")) 16 | (r/guard :locale #{"en" "ru"} 17 | (r/mount dashboard :segment "dashboard")))] 18 | (route-testing routes 19 | :description [:show [:admin :dashboard/main] {}] 20 | :request {:uri "/admin/dashboard", :request-method :get} 21 | :response "dashboard url: /admin/dashboard") 22 | (route-testing routes 23 | :description [:show [:locale :dashboard/main] {:locale "en"}] 24 | :request {:uri "/en/dashboard", :request-method :get} 25 | :response "dashboard url: /en/dashboard"))) 26 | 27 | (deftest without-segment 28 | (let [dashboard-controller (r/controller 29 | (show [req] "dashboard")) 30 | dashboard (r/resource :dashboard/main dashboard-controller :segment false)] 31 | (for [routes [(r/mount dashboard) 32 | (r/mount dashboard :segment false)]] 33 | (route-testing routes 34 | :description [:show [:dashboard/main] {}] 35 | :request {:uri "", :request-method :get} 36 | :response "dashboard")))) 37 | 38 | (deftest middleware 39 | (let [forum-topics-controller (r/controller 40 | (show [req] 41 | (str "topic " 42 | (-> req ::r/params :forum/topic) 43 | " inside " 44 | (-> req :forum/scope)))) 45 | forum (r/resources :forum/topics :forum/topic forum-topics-controller) 46 | 47 | sites-controller {} 48 | site-forum-adapter (fn [handler] 49 | (fn [req] 50 | (-> req 51 | (assoc :forum/scope (str "site " 52 | (-> req ::r/params :site))) 53 | (handler)))) 54 | community-forum-adapter (fn [handler] 55 | (fn [req] 56 | (-> req 57 | (assoc :forum/scope "community") 58 | (handler)))) 59 | routes (r/group 60 | (r/mount forum :segment "community", :middleware community-forum-adapter) 61 | (r/resources :sites :site sites-controller 62 | (r/mount forum :segment "forum", :middleware site-forum-adapter)))] 63 | (route-testing routes 64 | :description [:show [:forum/topic] {:forum/topic "1"}] 65 | :request {:uri "/community/topics/1", :request-method :get} 66 | :response "topic 1 inside community") 67 | (route-testing routes 68 | :description [:show [:site :forum/topic] {:site "1", :forum/topic "2"}] 69 | :request {:uri "/sites/1/forum/topics/2", :request-method :get} 70 | :response "topic 2 inside site 1"))) 71 | -------------------------------------------------------------------------------- /test/darkleaf/router/resource_test.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.resource-test 2 | (:require [clojure.test :refer [deftest are]] 3 | [darkleaf.router :as r] 4 | [darkleaf.router.test-helpers :refer [route-testing make-middleware]])) 5 | 6 | (deftest defaults 7 | (let [star-controller (r/controller 8 | (show [req] "show resp") 9 | (new [req] "new resp") 10 | (create [req] "create resp") 11 | (edit [req] "edit resp") 12 | (update [req] "update resp") 13 | (put [req] "put resp") 14 | (destroy [req] "destroy resp")) 15 | star (r/resource :star star-controller)] 16 | (route-testing star 17 | :description [:new [:star] {}] 18 | :request {:uri "/star/new", :request-method :get} 19 | :response "new resp") 20 | (route-testing star 21 | :description [:create [:star] {}] 22 | :request {:uri "/star", :request-method :post} 23 | :response "create resp") 24 | (route-testing star 25 | :description [:show [:star] {}] 26 | :request {:uri "/star", :request-method :get} 27 | :response "show resp") 28 | (route-testing star 29 | :description [:edit [:star] {}] 30 | :request {:uri "/star/edit", :request-method :get} 31 | :response "edit resp") 32 | (route-testing star 33 | :description [:update [:star] {}] 34 | :request {:uri "/star", :request-method :patch} 35 | :response "update resp") 36 | (route-testing star 37 | :description [:put [:star] {}] 38 | :request {:uri "/star", :request-method :put} 39 | :response "put resp") 40 | (route-testing star 41 | :description [:destroy [:star] {}] 42 | :request {:uri "/star", :request-method :delete} 43 | :response "destroy resp"))) 44 | 45 | (deftest specificsegment 46 | (let [star-controller (r/controller 47 | (show [req] "show resp") 48 | (new [req] "new resp") 49 | (create [req] "create resp") 50 | (edit [req] "edit resp") 51 | (update [req] "update resp") 52 | (put [req] "put resp") 53 | (destroy [req] "destroy resp")) 54 | star (r/resource :star star-controller :segment "estrella")] 55 | (route-testing star 56 | :description [:new [:star] {}] 57 | :request {:uri "/estrella/new", :request-method :get} 58 | :response "new resp") 59 | (route-testing star 60 | :description [:create [:star] {}] 61 | :request {:uri "/estrella", :request-method :post} 62 | :response "create resp") 63 | (route-testing star 64 | :description [:show [:star] {}] 65 | :request {:uri "/estrella", :request-method :get} 66 | :response "show resp") 67 | (route-testing star 68 | :description [:edit [:star] {}] 69 | :request {:uri "/estrella/edit", :request-method :get} 70 | :response "edit resp") 71 | (route-testing star 72 | :description [:update [:star] {}] 73 | :request {:uri "/estrella", :request-method :patch} 74 | :response "update resp") 75 | (route-testing star 76 | :description [:put [:star] {}] 77 | :request {:uri "/estrella", :request-method :put} 78 | :response "put resp") 79 | (route-testing star 80 | :description [:destroy [:star] {}] 81 | :request {:uri "/estrella", :request-method :delete} 82 | :response "destroy resp"))) 83 | 84 | (deftest wihout-segment 85 | (let [star-controller (r/controller 86 | (show [req] "show resp") 87 | (new [req] "new resp") 88 | (create [req] "create resp") 89 | (edit [req] "edit resp") 90 | (update [req] "update resp") 91 | (put [req] "put resp") 92 | (destroy [req] "destroy resp")) 93 | star (r/resource :star star-controller :segment false)] 94 | (route-testing star 95 | :description [:new [:star] {}] 96 | :request {:uri "/new", :request-method :get} 97 | :response "new resp") 98 | (route-testing star 99 | :description [:create [:star] {}] 100 | :request {:uri "", :request-method :post} 101 | :response "create resp") 102 | (route-testing star 103 | :description [:show [:star] {}] 104 | :request {:uri "", :request-method :get} 105 | :response "show resp") 106 | (route-testing star 107 | :description [:edit [:star] {}] 108 | :request {:uri "/edit", :request-method :get} 109 | :response "edit resp") 110 | (route-testing star 111 | :description [:update [:star] {}] 112 | :request {:uri "", :request-method :patch} 113 | :response "update resp") 114 | (route-testing star 115 | :description [:put [:star] {}] 116 | :request {:uri "", :request-method :put} 117 | :response "put resp") 118 | (route-testing star 119 | :description [:destroy [:star] {}] 120 | :request {:uri "", :request-method :delete} 121 | :response "destroy resp"))) 122 | 123 | (deftest nested 124 | (let [star-controller {} 125 | comments-controller (r/controller 126 | (show [req] "show resp")) 127 | routes (r/resource :star star-controller 128 | (r/resources :comments :comment comments-controller))] 129 | (route-testing routes 130 | :description [:show [:star :comment] {:comment "some-comment"}] 131 | :request {:uri "/star/comments/some-comment" 132 | :request-method :get} 133 | :response "show resp"))) 134 | 135 | (deftest middleware 136 | (let [star-controller (r/controller 137 | (middleware [handler] 138 | #(str "star // " (handler %))) 139 | (show [req] "show resp") 140 | (new [req] "new resp") 141 | (create [req] "create resp") 142 | (edit [req] "edit resp") 143 | (update [req] "update resp") 144 | (put [req] "put resp") 145 | (destroy [req] "destroy resp")) 146 | comments-controllers (r/controller 147 | (middleware [handler] 148 | #(str "comments // " (handler %))) 149 | (index [req] "comments index")) 150 | routes (r/resource :star star-controller 151 | (r/resources :comments :comment comments-controllers)) 152 | handler (r/make-handler routes)] 153 | (are [req resp] (= resp (handler req)) 154 | {:uri "/star", :request-method :get} 155 | "star // show resp" 156 | 157 | {:uri "/star/new", :request-method :get} 158 | "star // new resp" 159 | 160 | {:uri "/star", :request-method :post} 161 | "star // create resp" 162 | 163 | {:uri "/star/edit", :request-method :get} 164 | "star // edit resp" 165 | 166 | {:uri "/star", :request-method :patch} 167 | "star // update resp" 168 | 169 | {:uri "/star", :request-method :put} 170 | "star // put resp" 171 | 172 | {:uri "/star", :request-method :delete} 173 | "star // destroy resp" 174 | 175 | {:uri "/star/comments", :request-method :get} 176 | "star // comments // comments index"))) 177 | -------------------------------------------------------------------------------- /test/darkleaf/router/resources_test.cljc: -------------------------------------------------------------------------------- 1 | (ns darkleaf.router.resources-test 2 | (:require [clojure.test :refer [deftest are]] 3 | [darkleaf.router :as r] 4 | [darkleaf.router.test-helpers :refer [route-testing make-middleware]])) 5 | 6 | (deftest defaults 7 | (let [pages-controller (r/controller 8 | (index [req] 9 | "index resp") 10 | (new [req] 11 | "new resp") 12 | (create [req] 13 | "create resp") 14 | (show [req] 15 | (str "show " (-> req ::r/params :page))) 16 | (edit [req] 17 | (str "edit " (-> req ::r/params :page))) 18 | (update [req] 19 | (str "update " (-> req ::r/params :page))) 20 | (put [req] 21 | (str "put " (-> req ::r/params :page))) 22 | (destroy [req] 23 | (str "destroy " (-> req ::r/params :page)))) 24 | pages (r/resources :pages :page pages-controller)] 25 | (route-testing pages 26 | :description [:index [:pages] {}] 27 | :request {:uri "/pages", :request-method :get} 28 | :response "index resp") 29 | (route-testing pages 30 | :description [:new [:page] {}] 31 | :request {:uri "/pages/new", :request-method :get} 32 | :response "new resp") 33 | (route-testing pages 34 | :description [:create [:page] {}] 35 | :request {:uri "/pages", :request-method :post} 36 | :response "create resp") 37 | (route-testing pages 38 | :description [:show [:page] {:page "about"}] 39 | :request {:uri "/pages/about", :request-method :get} 40 | :response "show about") 41 | (route-testing pages 42 | :description [:edit [:page] {:page "about"}] 43 | :request {:uri "/pages/about/edit", :request-method :get} 44 | :response "edit about") 45 | (route-testing pages 46 | :description [:update [:page] {:page "about"}] 47 | :request {:uri "/pages/about", :request-method :patch} 48 | :response "update about") 49 | (route-testing pages 50 | :description [:put [:page] {:page "about"}] 51 | :request {:uri "/pages/about", :request-method :put} 52 | :response "put about") 53 | (route-testing pages 54 | :description [:destroy [:page] {:page "about"}] 55 | :request {:uri "/pages/about", :request-method :delete} 56 | :response "destroy about"))) 57 | 58 | (deftest specific-segment 59 | (let [people-controller (r/controller 60 | (index [req] "index resp") 61 | (show [req] "show resp") 62 | (new [req] "new resp") 63 | (create [req] "create resp") 64 | (edit [req] "edit resp") 65 | (update [req] "update resp") 66 | (put [req] "put resp") 67 | (destroy [req] "destroy resp")) 68 | people (r/resources :people :person people-controller 69 | :segment "menschen")] 70 | (route-testing people 71 | :description [:index [:people] {}] 72 | :request {:uri "/menschen", :request-method :get} 73 | :response "index resp") 74 | (route-testing people 75 | :description [:new [:person] {}] 76 | :request {:uri "/menschen/new", :request-method :get} 77 | :response "new resp") 78 | (route-testing people 79 | :description [:create [:person] {}] 80 | :request {:uri "/menschen", :request-method :post} 81 | :response "create resp") 82 | (route-testing people 83 | :description [:show [:person] {:person "some-id"}] 84 | :request {:uri "/menschen/some-id", :request-method :get} 85 | :response "show resp") 86 | (route-testing people 87 | :description [:edit [:person] {:person "some-id"}] 88 | :request {:uri "/menschen/some-id/edit", :request-method :get} 89 | :response "edit resp") 90 | (route-testing people 91 | :description [:update [:person] {:person "some-id"}] 92 | :request {:uri "/menschen/some-id", :request-method :patch} 93 | :response "update resp") 94 | (route-testing people 95 | :description [:put [:person] {:person "some-id"}] 96 | :request {:uri "/menschen/some-id", :request-method :put} 97 | :response "put resp") 98 | (route-testing people 99 | :description [:destroy [:person] {:person "some-id"}] 100 | :request {:uri "/menschen/some-id", :request-method :delete} 101 | :response "destroy resp"))) 102 | 103 | (deftest without-segment 104 | (let [pages-controller (r/controller 105 | (index [req] 106 | "index resp") 107 | (new [req] 108 | "new resp") 109 | (create [req] 110 | "create resp") 111 | (show [req] 112 | (str "show " (-> req ::r/params :page))) 113 | (edit [req] 114 | (str "edit " (-> req ::r/params :page))) 115 | (update [req] 116 | (str "update " (-> req ::r/params :page))) 117 | (put [req] 118 | (str "put " (-> req ::r/params :page))) 119 | (destroy [req] 120 | (str "destroy " (-> req ::r/params :page)))) 121 | pages (r/resources :pages :page pages-controller, :segment false)] 122 | (route-testing pages 123 | :description [:index [:pages] {}] 124 | :request {:uri "", :request-method :get} 125 | :response "index resp") 126 | (route-testing pages 127 | :description [:new [:page] {}] 128 | :request {:uri "/new", :request-method :get} 129 | :response "new resp") 130 | (route-testing pages 131 | :description [:create [:page] {}] 132 | :request {:uri "", :request-method :post} 133 | :response "create resp") 134 | (route-testing pages 135 | :description [:show [:page] {:page "some"}] 136 | :request {:uri "/some", :request-method :get} 137 | :response "show some") 138 | (route-testing pages 139 | :description [:edit [:page] {:page "some"}] 140 | :request {:uri "/some/edit", :request-method :get} 141 | :response "edit some") 142 | (route-testing pages 143 | :description [:update [:page] {:page "some"}] 144 | :request {:uri "/some", :request-method :patch} 145 | :response "update some") 146 | (route-testing pages 147 | :description [:put [:page] {:page "some"}] 148 | :request {:uri "/some", :request-method :put} 149 | :response "put some") 150 | (route-testing pages 151 | :description [:destroy [:page] {:page "some"}] 152 | :request {:uri "/some", :request-method :delete} 153 | :response "destroy some"))) 154 | 155 | (deftest nested 156 | (let [pages-controller (r/controller 157 | (index [req] "some") 158 | (show [req] "pages show resp")) 159 | comments-controller (r/controller 160 | (show [req] "show resp")) 161 | star-controller (r/controller 162 | (show [req] 163 | (str "page " 164 | (-> req ::r/params :page) 165 | " star show resp"))) 166 | routes (r/resources :pages :page pages-controller 167 | (r/resources :comments :comment comments-controller) 168 | (r/resource :star star-controller))] 169 | (route-testing routes 170 | :description [:show 171 | [:page :comment] 172 | {:page "some-page", :comment "some-comment"}] 173 | :request {:uri "/pages/some-page/comments/some-comment" 174 | :request-method :get} 175 | :response "show resp") 176 | (route-testing routes 177 | :description [:show [:page :star] {:page 1}] 178 | :request {:uri "/pages/1/star", :request-method :get} 179 | :response "page 1 star show resp"))) 180 | 181 | (deftest middleware 182 | (let [pages-controller (r/controller 183 | (middleware [handler] 184 | #(str "pages // " (handler %))) 185 | (collection-middleware [handler] 186 | #(str "collection // " (handler %))) 187 | (member-middleware [handler] 188 | #(str "member // " (handler %))) 189 | (index [req] "index resp") 190 | (show [req] "show resp") 191 | (new [req] "new resp") 192 | (create [req] "create resp") 193 | (edit [req] "edit resp") 194 | (update [req] "update resp") 195 | (put [req] "put resp") 196 | (destroy [req] "destroy resp")) 197 | star-controller {:middleware (make-middleware "star") 198 | :show (fn [req] "show star resp")} 199 | routes (r/resources :pages :page pages-controller 200 | (r/resource :star star-controller)) 201 | handler (r/make-handler routes)] 202 | (are [req resp] (= resp (handler req)) 203 | {:uri "/pages", :request-method :get} 204 | "pages // collection // index resp" 205 | 206 | {:uri "/pages/1", :request-method :get} 207 | "pages // member // show resp" 208 | 209 | {:uri "/pages/new", :request-method :get} 210 | "pages // collection // new resp" 211 | 212 | {:uri "/pages", :request-method :post} 213 | "pages // collection // create resp" 214 | 215 | {:uri "/pages/1/edit", :request-method :get} 216 | "pages // member // edit resp" 217 | 218 | {:uri "/pages/1", :request-method :patch} 219 | "pages // member // update resp" 220 | 221 | {:uri "/pages/1", :request-method :put} 222 | "pages // member // put resp" 223 | 224 | {:uri "/pages/1", :request-method :delete} 225 | "pages // member // destroy resp" 226 | 227 | {:uri "/pages/1/star", :request-method :get} 228 | "pages // member // star // show star resp"))) 229 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Router 2 | 3 | [![Build Status](https://travis-ci.org/darkleaf/router.svg?branch=master)](https://travis-ci.org/darkleaf/router) 4 | [![Clojars Project](https://img.shields.io/clojars/v/darkleaf/router.svg)](https://clojars.org/darkleaf/router) 5 | 6 | Bidirectional RESTfull Ring router for clojure and clojurescript. 7 | 8 | ## Comparation 9 | 10 | | library | clj | cljs | dsl | named routes | mountable apps | abstraction | export format | extensibility | 11 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | 12 | | [compojure](https://github.com/weavejester/compojure) | ✓ | | macros | | | url | | | 13 | | [secretary](https://github.com/gf3/secretary) | | ✓ | macros | ✓ | | url | | protocols | 14 | | [bidi](https://github.com/juxt/bidi) | ✓ | ✓ | data/functions | ✓ | | url | route description data | protocols | 15 | | [darkleaf/router](https://github.com/darkleaf/router) | ✓ | ✓ | functions | ✓ | ✓ | resource | [explain data](#explain) | protocols | 16 | 17 | ## Usage 18 | 19 | ``` clojure 20 | (ns app.some-ns 21 | (:require [darkleaf.router :as r] 22 | [ring.util.response :refer [response]])) 23 | 24 | (r/defcontroller controller 25 | (index [req] 26 | (let [request-for (::r/request-for req)] 27 | (response (str (request-for :index [:pages] {})))))) 28 | 29 | (def routing (r/resources :pages :page controller)) 30 | 31 | (def handler (r/make-handler routing)) 32 | (def request-for (r/make-request-for routing)) 33 | 34 | (handler {:uri "/pages", :request-method :get}) ;; call index action from controller 35 | (request-for :index [:pages] {}) ;; returns {:uri "/pages", :request-method :get} 36 | ``` 37 | 38 | Single routing namespace: 39 | ``` clojure 40 | (ns app.routing 41 | (:require 42 | [darkleaf.router :as r] 43 | [app.controllers.main :as main] 44 | [app.controllers.session :as session] 45 | [app.controllers.account.invites :as account.invites] 46 | [app.controllers.users :as users] 47 | [app.controllers.users.statistics :as users.statistics] 48 | [app.controllers.users.pm-bonus :as users.pm-bonus] 49 | [app.controllers.projects :as projects] 50 | [app.controllers.projects.status :as projects.status] 51 | [app.controllers.projects.completion :as projects.completion] 52 | [app.controllers.tasks :as tasks] 53 | [app.controllers.tasks.status :as tasks.status] 54 | [app.controllers.tasks.comments :as tasks.comments])) 55 | 56 | (def routes 57 | (r/group 58 | (r/resource :main main/controller :segment false) 59 | (r/resource :session session/controller) 60 | (r/section :account 61 | (r/resources :invites :invite account.invites/controller)) 62 | (r/resources :users :user users/controller 63 | (r/resource :statistics users.statistics/controller) 64 | (r/resource :pm-bonus users.pm-bonus/controller)) 65 | (r/resources :projects :project projects/controller 66 | (r/resource :status projects.status/controller) 67 | (r/resource :completion projects.completion/controller)) 68 | (r/resources :tasks :task tasks/controller 69 | (r/resource :status tasks.status/controller) 70 | (r/resources :comments tasks.comments/controller)))) 71 | ``` 72 | 73 | Multiple routing namespaces: 74 | ``` clojure 75 | (ns app.routes.main 76 | (:require 77 | [darkleaf.router :as r])) 78 | 79 | (r/defcontroller controller 80 | (show [req] ...)) 81 | 82 | (def routes (r/resource :main main-controller :segment false)) 83 | 84 | (ns app.routes 85 | (:require 86 | [darkleaf.router :as r] 87 | [app.routes.main :as main] 88 | [app.routes.session :as session] 89 | [app.routes.account :as account] 90 | [app.routes.users :as users] 91 | [app.routes.projects :as projects] 92 | [app.routes.tasks :as tasks])) 93 | 94 | (def routes 95 | (r/group 96 | main/routes 97 | session/routes 98 | account/routes 99 | users/routes 100 | projects/routes 101 | tasks/routes)) 102 | ``` 103 | 104 | ## Use cases 105 | 106 | * [resource composition / additional controller actions](test/darkleaf/router/use_cases/resource_composition_test.cljc) 107 | * [member middleware](test/darkleaf/router/use_cases/member_middleware_test.cljc) 108 | * [extending / domain constraint](test/darkleaf/router/use_cases/domain_constraint_test.cljc) 109 | 110 | ## Rationale 111 | 112 | Routing libraries work similarly on all programming languages: they only map uri with a handler using templates. 113 | For example compojure, sinatra, express.js, cowboy. 114 | 115 | There are some downsides of this approach. 116 | 117 | 1. No reverse or named routing. Url is set in templates as a string. 118 | 2. Absence of structure. Libraries do not offer any ways of code structure, that results of chaos in url and unclean code. 119 | 3. Inability to mount an external application. Inability to create html links related with mount point. 120 | 4. Inability to serialize routing and use it in other external applications for request forming. 121 | 122 | Most of these problems are solved in [Ruby on Rails](http://guides.rubyonrails.org/routing.html). 123 | 124 | 1. If you know the action, controller name and parameters, you can get url, for example: edit_admin_post_path(@post.id). 125 | 2. You can use rest resources to describe routing. 126 | Actions of controllers match to handlers. 127 | However, framework allows to add non-standart actions into controller, 128 | that makes your code unlean later. 129 | 3. There is an engine support. For example, you can mount a forum engine into your project or 130 | decompose your application into several engines. 131 | 4. There is an API for routes traversing, which uses `rake routes` command. The library 132 | [js-routes](https://github.com/railsware/js-routes) brings url helpers in js. 133 | 134 | Solution my library suggests. 135 | 136 | 1. Knowing action, scope and params, we can get the request, which invokes the handler of this route: 137 | `(request-for :edit [:admin :post] {:post "1"})`. 138 | 2. The main abstraction is the rest resource. Controller contains only standard actions. 139 | You can see [resource composition](test/darkleaf/router/use_cases/resource_composition_test.cljc) how to deal with it. 140 | 3. Ability to mount an external application. See [example](#mount) for details. 141 | 4. The library interface is identical in clojure and clojurecript, that allows to share the code between server and 142 | client using .cljc files. You can also export routing description with cross-platform templates as a simple data 143 | structure. See [example](#explain) for details. 144 | 145 | ## Resources 146 | 147 | | Action name | Scope | Params | Http method | Url | Type | Used for | 148 | | --- | --- | --- | --- | --- | --- | --- | 149 | | index | [:pages] | {} | Get | /pages | collection | display a list of pages | 150 | | show | [:page] | {:page 1} | Get | /pages/1 | member | display a specific page | 151 | | new | [:page] | {} | Get | /pages/new | collection | display a form for creating new page | 152 | | create | [:page] | {} | Post | /pages | collection | create a new page | 153 | | edit | [:page] | {:page 1} | Get | /pages/1/edit | member | display a form for updating page | 154 | | update | [:page] | {:page 1} | Patch | /pages/1 | member | update a specific page | 155 | | put | [:page] | {:page 1} | Put | /pages/1 | member | upsert a specific page, may be combined with edit action | 156 | | destroy | [:page] | {:page 1} | Delete | /pages/1 | member | delete a specific page | 157 | 158 | ``` clojure 159 | (ns app.some-ns 160 | (:require [darkleaf.router :as r] 161 | [ring.util.response :refer [response]])) 162 | 163 | ;; all items are optional 164 | (r/defcontroller pages-controller 165 | (middleware [h] 166 | (fn [req] (h req))) 167 | (collection-middleware [h] 168 | (fn [req] (h req))) 169 | (member-middleware [h] 170 | (fn [req] (h req))) 171 | (index [req] 172 | (response "index resp")) 173 | (show [req] 174 | (response "show resp")) 175 | (new [req] 176 | (response "new resp")) 177 | (create [req] 178 | (response "create resp")) 179 | (edit [req] 180 | (response "edit resp")) 181 | (update [req] 182 | (response "update resp")) 183 | (put [req] 184 | (response "put resp")) 185 | (destroy [req] 186 | (response "destroy resp"))) 187 | 188 | ;; :index [:pages] {} -> /pages 189 | ;; :show [:page] {:page 1} -> /pages/1 190 | (r/resources :pages :page pages-controller) 191 | 192 | ;; :index [:people] {} -> /menschen 193 | ;; :show [:person] {:person 1} -> /menschen/1 194 | (r/resources :people :person people-controller :segment "menschen") 195 | 196 | ;; :index [:people] {} -> / 197 | ;; :show [:person] {:person 1} -> /1 198 | (r/resources :people :person people-controller :segment false) 199 | 200 | ;; :put [:page :star] {:page 1} -> PUT /pages/1/star 201 | (r/resources :pages :page pages-controller 202 | (r/resource :star star-controller) 203 | ``` 204 | 205 | There are 3 types of middlewares: 206 | 207 | * `middleware` applied to all action handlers including nested. 208 | * `collection-middleware` applied only to index, new and create actions. 209 | * `member-middleware` applied to show, edit, update, put, delete and all nested handlers, look 210 | [here](test/darkleaf/router/use_cases/member_middleware_test.cljc) for details. 211 | 212 | Please see [test](test/darkleaf/router/resources_test.cljc) for all examples. 213 | 214 | ## Resource 215 | 216 | | Action name | Scope | Params | Http method | Url | Used for 217 | | --- | --- | --- | --- | --- | --- | 218 | | show | [:star] | {} | Get | /star | display a specific star | 219 | | new | [:star] | {} | Get | /star/new | display a form for creating new star | 220 | | create | [:star] | {} | Post | /star | create a new star | 221 | | edit | [:star] | {} | Get | /star/edit | display a form for updating star | 222 | | update | [:star] | {} | Patch | /star | update a specific star | 223 | | put | [:star] | {} | Put | /star | upsert a specific star, may be combined with edit action | 224 | | destroy | [:star] | {} | Delete | /star | delete a specific star | 225 | 226 | ``` clojure 227 | ;; all items are optional 228 | (r/defcontroller star-controller 229 | ;; will be applied to nested routes too 230 | (middleware [h] 231 | (fn [req] (h req))) 232 | (show [req] 233 | (response "show resp")) 234 | (new [req] 235 | (response "new resp")) 236 | (create [req] 237 | (response "create resp")) 238 | (edit [req] 239 | (response "edit resp")) 240 | (update [req] 241 | (response "update resp")) 242 | (put [req] 243 | (response "put resp")) 244 | (destroy [req] 245 | (response "destroy resp"))) 246 | 247 | ;; :show [:star] {} -> /star 248 | (r/resource :star star-controller) 249 | 250 | ;; :show [:star] {} -> /estrella 251 | (r/resource :star star-controller :segment "estrella") 252 | 253 | ;; :show [:star] {} -> / 254 | (r/resource :star star-controller :segment false) 255 | 256 | ;; :index [:star :comments] {} -> /star/comments 257 | (r/resource :star star-controller 258 | (r/resources :comments :comment comments-controller) 259 | ``` 260 | 261 | Please see [test](test/darkleaf/router/resource_test.cljc) for exhaustive examples. 262 | 263 | ## Group 264 | 265 | This function combines multiple routes into one and applies optional middleware. 266 | 267 | ``` clojure 268 | (r/defcontroller posts-controller 269 | (show [req] (response "show post resp"))) 270 | (r/defcontroller news-controller 271 | (show [req] (response "show news resp"))) 272 | 273 | ;; :show [:post] {:post 1} -> /posts/1 274 | ;; :show [:news] {:news 1} -> /news/1 275 | (r/group 276 | (r/resources :posts :post posts-controller) 277 | (r/resources :news :news news-controller))) 278 | 279 | (r/group :middleware (fn [h] (fn [req] (h req))) 280 | (r/resources :posts :post posts-controller) 281 | (r/resources :news :news news-controller)) 282 | ``` 283 | 284 | Please see [test](test/darkleaf/router/group_test.cljc) for exhaustive examples. 285 | 286 | ## Section 287 | 288 | ``` clojure 289 | ;; :index [:admin :pages] {} -> /admin/pages 290 | (r/section :admin 291 | (r/resources :pages :page pages-controller)) 292 | 293 | ;; :index [:admin :pages] {} -> /private/pages 294 | (r/section :admin, :segment "private" 295 | (r/resources :pages :page pages-controller)) 296 | 297 | (r/section :admin, :middleware (fn [h] (fn [req] (h req))) 298 | (r/resources :pages :page pages-controller)) 299 | ``` 300 | 301 | Please see [test](test/darkleaf/router/section_test.cljc) for exhaustive examples. 302 | 303 | ## Guard 304 | 305 | ``` clojure 306 | ;; :index [:locale :pages] {:locale "ru"} -> /ru/pages 307 | ;; :index [:locale :pages] {:locale "wrong"} -> not found 308 | (r/guard :locale #{"ru" "en"} 309 | (r/resources :pages :page pages-controller)) 310 | 311 | (r/guard :locale #(= "en" %) 312 | (r/resources :pages :page pages-controller)) 313 | 314 | (r/guard :locale #{"ru" "en"} :middleware (fn [h] (fn [req] (h req))) 315 | (r/resources :pages :page pages-controller)) 316 | ``` 317 | 318 | Please see [test](test/darkleaf/router/guard_test.cljc) for exhaustive examples. 319 | 320 | ## Mount 321 | 322 | This function allows to mount isolated applications. `request-for` inside `request` map works regarding the mount point. 323 | 324 | ```clojure 325 | (def dashboard-app (r/resource :dashboard/main dashboard-controller :segment false)) 326 | 327 | ;; show [:admin :dashboard/main] {} -> /admin/dashboard 328 | (r/section :admin 329 | (r/mount dashboard-app :segment "dashboard")) 330 | 331 | ;; show [:admin :dashboard/main] {} -> /admin 332 | (r/section :admin 333 | (r/mount dashboard-app :segment false)) 334 | 335 | ;; show [:admin :dashboard/main] {} -> /admin 336 | (r/section :admin 337 | (r/mount dashboard-app)) 338 | 339 | (r/section :admin 340 | (r/mount dashboard-app :segment "dashboard", :middleware (fn [h] (fn [req] (h req))))) 341 | ``` 342 | 343 | Please see [test](test/darkleaf/router/mount_test.cljc) for exhaustive examples. 344 | 345 | ## Pass 346 | 347 | Passes any request in the current scope to a specified handler. 348 | Inner segments are available as `(-> req ::r/params :segments)`. 349 | Action name is provided by request-method. 350 | It can be used for creating custom 404 page for current scope. 351 | 352 | ```clojure 353 | (defn handler (fn [req] (response "dashboard"))) 354 | 355 | ;; :get [:admin :dashboard] {} -> /admin/dashboard 356 | ;; :post [:admin :dashboard] {:segments ["private" "users"]} -> POST /admin/dashboard/private/users 357 | (r/section :admin 358 | (r/pass :dashboard handler)) 359 | 360 | ;; :get [:admin :dashboard] {} -> /admin/monitoring 361 | ;; :post [:admin :dashboard] {:segments ["private" "users"]} -> POST /admin/monitoring/private/users 362 | (r/section :admin 363 | (r/pass :dashboard handler :segment "monitoring")) 364 | 365 | ;; :get [:not-found] {} -> / 366 | ;; :post [:not-found] {:segments ["foo" "bar"]} -> POST /foo/bar 367 | (r/pass :not-found handler :segment false) 368 | ``` 369 | 370 | Please see [test](test/darkleaf/router/pass_test.cljc) for exhaustive examples. 371 | 372 | ## Additional request keys 373 | 374 | Handler adds keys for request map: 375 | * :darkleaf.router/action 376 | * :darkleaf.router/scope 377 | * :darkleaf.router/params 378 | * :darkleaf.router/request-for 379 | 380 | Please see [test](test/darkleaf/router/additional_request_keys_test.cljc) for exhaustive examples. 381 | 382 | ## Async 383 | 384 | [Asynchronous ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) handlers support. 385 | It also can be used in [macchiato-framework](https://github.com/macchiato-framework/examples/tree/master/auth-example-router). 386 | 387 | ``` clojure 388 | (r/defcontroller pages-controller 389 | (index [req resp raise] 390 | (future (resp response)))) 391 | 392 | (def pages (r/resources :pages :page pages-controller)) 393 | (def handler (r/make-handler pages)) 394 | 395 | (defn respond [val]) ;; from web server 396 | (defn error [e]) ;; from web server 397 | 398 | (handler {:request-method :get, :uri "/pages"} respond error) 399 | ``` 400 | 401 | Please see [clj test](test/darkleaf/router/async_test.clj) 402 | and [cljs test](test/darkleaf/router/async_test.cljs) 403 | for exhaustive examples. 404 | 405 | ## Explain 406 | 407 | ```clojure 408 | (r/defcontroller people-controller 409 | (index [req] (response "index")) 410 | (show [req] (response "show"))) 411 | 412 | (def routes (r/resources :people :person people-controller)) 413 | (pprint (r/explain routes)) 414 | ``` 415 | 416 | ```clojure 417 | [{:action :index, 418 | :scope [:people], 419 | :params-kmap {}, 420 | :req {:uri "/people", :request-method :get}} 421 | {:action :show, 422 | :scope [:person], 423 | :params-kmap {:person "%3Aperson"}, 424 | :req {:uri "/people{/%3Aperson}", :request-method :get}}] 425 | ``` 426 | 427 | It useful for: 428 | 429 | + inspection routing structure 430 | + mistakes detection 431 | + cross-platform routes serialization 432 | + documentation generation 433 | 434 | [URI Template](https://tools.ietf.org/html/rfc6570) uses for templating. 435 | Url encode is applied for ability to use keywords as a template variable 436 | because of the fact that clojure keywords contains forbidden symbols. 437 | Template parameters and :params mapping is set with :params-kmap. 438 | 439 | ## HTML 440 | 441 | HTML doesn’t support HTTP methods except GET и POST. 442 | You need to add the hidden field _method with put, patch or delete value to send PUT, PATCH or DELETE request. 443 | It is also necessary to wrap a handler with `darkleaf.router.html.method-override/wrap-method-override`. 444 | Use it with `ring.middleware.params/wrap-params` and `ring.middleware.keyword-params/wrap-keyword-params`. 445 | 446 | Please see [examples](test/darkleaf/router/html/method_override_test.cljc). 447 | 448 | In future releases I'm going to add js code for arbitrary request sending using html links. 449 | 450 | ## Questions 451 | 452 | You can create github issue with your question. 453 | 454 | ## TODO 455 | 456 | * docs 457 | * pre, assert 458 | 459 | ## License 460 | 461 | Copyright © 2016 Mikhail Kuzmin 462 | 463 | Distributed under the Eclipse Public License version 1.0. 464 | --------------------------------------------------------------------------------