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]
```
* `: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:
```clj
[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)
* 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.
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 `: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:
@ -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 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
(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 (see 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. 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
@ -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.

View file

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

View file

@ -14,7 +14,7 @@
(update acc method expand opts)
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)
compile (fn [[path data] opts scope]
(interceptor/compile-result [path data] opts scope))
@ -29,7 +29,12 @@
(fn [acc method]
(cond-> acc
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))]
(if-not (seq childs)
(->methods true top)
@ -45,6 +50,14 @@
support for http-methods and Interceptors. See [docs](https://metosin.github.io/reitit/)
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:
(router
@ -58,7 +71,9 @@
([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 ring/default-options-handler} opts)]
(r/router data opts))))
(defn routing-interceptor

View file

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

View file

@ -7,6 +7,9 @@
[ring.util.response :as response]])
[clojure.string :as str]))
(declare get-match)
(declare get-router)
(def http-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])
@ -25,7 +28,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 +40,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 +56,11 @@
(->methods (:handler top) data)
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
;;
@ -57,6 +70,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 +85,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 +203,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]

View file

@ -27,8 +27,8 @@
[metosin/spec-tools "0.7.1"]
[metosin/schema-tools "0.10.4"]
[metosin/ring-swagger-ui "2.2.10"]
[metosin/muuntaja "0.6.0"]
[metosin/jsonista "0.2.1"]
[metosin/muuntaja "0.6.1"]
[metosin/jsonista "0.2.2"]
[metosin/sieppari "0.0.0-alpha5"]]
:plugins [[jonase/eastwood "0.2.6"]
@ -65,10 +65,10 @@
[ring "1.7.0"]
[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/sieppari "0.0.0-alpha5"]
[metosin/jsonista "0.2.1"]
[metosin/jsonista "0.2.2"]
[criterium "0.4.4"]
[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"
(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
(let [promise #(let [value (atom ::nil)]
(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"
(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
(let [promise #(let [value (atom ::nil)]
(fn

View file

@ -183,6 +183,21 @@
(-> {: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")}]
["/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
(let [app (ring/ring-handler
(ring/router