Merge pull request #148 from metosin/Generate-Options-endpoints-by-Default

Generate options-endpoints for ring by default
This commit is contained in:
Tommi Reiman 2018-09-24 20:28:01 +03:00 committed by GitHub
commit 84501e3b40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 296 additions and 53 deletions

View file

@ -31,12 +31,40 @@
; #object[reitit.core$single_static_path_router] ; #object[reitit.core$single_static_path_router]
``` ```
* `:options` requests are served for all routes by default with 200 OK to better support things like [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)
* the default handler is not documented in Swagger
* new router option `:reitit.ring/default-options-handler` to change this behavior. Setting `nil` disables this.
* updated deps: * updated deps:
```clj ```clj
[ring/ring-core "1.7.0"] is available but we use "1.6.3" [ring/ring-core "1.7.0"] is available but we use "1.6.3"
``` ```
## `reitit-http`
* `:options` requests are served for all routes by default with 200 OK to better support things like [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)
* the default handler is not documented in Swagger
* new router option `:reitit.http/default-options-handler` to change this behavior. Setting `nil` disables this.
## `reitit-middleware`
* fix `reitit.ring.middleware.parameters/parameters-middleware`
* updated deps:
```clj
[metosin/muuntaja "0.6.1"] is available but we use "0.6.0"
```
## `reitit-swagger-ui`
* updated deps:
```clj
[metosin/jsonista "0.2.2"] is available but we use "0.2.1"
```
## 0.2.2 (2018-09-09) ## 0.2.2 (2018-09-09)
* better documentation for interceptors * better documentation for interceptors

View file

