From da1cbf7121bf90e2de09497443a9011c1d261987 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 23 Sep 2018 10:52:17 +0300 Subject: [PATCH] Generate options-endpoints for ring by default --- doc/ring/ring.md | 109 +++++++++++++++++------ modules/reitit-ring/src/reitit/ring.cljc | 30 +++++-- test/cljc/reitit/ring_test.cljc | 41 +++++++++ test/cljc/reitit/swagger_test.clj | 13 +++ 4 files changed, 159 insertions(+), 34 deletions(-) diff --git a/doc/ring/ring.md b/doc/ring/ring.md index 340a1f2b..a828495d 100644 --- a/doc/ring/ring.md +++ b/doc/ring/ring.md @@ -2,15 +2,25 @@ [Ring](https://github.com/ring-clojure/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](https://github.com/ring-clojure/ring/wiki/Concepts). + ```clj [metosin/reitit-ring "0.2.2"] ``` -Ring-router adds support for [handlers](https://github.com/ring-clojure/ring/wiki/Concepts#handlers), [middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware) and routing based on `:request-method`. Ring-router is created with `reitit.ring/router` function. It uses a custom route compiler, creating a optimized data structure for handling route matches, with compiled middleware chain & handlers for all request methods. It also ensures that all routes have a `:handler` defined. `reitit.ring/ring-handler` is used to create a Ring handler out of ring-router. +## `reitit.ring/ring-router` -### Example +`ring-router` is a higher order router, which adds support for `:request-method` based routing, [handlers](https://github.com/ring-clojure/ring/wiki/Concepts#handlers) and [middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware). + + It accepts the following options: -Simple Ring app: +| 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: ```clj (require '[reitit.ring :as ring]) @@ -18,10 +28,37 @@ Simple Ring app: (defn handler [_] {:status 200, :body "ok"}) -(def app - (ring/ring-handler - (ring/router - ["/ping" handler]))) +(def router + (ring/router + ["/ping" {:get handler}])) +``` + +Match contains the ring-optimized `:result`: + +```clj +(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](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) request handling. The following options are available: + +| key | description | +| --------------|-------------| +| `:middleware` | Optional sequence of middleware that wrap the ring-handler" + +Simple Ring app: + +```clj +(def app (ring/ring-handler router)) ``` Applying the handler: @@ -36,31 +73,46 @@ Applying the handler: ; {:status 200, :body "ok"} ``` -The expanded routes shows the compilation results: +The router can be accessed via `get-router`: ```clj -(-> app (ring/get-router) (reitit/routes)) -; [["/ping" -; {:handler #object[...]} -; #Methods{:any #Endpoint{:data {:handler #object[...]}, -; :handler #object[...], -; :middleware []}}]] +(-> 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 []}}]] ``` -Note the compiled resuts as third element in the route vector. - # Request-method based routing -Handler are also looked under request-method keys: `:get`, `:head`, `:patch`, `:delete`, `:options`, `:post` or `:put`. Top-level handler is used if request-method based handler is not found. +Handlers can be placed either to the top-level (all methods) or under a spesific 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](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing). ```clj (def app (ring/ring-handler (ring/router - ["/ping" {:name ::ping - :get handler - :post handler}]))) + [["/all" handler] + ["/ping" {:name ::ping + :get handler + :post handler}]]))) +``` +Top-level handler catches all methods: + +```clj +(app {:request-method :delete, :uri "/all"}) +; {:status 200, :body "ok"} +``` + +Method-level handler catches only the method: + +```clj (app {:request-method :get, :uri "/ping"}) ; {:status 200, :body "ok"} @@ -68,19 +120,26 @@ Handler are also looked under request-method keys: `:get`, `:head`, `:patch`, `: ; nil ``` +By default, `:options` is also supported (ree router options to change this): + +```clj +(app {:request-method :options, :uri "/ping"}) +; {:status 200, :body ""} +``` + Name-based reverse routing: ```clj (-> app (ring/get-router) - (reitit/match-by-name ::ping) - :path) + (r/match-by-name ::ping) + (r/match->path)) ; "/ping" ``` # Middleware -Middleware can be added with a `:middleware` key, either to top-level or under `:request-method` submap. It's value should be a vector of any the following: +Middleware can be mounted using a `:middleware` key - either to top-level or under `:request-method` submap. It's 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 @@ -141,7 +200,3 @@ Top-level middleware, applied before any routing is done: (app {:request-method :get, :uri "/api/get"}) ; {:status 200, :body [:top :api :ok]} ``` - -# Async Ring - -All built-in middleware provide both 2 and 3-arity and are compiled for both Clojure & ClojureScript, so they work with [Async Ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) and [Node.js](https://nodejs.org) too. diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc index 3b08e113..20e8c890 100644 --- a/modules/reitit-ring/src/reitit/ring.cljc +++ b/modules/reitit-ring/src/reitit/ring.cljc @@ -25,7 +25,7 @@ (update acc method expand opts) acc)) data http-methods)]) -(defn compile-result [[path data] opts] +(defn compile-result [[path data] {:keys [::default-options-handler] :as opts}] (let [[top childs] (group-keys data) ->endpoint (fn [p d m s] (-> (middleware/compile-result [p d] opts s) @@ -37,7 +37,12 @@ (fn [acc method] (cond-> acc any? (assoc method (->endpoint path data method nil)))) - (map->Methods {}) + (map->Methods + {:options + (if default-options-handler + (->endpoint path (assoc data + :handler default-options-handler + :no-doc true) :options nil))}) http-methods))] (if-not (seq childs) (->methods true top) @@ -48,6 +53,9 @@ (->methods (:handler top) data) childs)))) +(defn default-options-handler [_] + {:status 200, :body ""}) + ;; ;; public api ;; @@ -57,6 +65,14 @@ support for http-methods and Middleware. See [docs](https://metosin.github.io/reitit/) for details. + 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 @@ -64,13 +80,13 @@ [\"/users\" {:get get-user :post update-user :delete {:middleware [wrap-delete] - :handler delete-user}}]]) - - See router options from [[reitit.core/router]] and [[reitit.middleware/router]]." + :handler delete-user}}]])" ([data] (router data nil)) ([data opts] - (let [opts (meta-merge {:coerce coerce-handler, :compile compile-result} opts)] + (let [opts (merge {:coerce coerce-handler + :compile compile-result + ::default-options-handler default-options-handler} opts)] (r/router data opts)))) (defn routes @@ -182,7 +198,7 @@ | key | description | | --------------|-------------| - | `:middleware` | Optional sequence of middleware that are wrap the [[ring-handler]]" + | `:middleware` | Optional sequence of middleware that wrap the ring-handler" ([router] (ring-handler router nil)) ([router default-handler] diff --git a/test/cljc/reitit/ring_test.cljc b/test/cljc/reitit/ring_test.cljc index 2038855d..13224848 100644 --- a/test/cljc/reitit/ring_test.cljc +++ b/test/cljc/reitit/ring_test.cljc @@ -197,6 +197,47 @@ (testing "handler rejects" (is (= -406 (:status (app {:request-method :get, :uri "/pong"})))))))))) +(deftest default-options-handler-test + (let [response {:status 200, :body "ok"} + options-response (ring/default-options-handler :request)] + + (testing "with defaults" + (let [app (ring/ring-handler + (ring/router + [["/get" {:get (constantly response)}] + ["/options" {:options (constantly response)}] + ["/any" (constantly response)]]))] + + (testing "endpoint with a non-options handler" + (is (= response (app {:request-method :get, :uri "/get"}))) + (is (= options-response (app {:request-method :options, :uri "/get"})))) + + (testing "endpoint with a options handler" + (is (= response (app {:request-method :options, :uri "/options"})))) + + (testing "endpoint with top-level handler" + (is (= response (app {:request-method :get, :uri "/any"}))) + (is (= response (app {:request-method :options, :uri "/any"})))))) + + (testing "disabled via options" + (let [app (ring/ring-handler + (ring/router + [["/get" {:get (constantly response)}] + ["/options" {:options (constantly response)}] + ["/any" (constantly response)]] + {::ring/default-options-handler nil}))] + + (testing "endpoint with a non-options handler" + (is (= response (app {:request-method :get, :uri "/get"}))) + (is (= nil (app {:request-method :options, :uri "/get"})))) + + (testing "endpoint with a options handler" + (is (= response (app {:request-method :options, :uri "/options"})))) + + (testing "endpoint with top-level handler" + (is (= response (app {:request-method :get, :uri "/any"}))) + (is (= response (app {:request-method :options, :uri "/any"})))))))) + (deftest async-ring-test (let [promise #(let [value (atom ::nil)] (fn diff --git a/test/cljc/reitit/swagger_test.clj b/test/cljc/reitit/swagger_test.clj index 9ac8fda8..e531c6e6 100644 --- a/test/cljc/reitit/swagger_test.clj +++ b/test/cljc/reitit/swagger_test.clj @@ -183,6 +183,19 @@ (-> {:request-method :get :uri "/swagger.json"} (app) :body :x-id))))) +(deftest with-options-endpoint-test + (let [app (ring/ring-handler + (ring/router + [["/ping" + {:options (constantly "options")}] + ["/swagger.json" + {:get {:no-doc true + :handler (swagger/create-swagger-handler)}}]]))] + (is (= ["/ping"] (spec-paths app "/swagger.json"))) + (is (= #{::swagger/default} + (-> {:request-method :get :uri "/swagger.json"} + (app) :body :x-id))))) + (deftest all-parameter-types-test (let [app (ring/ring-handler (ring/router