diff --git a/CHANGELOG.md b/CHANGELOG.md index 24c725c3..618e38b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/doc/ring/ring.md b/doc/ring/ring.md index 340a1f2b..6bad7d19 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 `: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. diff --git a/examples/ring-spec-swagger/project.clj b/examples/ring-spec-swagger/project.clj index cfea302e..d8b803d6 100644 --- a/examples/ring-spec-swagger/project.clj +++ b/examples/ring-spec-swagger/project.clj @@ -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}) \ No newline at end of file diff --git a/examples/ring-swagger/project.clj b/examples/ring-swagger/project.clj index 98a21bf4..b189761b 100644 --- a/examples/ring-swagger/project.clj +++ b/examples/ring-swagger/project.clj @@ -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}) diff --git a/modules/reitit-http/src/reitit/http.cljc b/modules/reitit-http/src/reitit/http.cljc index c7a8f852..fff47f52 100644 --- a/modules/reitit-http/src/reitit/http.cljc +++ b/modules/reitit-http/src/reitit/http.cljc @@ -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 diff --git a/modules/reitit-middleware/src/reitit/ring/middleware/parameters.clj b/modules/reitit-middleware/src/reitit/ring/middleware/parameters.clj index 84355093..20eef688 100644 --- a/modules/reitit-middleware/src/reitit/ring/middleware/parameters.clj +++ b/modules/reitit-middleware/src/reitit/ring/middleware/parameters.clj @@ -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}) diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc index 3b08e113..fdb44e29 100644 --- a/modules/reitit-ring/src/reitit/ring.cljc +++ b/modules/reitit-ring/src/reitit/ring.cljc @@ -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] diff --git a/project.clj b/project.clj index 7fe4f3cc..5a7e38c0 100644 --- a/project.clj +++ b/project.clj @@ -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"] diff --git a/test/clj/reitit/http/interceptors/parameters_test.clj b/test/clj/reitit/http/interceptors/parameters_test.clj index 1a1a019a..6618a762 100644 --- a/test/clj/reitit/http/interceptors/parameters_test.clj +++ b/test/clj/reitit/http/interceptors/parameters_test.clj @@ -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"}))))) diff --git a/test/clj/reitit/http_test.clj b/test/clj/reitit/http_test.clj index 9cfa35bf..f964b049 100644 --- a/test/clj/reitit/http_test.clj +++ b/test/clj/reitit/http_test.clj @@ -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 diff --git a/test/clj/reitit/ring/middleware/parameters_test.clj b/test/clj/reitit/ring/middleware/parameters_test.clj index 880a81d9..e9658a1f 100644 --- a/test/clj/reitit/ring/middleware/parameters_test.clj +++ b/test/clj/reitit/ring/middleware/parameters_test.clj @@ -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"}))))) diff --git a/test/cljc/reitit/ring_test.cljc b/test/cljc/reitit/ring_test.cljc index 2038855d..e339c73a 100644 --- a/test/cljc/reitit/ring_test.cljc +++ b/test/cljc/reitit/ring_test.cljc @@ -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 diff --git a/test/cljc/reitit/swagger_test.clj b/test/cljc/reitit/swagger_test.clj index 9ac8fda8..ac0cd90f 100644 --- a/test/cljc/reitit/swagger_test.clj +++ b/test/cljc/reitit/swagger_test.clj @@ -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