@ -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. [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 ```clj
[metosin/reitit-ring "0.2.2"] [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).
Simple Ring app: 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:
```clj ```clj
(require '[reitit.ring :as ring]) (require '[reitit.ring :as ring])
@ -18,10 +28,37 @@ Simple Ring app:
(defn handler [_] (defn handler [_]
{:status 200, :body "ok"}) {:status 200, :body "ok"})
(def app (def router
(ring/ring-handler (ring/router
(ring/router ["/ping" {:get handler}]))
["/ping" handler]))) ```
Match contains `:result` compiled by the `ring-router`:
```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: Applying the handler:
@ -36,31 +73,46 @@ Applying the handler:
; {:status 200, :body "ok"} ; {:status 200, :body "ok"}
``` ```
The expanded routes shows the compilation results: The router can be accessed via `get-router`:
```clj ```clj
(-> app (ring/get-router) (reitit/routes)) (-> app (ring/get-router) (r/compiled-routes))
; [["/ping" ;[["/ping"
; {:handler #object[...]} ; {:handler #object[...]}
; #Methods{:any #Endpoint{:data {:handler #object[...]}, ; #Methods{:get #Endpoint{:data {:handler #object[...]}
; :handler #object[...], ; :handler #object[...]
; :middleware []}}]] ; :middleware []}
; :options #Endpoint{:data {:handler #object[...]}
; :handler #object[...]
; :middleware []}}]]
``` ```
Note the compiled resuts as third element in the route vector.
# Request-method based routing # 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 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](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing).
```clj ```clj
(def app (def app
(ring/ring-handler (ring/ring-handler
(ring/router (ring/router
["/ping" {:name ::ping [["/all" handler]
:get handler ["/ping" {:name ::ping
:post handler}]))) :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"}) (app {:request-method :get, :uri "/ping"})
; {:status 200, :body "ok"} ; {:status 200, :body "ok"}
@ -68,19 +120,26 @@ Handler are also looked under request-method keys: `:get`, `:head`, `:patch`, `:
; nil ; nil
``` ```
By default, `:options` is also supported (see router options to change this):
```clj
(app {:request-method :options, :uri "/ping"})
; {:status 200, :body ""}
```
Name-based reverse routing: Name-based reverse routing:
```clj ```clj
(-> app (-> app
(ring/get-router) (ring/get-router)
(reitit/match-by-name ::ping) (r/match-by-name ::ping)
:path) (r/match->path))
; "/ping" ; "/ping"
``` ```
# Middleware # 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. Its value should be a vector of `reitit.middleware/IntoMiddleware` values. These include:
1. normal ring middleware function `handler -> request -> response` 1. normal ring middleware function `handler -> request -> response`
2. vector of middleware function `[handler args*] -> request -> response` and it's arguments 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"}) (app {:request-method :get, :uri "/api/get"})
; {:status 200, :body [:top :api :ok]} ; {: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.

View file

@ -1,7 +1,6 @@
(defproject ring-example "0.1.0-SNAPSHOT" (defproject ring-example "0.1.0-SNAPSHOT"
:description "Reitit Ring App with Swagger" :description "Reitit Ring App with Swagger"
:dependencies [[org.clojure/clojure "1.9.0"] :dependencies [[org.clojure/clojure "1.9.0"]
[ring/ring-jetty-adapter "1.7.0-RC2"] [ring/ring-jetty-adapter "1.7.0"]
[metosin/reitit "0.2.3-SNAPSHOT"]] [metosin/reitit "0.2.3-SNAPSHOT"]]
:profiles {:dev {:dependencies [[ring/ring-mock "0.3.2"]] :repl-options {:init-ns example.server})
:repl-options {:init-ns example.server}}})

View file

@ -1,6 +1,6 @@
(defproject ring-example "0.1.0-SNAPSHOT" (defproject ring-example "0.1.0-SNAPSHOT"
:description "Reitit Ring App with Swagger" :description "Reitit Ring App with Swagger"
:dependencies [[org.clojure/clojure "1.9.0"] :dependencies [[org.clojure/clojure "1.9.0"]
[ring/ring-jetty-adapter "1.7.0-RC2"] [ring/ring-jetty-adapter "1.7.0"]
[metosin/reitit "0.2.2"]] [metosin/reitit "0.2.3-SNAPSHOT"]]
:repl-options {:init-ns example.server}) :repl-options {:init-ns example.server})

View file

@ -14,7 +14,7 @@
(update acc method expand opts) (update acc method expand opts)
acc)) data ring/http-methods)]) acc)) data ring/http-methods)])
(defn compile-result [[path data] opts] (defn compile-result [[path data] {:keys [::default-options-handler] :as opts}]
(let [[top childs] (ring/group-keys data) (let [[top childs] (ring/group-keys data)
compile (fn [[path data] opts scope] compile (fn [[path data] opts scope]
(interceptor/compile-result [path data] opts scope)) (interceptor/compile-result [path data] opts scope))
@ -29,7 +29,12 @@
(fn [acc method] (fn [acc method]
(cond-> acc (cond-> acc
any? (assoc method (->endpoint path data method nil)))) any? (assoc method (->endpoint path data method nil))))
(ring/map->Methods {}) (ring/map->Methods
{:options
(if default-options-handler
(->endpoint path (assoc data
:handler default-options-handler
:no-doc true) :options nil))})
ring/http-methods))] ring/http-methods))]
(if-not (seq childs) (if-not (seq childs)
(->methods true top) (->methods true top)
@ -45,6 +50,14 @@
support for http-methods and Interceptors. See [docs](https://metosin.github.io/reitit/) support for http-methods and Interceptors. See [docs](https://metosin.github.io/reitit/)
for details. for details.
Options:
| key | description |
| ---------------------------------------|-------------|
| `:reitit.interceptor/transform` | Function of `[Interceptor] => [Interceptor]` to transform the expanded Interceptors (default: identity).
| `:reitit.interceptor/registry` | Map of `keyword => IntoInterceptor` to replace keyword references into Interceptors
| `:reitit.http/default-options-handler` | Default handler for `:options` method in endpoints (default: reitit.ring/default-options-handler)
Example: Example:
(router (router
@ -58,7 +71,9 @@
([data] ([data]
(router data nil)) (router data nil))
([data opts] ([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 ring/default-options-handler} opts)]
(r/router data opts)))) (r/router data opts))))
(defn routing-interceptor (defn routing-interceptor

View file

@ -10,6 +10,4 @@
:form-params - a map of parameters from the body :form-params - a map of parameters from the body
:params - a merged map of all types of parameter" :params - a merged map of all types of parameter"
{:name ::parameters {:name ::parameters
:enter (fn [ctx] :wrap params/wrap-params})
(let [request (:request ctx)]
(assoc ctx :request (params/params-request request))))})

View file

