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 |
(nsapp.some-ns
(:require [darkleaf.router :as r]
[ring.util.response :refer [response]]))
(r/defcontrollercontroller
(index [req]
(let [request-for (::r/request-for req)]
(response (str (request-for:index [:pages] {}))))))
(defrouting (r/resources:pages:page controller))
(defhandler (r/make-handler routing))
(defrequest-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:
(nsapp.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]))
(defroutes
(r/group
(r/resource:main main/controller :segmentfalse)
(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:
(nsapp.routes.main
(:require
[darkleaf.router :as r]))
(r/defcontrollercontroller
(show [req] ...))
(defroutes (r/resource:main main-controller :segmentfalse))
(nsapp.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]))
(defroutes
(r/group
main/routes
session/routes
account/routes
users/routes
projects/routes
tasks/routes))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 |
(nsapp.some-ns
(:require [darkleaf.router :as r]
[ring.util.response :refer [response]]))
;; all items are optional
(r/defcontrollerpages-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 :segmentfalse)
;; :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/defcontrollerstar-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 :segmentfalse)
;; :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/defcontrollerposts-controller
(show [req] (response"show post resp")))
(r/defcontrollernews-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.
(defdashboard-app (r/resource:dashboard/main dashboard-controller :segmentfalse))
;; 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 :segmentfalse))
;; 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.
(defnhandler (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 :segmentfalse)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/defcontrollerpages-controller
(index [req resp raise]
(future (resp response))))
(defpages (r/resources:pages:page pages-controller))
(defhandler (r/make-handler pages))
(defnrespond [val]) ;; from web server
(defnerror [e]) ;; from web server
(handler {:request-method:get, :uri"/pages"} respond error)Please see clj test and cljs test for exhaustive examples.
(r/defcontrollerpeople-controller
(index [req] (response"index"))
(show [req] (response"show")))
(defroutes (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.