reitit/doc/ring/ring.md
Alexander Kiel a19849fe58 Make Map Destructuring of Namespaced Keys more Beautiful
It's possible to put the :keys keyword in the namespace of the keys one likes to
destructure. With that one can use symbols in the vector again. One advantage of
having symbols is, that Cursive grays them out if not used. I found two
occurrences of unused destructured keys.
2019-07-13 17:02:41 +03:00

6.1 KiB

Ring Router

Ring is a Clojure web applications library inspired by Python's WSGI and Ruby's Rack. By abstracting the details of HTTP into a simple, unified API, Ring allows web applications to be constructed of modular components that can be shared among a variety of applications, web servers, and web frameworks.

Read more about the Ring Concepts.

[metosin/reitit-ring "0.3.9"]

reitit.ring/ring-router

ring-router is a higher order router, which adds support for :request-method based routing, handlers and middleware.

It accepts the following options:

key description
:reitit.middleware/transform Function of [Middleware] => [Middleware] to transform the expanded Middleware (default: identity).
:reitit.middleware/registry Map of keyword => IntoMiddleware to replace keyword references into Middleware
:reitit.ring/default-options-handler Default handler for :options method in endpoints (default: default-options-handler)

Example router:

(require '[reitit.ring :as ring])

(defn handler [_]
  {:status 200, :body "ok"})

(def router
  (ring/router
    ["/ping" {:get handler}]))

Match contains :result compiled by the ring-router:

(require '[reitit.core :as r])

(r/match-by-path router "/ping")
;#Match{:template "/ping"
;       :data {:get {:handler #object[...]}}
;       :result #Methods{:get #Endpoint{...}
;                        :options #Endpoint{...}}
;       :path-params {}
;       :path "/ping"}

reitit.ring/ring-handler

Given a ring-router, optional default-handler & options, ring-handler function will return a valid ring handler supporting both synchronous and asynchronous request handling. The following options are available:

key description
:middleware Optional sequence of middleware that wrap the ring-handler"
:inject-match? Boolean to inject match into request under :reitit.core/match key (default true)
:inject-router? Boolean to inject router into request under :reitit.core/router key (default true)

Simple Ring app:

(def app (ring/ring-handler router))

Applying the handler:

(app {:request-method :get, :uri "/favicon.ico"})
; nil
(app {:request-method :get, :uri "/ping"})
; {:status 200, :body "ok"}

The router can be accessed via get-router:

(-> app (ring/get-router) (r/compiled-routes))
;[["/ping"
;  {:handler #object[...]}
;  #Methods{:get #Endpoint{:data {:handler #object[...]}
;                          :handler #object[...]
;                          :middleware []}
;           :options #Endpoint{:data {:handler #object[...]}
;                              :handler #object[...]
;                              :middleware []}}]]

Request-method based routing

Handlers can be placed either to the top-level (all methods) or under a specific method (:get, :head, :patch, :delete, :options, :post, :put or :trace). Top-level handler is used if request-method based handler is not found.

By default, the :options route is generated for all paths - to enable thing like CORS.

(def app
  (ring/ring-handler
    (ring/router
      [["/all" handler]
       ["/ping" {:name ::ping
                 :get handler
                 :post handler}]])))

Top-level handler catches all methods:

(app {:request-method :delete, :uri "/all"})
; {:status 200, :body "ok"}

Method-level handler catches only the method:

(app {:request-method :get, :uri "/ping"})
; {:status 200, :body "ok"}

(app {:request-method :put, :uri "/ping"})
; nil

By default, :options is also supported (see router options to change this):

(app {:request-method :options, :uri "/ping"})
; {:status 200, :body ""}

Name-based reverse routing:

(-> app
    (ring/get-router)
    (r/match-by-name ::ping)
    (r/match->path))
; "/ping"

Middleware

Middleware can be mounted using a :middleware key - either to top-level or under request method submap. Its value should be a vector of reitit.middleware/IntoMiddleware values. These include:

  1. normal ring middleware function handler -> request -> response
  2. vector of middleware function [handler args*] -> request -> response and it's arguments
  3. a data-driven middleware record or a map
  4. a Keyword name, to lookup the middleware from a Middleware Registry

A middleware and a handler:

(defn wrap [handler id]
  (fn [request]
    (handler (update request ::acc (fnil conj []) id))))

(defn handler [{::keys [acc]}]
  {:status 200, :body (conj acc :handler)})

App with nested middleware:

(def app
  (ring/ring-handler
    (ring/router
      ;; a middleware function
      ["/api" {:middleware [#(wrap % :api)]}
       ["/ping" handler]
       ;; a middleware vector at top level
       ["/admin" {:middleware [[wrap :admin]]}
        ["/db" {:middleware [[wrap :db]]
                ;; a middleware vector at under a method
                :delete {:middleware [[wrap :delete]]
                         :handler handler}}]]])))

Middleware is applied correctly:

(app {:request-method :delete, :uri "/api/ping"})
; {:status 200, :body [:api :handler]}
(app {:request-method :delete, :uri "/api/admin/db"})
; {:status 200, :body [:api :admin :db :delete :handler]}

Top-level middleware, applied before any routing is done:

(def app
  (ring/ring-handler
    (ring/router
      ["/api" {:middleware [[mw :api]]}
       ["/get" {:get handler}]])
    nil 
    {:middleware [[mw :top]]}))

(app {:request-method :get, :uri "/api/get"})
; {:status 200, :body [:top :api :ok]}