{"id":13801273,"url":"https://github.com/darkleaf/router","last_synced_at":"2025-04-14T10:40:43.718Z","repository":{"id":62432252,"uuid":"67718006","full_name":"darkleaf/router","owner":"darkleaf","description":"Bidirectional Ring router. REST oriented. Rails inspired.","archived":false,"fork":false,"pushed_at":"2017-04-30T14:48:23.000Z","size":105,"stargazers_count":81,"open_issues_count":0,"forks_count":4,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-03-27T23:51:06.555Z","etag":null,"topics":["clojure","clojure-library","rest","rest-api","ring","router","routes","routing"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/darkleaf.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-09-08T15:54:59.000Z","updated_at":"2024-08-31T21:54:41.000Z","dependencies_parsed_at":"2022-11-01T21:01:05.026Z","dependency_job_id":null,"html_url":"https://github.com/darkleaf/router","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkleaf%2Frouter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkleaf%2Frouter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkleaf%2Frouter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkleaf%2Frouter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/darkleaf","download_url":"https://codeload.github.com/darkleaf/router/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248866390,"owners_count":21174533,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","clojure-library","rest","rest-api","ring","router","routes","routing"],"created_at":"2024-08-04T00:01:21.128Z","updated_at":"2025-04-14T10:40:43.699Z","avatar_url":"https://github.com/darkleaf.png","language":"Clojure","readme":"# Router\n\n[![Build Status](https://travis-ci.org/darkleaf/router.svg?branch=master)](https://travis-ci.org/darkleaf/router)\n[![Clojars Project](https://img.shields.io/clojars/v/darkleaf/router.svg)](https://clojars.org/darkleaf/router)\n\nBidirectional RESTfull Ring router for clojure and clojurescript.\n\n## Comparation\n\n| library | clj | cljs | dsl | named routes | mountable apps | abstraction | export format | extensibility |\n| --- | --- | --- | ---  | --- | --- | --- | --- | --- |\n| [compojure](https://github.com/weavejester/compojure) | ✓ |   | macros         |   |   | url      |                          |           |\n| [secretary](https://github.com/gf3/secretary)         |   | ✓ | macros         | ✓ |   | url      |                          | protocols |\n| [bidi](https://github.com/juxt/bidi)                  | ✓ | ✓ | data/functions | ✓ |   | url      | route description data   | protocols |\n| [darkleaf/router](https://github.com/darkleaf/router) | ✓ | ✓ | functions      | ✓ | ✓ | resource | [explain data](#explain) | protocols |\n\n## Usage\n\n``` clojure\n(ns app.some-ns\n  (:require [darkleaf.router :as r]\n            [ring.util.response :refer [response]]))\n\n(r/defcontroller controller\n  (index [req]\n    (let [request-for (::r/request-for req)]\n      (response (str (request-for :index [:pages] {}))))))\n\n(def routing (r/resources :pages :page controller))\n\n(def handler (r/make-handler routing))\n(def request-for (r/make-request-for routing))\n\n(handler {:uri \"/pages\", :request-method :get}) ;; call index action from controller\n(request-for :index [:pages] {}) ;; returns {:uri \"/pages\", :request-method :get}\n```\n\nSingle routing namespace:\n``` clojure\n(ns app.routing\n  (:require\n   [darkleaf.router :as r]\n   [app.controllers.main :as main]\n   [app.controllers.session :as session]\n   [app.controllers.account.invites :as account.invites]\n   [app.controllers.users :as users]\n   [app.controllers.users.statistics :as users.statistics]\n   [app.controllers.users.pm-bonus :as users.pm-bonus]\n   [app.controllers.projects :as projects]\n   [app.controllers.projects.status :as projects.status]\n   [app.controllers.projects.completion :as projects.completion]\n   [app.controllers.tasks :as tasks]\n   [app.controllers.tasks.status :as tasks.status]\n   [app.controllers.tasks.comments :as tasks.comments]))\n   \n(def routes\n  (r/group\n    (r/resource :main main/controller :segment false)\n    (r/resource :session session/controller)\n    (r/section :account\n      (r/resources :invites :invite account.invites/controller)) \n    (r/resources :users :user users/controller\n      (r/resource :statistics users.statistics/controller)\n      (r/resource :pm-bonus users.pm-bonus/controller))\n    (r/resources :projects :project projects/controller\n      (r/resource :status projects.status/controller)\n      (r/resource :completion projects.completion/controller))\n    (r/resources :tasks :task tasks/controller\n      (r/resource :status tasks.status/controller)\n      (r/resources :comments tasks.comments/controller))))\n```\n\nMultiple routing namespaces:\n``` clojure\n(ns app.routes.main\n  (:require\n   [darkleaf.router :as r]))\n\n(r/defcontroller controller\n  (show [req] ...))\n\n(def routes (r/resource :main main-controller :segment false))\n\n(ns app.routes\n  (:require\n   [darkleaf.router :as r]\n   [app.routes.main :as main]\n   [app.routes.session :as session]\n   [app.routes.account :as account]\n   [app.routes.users :as users]\n   [app.routes.projects :as projects]\n   [app.routes.tasks :as tasks]))\n\n(def routes\n  (r/group\n    main/routes\n    session/routes\n    account/routes\n    users/routes\n    projects/routes\n    tasks/routes))\n```\n\n## Use cases\n\n* [resource composition / additional controller actions](test/darkleaf/router/use_cases/resource_composition_test.cljc)\n* [member middleware](test/darkleaf/router/use_cases/member_middleware_test.cljc)\n* [extending / domain constraint](test/darkleaf/router/use_cases/domain_constraint_test.cljc)\n\n## Rationale\n\nRouting libraries work similarly on all programming languages: they only map uri with a handler using templates.\nFor example compojure, sinatra, express.js, cowboy.\n\nThere are some downsides of this approach.\n\n1. No reverse or named routing. Url is set in templates as a string.\n2. Absence of structure. Libraries do not offer any ways of code structure, that results of chaos in url and unclean code.\n3. Inability to mount an external application. Inability to create html links related with mount point.\n4. Inability to serialize routing and use it in other external applications for request forming.\n\nMost of these problems are solved in [Ruby on Rails](http://guides.rubyonrails.org/routing.html).\n\n1. If you know the action, controller name and parameters, you can get url, for example: edit_admin_post_path(@post.id).\n2. You can use rest resources to describe routing.\n   Actions of controllers match to handlers.\n   However, framework allows to add non-standart actions into controller,\n   that makes your code unlean later.\n3. There is an engine support. For example, you can mount a forum engine into your project or\n   decompose your application into several engines.\n4. There is an API for routes traversing, which uses `rake routes` command. The library\n   [js-routes](https://github.com/railsware/js-routes) brings url helpers in js.\n\nSolution my library suggests.\n\n1. Knowing action, scope and params, we can get the request, which invokes the handler of this route:\n   `(request-for :edit [:admin :post] {:post \"1\"})`.\n2. The main abstraction is the rest resource. Controller contains only standard actions.\n   You can see [resource composition](test/darkleaf/router/use_cases/resource_composition_test.cljc) how to deal with it.\n3. Ability to mount an external application. See [example](#mount) for details.\n4. The library interface is identical in clojure and clojurecript, that allows to share the code between server and\n   client using .cljc files. You can also export routing description with cross-platform templates as a simple data\n   structure. See [example](#explain) for details.\n\n## Resources\n\n| Action name | Scope | Params | Http method | Url | Type | Used for |\n| --- | --- | --- | --- | --- | --- | --- |\n| index   | [:pages] | {}        | Get    | /pages        | collection | display a list of pages  |\n| show    | [:page]  | {:page\u0026nbsp;1} | Get    | /pages/1      | member     | display a specific page |\n| new     | [:page]  | {}        | Get    | /pages/new    | collection | display a form for creating new page |\n| create  | [:page]  | {}        | Post   | /pages        | collection | create a new page |\n| edit    | [:page]  | {:page 1} | Get    | /pages/1/edit | member     | display a form for updating page |\n| update  | [:page]  | {:page 1} | Patch  | /pages/1      | member     | update a specific page |\n| put     | [:page]  | {:page 1} | Put    | /pages/1      | member     | upsert a specific page, may be combined with edit action |\n| destroy | [:page]  | {:page 1} | Delete | /pages/1      | member     | delete a specific page |\n\n``` clojure\n(ns app.some-ns\n  (:require [darkleaf.router :as r]\n            [ring.util.response :refer [response]]))\n\n;; all items are optional\n(r/defcontroller pages-controller\n  (middleware [h]\n    (fn [req] (h req)))\n  (collection-middleware [h]\n    (fn [req] (h req)))\n  (member-middleware [h]\n    (fn [req] (h req)))\n  (index [req]\n    (response \"index resp\"))\n  (show [req]\n    (response \"show resp\"))\n  (new [req]\n    (response \"new resp\"))\n  (create [req]\n    (response \"create resp\"))\n  (edit [req]\n    (response \"edit resp\"))\n  (update [req]\n    (response \"update resp\"))\n  (put [req]\n    (response \"put resp\"))\n  (destroy [req]\n    (response \"destroy resp\")))\n\n;; :index [:pages] {} -\u003e /pages\n;; :show [:page] {:page 1} -\u003e /pages/1\n(r/resources :pages :page pages-controller)\n\n;; :index [:people] {} -\u003e /menschen\n;; :show [:person] {:person 1} -\u003e /menschen/1\n(r/resources :people :person people-controller :segment \"menschen\")\n\n;; :index [:people] {} -\u003e /\n;; :show [:person] {:person 1} -\u003e /1\n(r/resources :people :person people-controller :segment false)\n\n;; :put [:page :star] {:page 1} -\u003e PUT /pages/1/star\n(r/resources :pages :page pages-controller\n  (r/resource :star star-controller)\n```\n\nThere are 3 types of middlewares:\n\n* `middleware` applied to all action handlers including nested.\n* `collection-middleware` applied only to index, new and create actions.\n* `member-middleware` applied to show, edit, update, put, delete and all nested handlers, look\n  [here](test/darkleaf/router/use_cases/member_middleware_test.cljc) for details.\n\nPlease see [test](test/darkleaf/router/resources_test.cljc) for all examples.\n\n## Resource\n\n| Action name | Scope | Params | Http method | Url | Used for\n| --- | --- | --- | --- | --- | --- |\n| show    | [:star] | {} | Get    | /star      | display a specific star |\n| new     | [:star] | {} | Get    | /star/new  | display a form for creating new star |\n| create  | [:star] | {} | Post   | /star      | create a new star |\n| edit    | [:star] | {} | Get    | /star/edit | display a form for updating star |\n| update  | [:star] | {} | Patch  | /star      | update a specific star |\n| put     | [:star] | {} | Put    | /star      | upsert a specific star, may be combined with edit action |\n| destroy | [:star] | {} | Delete | /star      | delete a specific star |\n\n``` clojure\n;; all items are optional\n(r/defcontroller star-controller\n  ;; will be applied to nested routes too\n  (middleware [h]\n    (fn [req] (h req)))\n  (show [req]\n    (response \"show resp\"))\n  (new [req]\n    (response \"new resp\"))\n  (create [req]\n    (response \"create resp\"))\n  (edit [req]\n    (response \"edit resp\"))\n  (update [req]\n    (response \"update resp\"))\n  (put [req]\n    (response \"put resp\"))\n  (destroy [req]\n    (response \"destroy resp\")))\n\n;; :show [:star] {} -\u003e /star\n(r/resource :star star-controller)\n\n;; :show [:star] {} -\u003e /estrella\n(r/resource :star star-controller :segment \"estrella\")\n\n;; :show [:star] {} -\u003e /\n(r/resource :star star-controller :segment false)\n\n;; :index [:star :comments] {} -\u003e /star/comments\n(r/resource :star star-controller\n  (r/resources :comments :comment comments-controller)\n```\n\nPlease see [test](test/darkleaf/router/resource_test.cljc) for exhaustive examples.\n\n## Group\n\nThis function combines multiple routes into one and applies optional middleware.\n\n``` clojure\n(r/defcontroller posts-controller\n  (show [req] (response \"show post resp\")))\n(r/defcontroller news-controller\n  (show [req] (response \"show news resp\")))\n\n;; :show [:post] {:post 1} -\u003e /posts/1\n;; :show [:news] {:news 1} -\u003e /news/1\n(r/group\n  (r/resources :posts :post posts-controller)\n  (r/resources :news :news news-controller)))\n\n(r/group :middleware (fn [h] (fn [req] (h req)))\n  (r/resources :posts :post posts-controller)\n  (r/resources :news :news news-controller))\n```\n\nPlease see [test](test/darkleaf/router/group_test.cljc) for exhaustive examples.\n\n## Section\n\n``` clojure\n;; :index [:admin :pages] {} -\u003e /admin/pages\n(r/section :admin\n  (r/resources :pages :page pages-controller))\n\n;; :index [:admin :pages] {} -\u003e /private/pages\n(r/section :admin, :segment \"private\"\n  (r/resources :pages :page pages-controller))\n\n(r/section :admin, :middleware (fn [h] (fn [req] (h req)))\n  (r/resources :pages :page pages-controller))\n```\n\nPlease see [test](test/darkleaf/router/section_test.cljc) for exhaustive examples.\n\n## Guard\n\n``` clojure\n;; :index [:locale :pages] {:locale \"ru\"} -\u003e /ru/pages\n;; :index [:locale :pages] {:locale \"wrong\"} -\u003e not found\n(r/guard :locale #{\"ru\" \"en\"}\n  (r/resources :pages :page pages-controller))\n\n(r/guard :locale #(= \"en\" %)\n  (r/resources :pages :page pages-controller))\n\n(r/guard :locale #{\"ru\" \"en\"} :middleware (fn [h] (fn [req] (h req)))\n  (r/resources :pages :page pages-controller))\n```\n\nPlease see [test](test/darkleaf/router/guard_test.cljc) for exhaustive examples.\n\n## Mount\n\nThis function allows to mount isolated applications. `request-for` inside `request` map works regarding the mount point.\n\n```clojure\n(def dashboard-app (r/resource :dashboard/main dashboard-controller :segment false))\n\n;; show [:admin :dashboard/main] {} -\u003e /admin/dashboard\n(r/section :admin\n  (r/mount dashboard-app :segment \"dashboard\"))\n\n;; show [:admin :dashboard/main] {} -\u003e /admin\n(r/section :admin\n  (r/mount dashboard-app :segment false))\n\n;; show [:admin :dashboard/main] {} -\u003e /admin\n(r/section :admin\n  (r/mount dashboard-app))\n\n(r/section :admin\n  (r/mount dashboard-app :segment \"dashboard\", :middleware (fn [h] (fn [req] (h req)))))\n```\n\nPlease see [test](test/darkleaf/router/mount_test.cljc) for exhaustive examples.\n\n## Pass\n\nPasses any request in the current scope to a specified handler.\nInner segments are available as `(-\u003e req ::r/params :segments)`.\nAction name is provided by request-method.\nIt can be used for creating custom 404 page for current scope.\n\n```clojure\n(defn handler (fn [req] (response \"dashboard\")))\n\n;; :get [:admin :dashboard] {} -\u003e /admin/dashboard\n;; :post [:admin :dashboard] {:segments [\"private\" \"users\"]} -\u003e POST /admin/dashboard/private/users\n(r/section :admin\n  (r/pass :dashboard handler))\n\n;; :get [:admin :dashboard] {} -\u003e /admin/monitoring\n;; :post [:admin :dashboard] {:segments [\"private\" \"users\"]} -\u003e POST /admin/monitoring/private/users\n(r/section :admin\n  (r/pass :dashboard handler :segment \"monitoring\"))\n\n;; :get [:not-found] {} -\u003e /\n;; :post [:not-found] {:segments [\"foo\" \"bar\"]} -\u003e POST /foo/bar\n(r/pass :not-found handler :segment false)\n```\n\nPlease see [test](test/darkleaf/router/pass_test.cljc) for exhaustive examples.\n\n## Additional request keys\n\nHandler adds keys for request map:\n* :darkleaf.router/action\n* :darkleaf.router/scope\n* :darkleaf.router/params\n* :darkleaf.router/request-for\n\nPlease see [test](test/darkleaf/router/additional_request_keys_test.cljc) for exhaustive examples.\n\n## Async\n\n[Asynchronous ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) handlers support.\nIt also can be used in [macchiato-framework](https://github.com/macchiato-framework/examples/tree/master/auth-example-router).\n\n``` clojure\n(r/defcontroller pages-controller\n  (index [req resp raise]\n    (future (resp response))))\n\n(def pages (r/resources :pages :page pages-controller))\n(def handler (r/make-handler pages))\n\n(defn respond [val]) ;; from web server\n(defn error [e]) ;; from web server\n\n(handler {:request-method :get, :uri \"/pages\"} respond error)\n```\n\nPlease see [clj test](test/darkleaf/router/async_test.clj)\nand [cljs test](test/darkleaf/router/async_test.cljs)\nfor exhaustive examples.\n\n## Explain\n\n```clojure\n(r/defcontroller people-controller\n  (index [req] (response \"index\"))\n  (show [req] (response \"show\")))\n\n(def routes (r/resources :people :person people-controller))\n(pprint (r/explain routes))\n```\n\n```clojure\n[{:action :index,\n  :scope [:people],\n  :params-kmap {},\n  :req {:uri \"/people\", :request-method :get}}\n {:action :show,\n  :scope [:person],\n  :params-kmap {:person \"%3Aperson\"},\n  :req {:uri \"/people{/%3Aperson}\", :request-method :get}}]\n```\n\nIt useful for:\n\n+ inspection routing structure\n+ mistakes detection\n+ cross-platform routes serialization\n+ documentation generation\n\n[URI Template](https://tools.ietf.org/html/rfc6570) uses for templating.\nUrl encode is applied for ability to use keywords as a template variable\nbecause of the fact that clojure keywords contains forbidden symbols.\nTemplate parameters and :params mapping is set with :params-kmap.\n\n## HTML\n\nHTML doesn’t support HTTP methods except GET и POST.\nYou need to add the hidden field _method with put, patch or delete value to send PUT, PATCH or DELETE request.\nIt is also necessary to wrap a handler with `darkleaf.router.html.method-override/wrap-method-override`.\nUse it with `ring.middleware.params/wrap-params` and `ring.middleware.keyword-params/wrap-keyword-params`.\n\nPlease see [examples](test/darkleaf/router/html/method_override_test.cljc).\n\nIn future releases I'm going to add js code for arbitrary request sending using html links.\n\n## Questions\n\nYou can create github issue with your question.\n\n## TODO\n\n* docs\n* pre, assert\n\n## License\n\nCopyright © 2016 Mikhail Kuzmin\n\nDistributed under the Eclipse Public License version 1.0.\n","funding_links":[],"categories":["RESTful API","Awesome ClojureScript"],"sub_categories":["Routing"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdarkleaf%2Frouter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdarkleaf%2Frouter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdarkleaf%2Frouter/lists"}