Bidirectional RESTfull Ring router for clojure and clojurescript.
| library | clj | cljs | dsl | named routes | mountable apps | abstraction | export format | extensibility |
|---|---|---|---|---|---|---|---|---|
| compojure | ✓ | macros | url | |||||
| secretary | ✓ | macros | ✓ | url | protocols | |||
| bidi | ✓ | ✓ | data/functions | ✓ | url | route description data | protocols | |
| darkleaf/router | ✓ | ✓ | functions | ✓ | ✓ | resource | explain data | protocols |
(ns app.some-ns (:require [darkleaf.router :as r] [ring.util.response :refer [response]])) (r/defcontroller controller (index [req] (let [request-for (::r/request-for req)] (response (str (request-for :index [:pages] {})))))) (def routing (r/resources :pages :page controller)) (def handler (r/make-handler routing)) (def request-for (r/make-request-for routing)) (handler {:uri "/pages", :request-method :get}) ;; call index action from controller (request-for :index [:pages] {}) ;; returns {:uri "/pages", :request-method :get}Single routing namespace:
(ns app.routing (:require [darkleaf.router :as r] [app.controllers.main :as main] [app.controllers.session :as session] [app.controllers.account.invites :as account.invites] [app.controllers.users :as users] [app.controllers.users.statistics :as users.statistics] [app.controllers.users.pm-bonus :as users.pm-bonus] [app.controllers.projects :as projects] [app.controllers.projects.status :as projects.status] [app.controllers.projects.completion :as projects.completion] [app.controllers.tasks :as tasks] [app.controllers.tasks.status :as tasks.status] [app.controllers.tasks.comments :as tasks.comments])) (def routes (r/group (r/resource :main main/controller :segment false) (r/resource :session session/controller) (r/section :account (r/resources :invites :invite account.invites/controller)) (r/resources :users :user users/controller (r/resource :statistics users.statistics/controller) (r/resource :pm-bonus users.pm-bonus/controller)) (r/resources :projects :project projects/controller (r/resource :status projects.status/controller) (r/resource :completion projects.completion/controller)) (r/resources :tasks :task tasks/controller (r/resource :status tasks.status/controller) (r/resources :comments tasks.comments/controller))))Multiple routing namespaces:
(ns app.routes.main (:require [darkleaf.router :as r])) (r/defcontroller controller (show [req] ...)) (def routes (r/resource :main main-controller :segment false)) (ns app.routes (:require [darkleaf.router :as r] [app.routes.main :as main] [app.routes.session :as session] [app.routes.account :as account] [app.routes.users :as users] [app.routes.projects :as projects] [app.routes.tasks :as tasks])) (def routes (r/group main/routes session/routes account/routes users/routes projects/routes tasks/routes))- resource composition / additional controller actions
- member middleware
- extending / domain constraint
Routing libraries work similarly on all programming languages: they only map uri with a handler using templates. For example compojure, sinatra, express.js, cowboy.
There are some downsides of this approach.
- No reverse or named routing. Url is set in templates as a string.
- Absence of structure. Libraries do not offer any ways of code structure, that results of chaos in url and unclean code.
- Inability to mount an external application. Inability to create html links related with mount point.
- Inability to serialize routing and use it in other external applications for request forming.
Most of these problems are solved in Ruby on Rails.
- If you know the action, controller name and parameters, you can get url, for example: edit_admin_post_path(@post.id).
- You can use rest resources to describe routing. Actions of controllers match to handlers. However, framework allows to add non-standart actions into controller, that makes your code unlean later.
- There is an engine support. For example, you can mount a forum engine into your project or decompose your application into several engines.
- There is an API for routes traversing, which uses
rake routescommand. The library js-routes brings url helpers in js.
Solution my library suggests.
- Knowing action, scope and params, we can get the request, which invokes the handler of this route:
(request-for :edit [:admin :post] {:post "1"}). - The main abstraction is the rest resource. Controller contains only standard actions. You can see resource composition how to deal with it.
- Ability to mount an external application. See example for details.
- The library interface is identical in clojure and clojurecript, that allows to share the code between server and client using .cljc files. You can also export routing description with cross-platform templates as a simple data structure. See example for details.
| Action name | Scope | Params | Http method | Url | Type | Used for |
|---|---|---|---|---|---|---|
| index | [:pages] | {} | Get | /pages | collection | display a list of pages |
| show | [:page] | {:page 1} | Get | /pages/1 | member | display a specific page |
| new | [:page] | {} | Get | /pages/new | collection | display a form for creating new page |
| create | [:page] | {} | Post | /pages | collection | create a new page |
| edit | [:page] | {:page 1} | Get | /pages/1/edit | member | display a form for updating page |
| update | [:page] | {:page 1} | Patch | /pages/1 | member | update a specific page |
| put | [:page] | {:page 1} | Put | /pages/1 | member | upsert a specific page, may be combined with edit action |
| destroy | [:page] | {:page 1} | Delete | /pages/1 | member | delete a specific page |
(ns app.some-ns (:require [darkleaf.router :as r] [ring.util.response :refer [response]])) ;; all items are optional (r/defcontroller pages-controller (middleware [h] (fn [req] (h req))) (collection-middleware [h] (fn [req] (h req))) (member-middleware [h] (fn [req] (h req))) (index [req] (response "index resp")) (show [req] (response "show resp")) (new [req] (response "new resp")) (create [req] (response "create resp")) (edit [req] (response "edit resp")) (update [req] (response "update resp")) (put [req] (response "put resp")) (destroy [req] (response "destroy resp"))) ;; :index [:pages] {} -> /pages ;; :show [:page] {:page 1} -> /pages/1 (r/resources :pages :page pages-controller) ;; :index [:people] {} -> /menschen ;; :show [:person] {:person 1} -> /menschen/1 (r/resources :people :person people-controller :segment "menschen") ;; :index [:people] {} -> / ;; :show [:person] {:person 1} -> /1 (r/resources :people :person people-controller :segment false) ;; :put [:page :star] {:page 1} -> PUT /pages/1/star (r/resources :pages :page pages-controller (r/resource :star star-controller)There are 3 types of middlewares:
middlewareapplied to all action handlers including nested.collection-middlewareapplied only to index, new and create actions.member-middlewareapplied to show, edit, update, put, delete and all nested handlers, look here for details.
Please see test for all examples.
| Action name | Scope | Params | Http method | Url | Used for |
|---|---|---|---|---|---|
| show | [:star] | {} | Get | /star | display a specific star |
| new | [:star] | {} | Get | /star/new | display a form for creating new star |
| create | [:star] | {} | Post | /star | create a new star |
| edit | [:star] | {} | Get | /star/edit | display a form for updating star |
| update | [:star] | {} | Patch | /star | update a specific star |
| put | [:star] | {} | Put | /star | upsert a specific star, may be combined with edit action |
| destroy | [:star] | {} | Delete | /star | delete a specific star |
;; all items are optional (r/defcontroller star-controller ;; will be applied to nested routes too (middleware [h] (fn [req] (h req))) (show [req] (response "show resp")) (new [req] (response "new resp")) (create [req] (response "create resp")) (edit [req] (response "edit resp")) (update [req] (response "update resp")) (put [req] (response "put resp")) (destroy [req] (response "destroy resp"))) ;; :show [:star] {} -> /star (r/resource :star star-controller) ;; :show [:star] {} -> /estrella (r/resource :star star-controller :segment "estrella") ;; :show [:star] {} -> / (r/resource :star star-controller :segment false) ;; :index [:star :comments] {} -> /star/comments (r/resource :star star-controller (r/resources :comments :comment comments-controller)Please see test for exhaustive examples.
This function combines multiple routes into one and applies optional middleware.
(r/defcontroller posts-controller (show [req] (response "show post resp"))) (r/defcontroller news-controller (show [req] (response "show news resp"))) ;; :show [:post] {:post 1} -> /posts/1 ;; :show [:news] {:news 1} -> /news/1 (r/group (r/resources :posts :post posts-controller) (r/resources :news :news news-controller))) (r/group :middleware (fn [h] (fn [req] (h req))) (r/resources :posts :post posts-controller) (r/resources :news :news news-controller))Please see test for exhaustive examples.
;; :index [:admin :pages] {} -> /admin/pages (r/section :admin (r/resources :pages :page pages-controller)) ;; :index [:admin :pages] {} -> /private/pages (r/section :admin, :segment "private" (r/resources :pages :page pages-controller)) (r/section :admin, :middleware (fn [h] (fn [req] (h req))) (r/resources :pages :page pages-controller))Please see test for exhaustive examples.
;; :index [:locale :pages] {:locale "ru"} -> /ru/pages ;; :index [:locale :pages] {:locale "wrong"} -> not found (r/guard :locale #{"ru" "en"} (r/resources :pages :page pages-controller)) (r/guard :locale #(= "en" %) (r/resources :pages :page pages-controller)) (r/guard :locale #{"ru" "en"} :middleware (fn [h] (fn [req] (h req))) (r/resources :pages :page pages-controller))Please see test for exhaustive examples.
This function allows to mount isolated applications. request-for inside request map works regarding the mount point.
(def dashboard-app (r/resource :dashboard/main dashboard-controller :segment false)) ;; show [:admin :dashboard/main] {} -> /admin/dashboard (r/section :admin (r/mount dashboard-app :segment "dashboard")) ;; show [:admin :dashboard/main] {} -> /admin (r/section :admin (r/mount dashboard-app :segment false)) ;; show [:admin :dashboard/main] {} -> /admin (r/section :admin (r/mount dashboard-app)) (r/section :admin (r/mount dashboard-app :segment "dashboard", :middleware (fn [h] (fn [req] (h req)))))Please see test for exhaustive examples.
Passes any request in the current scope to a specified handler. Inner segments are available as (-> req ::r/params :segments). Action name is provided by request-method. It can be used for creating custom 404 page for current scope.
(defn handler (fn [req] (response "dashboard"))) ;; :get [:admin :dashboard] {} -> /admin/dashboard ;; :post [:admin :dashboard] {:segments ["private" "users"]} -> POST /admin/dashboard/private/users (r/section :admin (r/pass :dashboard handler)) ;; :get [:admin :dashboard] {} -> /admin/monitoring ;; :post [:admin :dashboard] {:segments ["private" "users"]} -> POST /admin/monitoring/private/users (r/section :admin (r/pass :dashboard handler :segment "monitoring")) ;; :get [:not-found] {} -> / ;; :post [:not-found] {:segments ["foo" "bar"]} -> POST /foo/bar (r/pass :not-found handler :segment false)Please see test for exhaustive examples.
Handler adds keys for request map:
- :darkleaf.router/action
- :darkleaf.router/scope
- :darkleaf.router/params
- :darkleaf.router/request-for
Please see test for exhaustive examples.
Asynchronous ring handlers support. It also can be used in macchiato-framework.
(r/defcontroller pages-controller (index [req resp raise] (future (resp response)))) (def pages (r/resources :pages :page pages-controller)) (def handler (r/make-handler pages)) (defn respond [val]) ;; from web server (defn error [e]) ;; from web server (handler {:request-method :get, :uri "/pages"} respond error)Please see clj test and cljs test for exhaustive examples.
(r/defcontroller people-controller (index [req] (response "index")) (show [req] (response "show"))) (def routes (r/resources :people :person people-controller)) (pprint (r/explain routes))[{:action :index, :scope [:people], :params-kmap {}, :req {:uri "/people", :request-method :get}} {:action :show, :scope [:person], :params-kmap {:person "%3Aperson"}, :req {:uri "/people{/%3Aperson}", :request-method :get}}]It useful for:
- inspection routing structure
- mistakes detection
- cross-platform routes serialization
- documentation generation
URI Template uses for templating. Url encode is applied for ability to use keywords as a template variable because of the fact that clojure keywords contains forbidden symbols. Template parameters and :params mapping is set with :params-kmap.
HTML doesn’t support HTTP methods except GET и POST. You need to add the hidden field _method with put, patch or delete value to send PUT, PATCH or DELETE request. It is also necessary to wrap a handler with darkleaf.router.html.method-override/wrap-method-override. Use it with ring.middleware.params/wrap-params and ring.middleware.keyword-params/wrap-keyword-params.
Please see examples.
In future releases I'm going to add js code for arbitrary request sending using html links.
You can create github issue with your question.
- docs
- pre, assert
Copyright © 2016 Mikhail Kuzmin
Distributed under the Eclipse Public License version 1.0.