@ -7,6 +7,9 @@
[ring.util.response :as response]]) [ring.util.response :as response]])
[clojure.string :as str])) [clojure.string :as str]))
(declare get-match)
(declare get-router)
(def http-methods #{:get :head :post :put :delete :connect :options :trace :patch}) (def http-methods #{:get :head :post :put :delete :connect :options :trace :patch})
(defrecord Methods [get head post put delete connect options trace patch]) (defrecord Methods [get head post put delete connect options trace patch])
(defrecord Endpoint [data handler path method middleware]) (defrecord Endpoint [data handler path method middleware])
@ -25,7 +28,7 @@
(update acc method expand opts) (update acc method expand opts)
acc)) data http-methods)]) 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) (let [[top childs] (group-keys data)
->endpoint (fn [p d m s] ->endpoint (fn [p d m s]
(-> (middleware/compile-result [p d] opts s) (-> (middleware/compile-result [p d] opts s)
@ -37,7 +40,12 @@
(fn [acc method] (fn [acc method]
(cond-> acc (cond-> acc
any? (assoc method (->endpoint path data method nil)))) 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))] http-methods))]
(if-not (seq childs) (if-not (seq childs)
(->methods true top) (->methods true top)
@ -48,6 +56,11 @@
(->methods (:handler top) data) (->methods (:handler top) data)
childs)))) childs))))
(defn default-options-handler [request]
(let [methods (->> request get-match :result (keep (fn [[k v]] (if v k))))
allow (->> methods (map (comp str/upper-case name)) (str/join ","))]
{:status 200, :body "", :headers {"Allow" allow}}))
;; ;;
;; public api ;; public api
;; ;;
@ -57,6 +70,14 @@
support for http-methods and Middleware. See [docs](https://metosin.github.io/reitit/) support for http-methods and Middleware. See [docs](https://metosin.github.io/reitit/)
for details. 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: Example:
(router (router
@ -64,13 +85,13 @@
[\"/users\" {:get get-user [\"/users\" {:get get-user
:post update-user :post update-user
:delete {:middleware [wrap-delete] :delete {:middleware [wrap-delete]
:handler delete-user}}]]) :handler delete-user}}]])"
See router options from [[reitit.core/router]] and [[reitit.middleware/router]]."
([data] ([data]
(router data nil)) (router data nil))
([data opts] ([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)))) (r/router data opts))))
(defn routes (defn routes
@ -182,7 +203,7 @@
| key | description | | key | description |
| --------------|-------------| | --------------|-------------|
| `:middleware` | Optional sequence of middleware that are wrap the [[ring-handler]]" | `:middleware` | Optional sequence of middleware that wrap the ring-handler"
([router] ([router]
(ring-handler router nil)) (ring-handler router nil))
([router default-handler] ([router default-handler]

View file

@ -27,8 +27,8 @@
[metosin/spec-tools "0.7.1"] [metosin/spec-tools "0.7.1"]
[metosin/schema-tools "0.10.4"] [metosin/schema-tools "0.10.4"]
[metosin/ring-swagger-ui "2.2.10"] [metosin/ring-swagger-ui "2.2.10"]
[metosin/muuntaja "0.6.0"] [metosin/muuntaja "0.6.1"]
[metosin/jsonista "0.2.1"] [metosin/jsonista "0.2.2"]
[metosin/sieppari "0.0.0-alpha5"]] [metosin/sieppari "0.0.0-alpha5"]]
:plugins [[jonase/eastwood "0.2.6"] :plugins [[jonase/eastwood "0.2.6"]
@ -65,10 +65,10 @@
[ring "1.7.0"] [ring "1.7.0"]
[ikitommi/immutant-web "3.0.0-alpha1"] [ikitommi/immutant-web "3.0.0-alpha1"]
[metosin/muuntaja "0.6.0"] [metosin/muuntaja "0.6.1"]
[metosin/ring-swagger-ui "2.2.10"] [metosin/ring-swagger-ui "2.2.10"]
[metosin/sieppari "0.0.0-alpha5"] [metosin/sieppari "0.0.0-alpha5"]
[metosin/jsonista "0.2.1"] [metosin/jsonista "0.2.2"]
[criterium "0.4.4"] [criterium "0.4.4"]
[org.clojure/test.check "0.9.0"] [org.clojure/test.check "0.9.0"]

View file

@ -1,3 +1,17 @@
(ns reitit.http.interceptors.parameters-test) (ns reitit.http.interceptors.parameters-test
(:require [clojure.test :refer [deftest testing is]]
[reitit.http.interceptors.parameters :as parameters]
[reitit.http :as http]
[reitit.interceptor.sieppari :as sieppari]))
;; TODO (deftest parameters-test
(let [app (http/ring-handler
(http/router
["/ping" {:get #(select-keys % [:params :query-params])}]
{:data {:interceptors [(parameters/parameters-interceptor)]}})
{:executor sieppari/executor})]
(is (= {:query-params {"kikka" "kukka"}
:params {"kikka" "kukka"}}
(app {:request-method :get
:uri "/ping"
:query-string "kikka=kukka"})))))

View file

@ -169,6 +169,50 @@
(testing "handler rejects" (testing "handler rejects"
(is (= -406 (:status (app {:request-method :get, :uri "/pong"})))))))))) (is (= -406 (:status (app {:request-method :get, :uri "/pong"}))))))))))
(deftest default-options-handler-test
(let [response {:status 200, :body "ok"}]
(testing "with defaults"
(let [app (http/ring-handler
(http/router
[["/get" {:get (constantly response)
:post (constantly response)}]
["/options" {:options (constantly response)}]
["/any" (constantly response)]])
{:executor sieppari/executor})]
(testing "endpoint with a non-options handler"
(is (= response (app {:request-method :get, :uri "/get"})))
(is (= {:status 200, :body "", :headers {"Allow" "GET,POST,OPTIONS"}}
(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 (http/ring-handler
(http/router
[["/get" {:get (constantly response)}]
["/options" {:options (constantly response)}]
["/any" (constantly response)]]
{::http/default-options-handler nil})
{:executor sieppari/executor})]
(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-http-test (deftest async-http-test
(let [promise #(let [value (atom ::nil)] (let [promise #(let [value (atom ::nil)]
(fn (fn

View file

@ -1,3 +1,15 @@
(ns reitit.ring.middleware.parameters-test) (ns reitit.ring.middleware.parameters-test
(:require [clojure.test :refer [deftest testing is]]
[reitit.ring.middleware.parameters :as parameters]
[reitit.ring :as ring]))
;; TODO (deftest parameters-test
(let [app (ring/ring-handler
(ring/router
["/ping" {:get #(select-keys % [:params :query-params])}]
{:data {:middleware [parameters/parameters-middleware]}}))]
(is (= {:query-params {"kikka" "kukka"}
:params {"kikka" "kukka"}}
(app {:request-method :get
:uri "/ping"
:query-string "kikka=kukka"})))))

View file

@ -197,6 +197,48 @@
(testing "handler rejects" (testing "handler rejects"
(is (= -406 (:status (app {:request-method :get, :uri "/pong"})))))))))) (is (= -406 (:status (app {:request-method :get, :uri "/pong"}))))))))))
(deftest default-options-handler-test
(let [response {:status 200, :body "ok"}]
(testing "with defaults"
(let [app (ring/ring-handler
(ring/router
[["/get" {:get (constantly response)
:post (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 (= {:status 200, :body "", :headers {"Allow" "GET,POST,OPTIONS"}}
(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 (deftest async-ring-test
(let [promise #(let [value (atom ::nil)] (let [promise #(let [value (atom ::nil)]
(fn (fn

View file

@ -183,6 +183,21 @@
(-> {:request-method :get :uri "/swagger.json"} (-> {:request-method :get :uri "/swagger.json"}
(app) :body :x-id))))) (app) :body :x-id)))))
(deftest with-options-endpoint-test
(let [app (ring/ring-handler
(ring/router
[["/ping"
{:options (constantly "options")}]
["/pong"
(constantly "options")]
["/swagger.json"
{:get {:no-doc true
:handler (swagger/create-swagger-handler)}}]]))]
(is (= ["/ping" "/pong"] (spec-paths app "/swagger.json")))
(is (= #{::swagger/default}
(-> {:request-method :get :uri "/swagger.json"}
(app) :body :x-id)))))
(deftest all-parameter-types-test (deftest all-parameter-types-test
(let [app (ring/ring-handler (let [app (ring/ring-handler
(ring/router (ring/router