diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a14835..f2624df6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,9 +19,15 @@ ; :path "/coffee/luwak"} ``` +### `reitit-ring` + +* `reitit.ring/default-handler` now works correctly with async ring +* new helper `reitit.ring/router` to compose routes outside of a router. +* `reitit.ring/create-resource-handler` function to serve static routes. + ### `reitit-swagger` -* New module to produce swagger-docs from routing tree, including `Coercion` definitions. Works with both middleware & interceptors. +* New module to produce swagger-docs from routing tree, including `Coercion` definitions. Works with both middleware & interceptors and Schema & Spec. ```clj (require '[reitit.ring :as ring]) diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index aec6ecfd..2ed3f462 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -21,6 +21,7 @@ * [Dev Workflow](advanced/dev_workflow.md) * [Ring](ring/README.md) * [Ring-router](ring/ring.md) + * [Static Resources](ring/static.md) * [Dynamic Extensions](ring/dynamic_extensions.md) * [Data-driven Middleware](ring/data_driven_middleware.md) * [Pluggable Coercion](ring/coercion.md) @@ -28,5 +29,5 @@ * [Compiling Middleware](ring/compiling_middleware.md) * [Performance](performance.md) * [Interceptors (WIP)](interceptors.md) -* [Swagger & Openapi (WIP)](openapi.md) +* [Swagger-support](swagger.md) * [FAQ](faq.md) diff --git a/doc/ring/README.md b/doc/ring/README.md index cf4968f0..d0d88756 100644 --- a/doc/ring/README.md +++ b/doc/ring/README.md @@ -1,6 +1,7 @@ # Ring * [Ring-router](ring.md) +* [Static Resources](static.md) * [Dynamic Extensions](dynamic_extensions.md) * [Data-driven Middleware](data_driven_middleware.md) * [Pluggable Coercion](coercion.md) diff --git a/doc/ring/static.md b/doc/ring/static.md new file mode 100644 index 00000000..edc23ce5 --- /dev/null +++ b/doc/ring/static.md @@ -0,0 +1,76 @@ +# Static Resources (Clojure Only) + +Static resources can be served with a help of `reitit.ring/create-resource-handler`. It takes optionally an options map and returns a ring handler to serve files from Classpath. It returns `java.io.File` instances, so ring adapters can use NIO to effective Stream the files. + +There are two options to serve the files. + +## Internal routes + +This is good option if static files can be from non-conflicting paths, e.g. `/assets/*`. + + +```clj +(require '[reitit.ring :as ring]) + +(ring/ring-handler + (ring/router + [["/ping" (constantly {:status 200, :body "pong"})] + ["/assets/*" (ring/create-resource-handler)]]) + (ring/create-default-handler)) +``` + +## External routes + +To serve files from conflicting paths, e.g. `/*`, one option is to mount them to default-handler branch of `ring-handler`. This way, they are only served if none of the actual routes have matched. + + +```clj +(ring/ring-handler + (ring/router + ["/ping" (constantly {:status 200, :body "pong"})]) + (ring/routes + (ring/create-resource-handler {:path "/"}) + (ring/create-default-handler))) +``` + +## Configuration + +`reitit.ring/create-resource-handler` takes optionally an options map to configure how the files are being served. + +| key | description | +| -------------|-------------| +| :parameter | optional name of the wildcard parameter, defaults to unnamed keyword `:` +| :root | optional resource root, defaults to `"public"` +| :mime-types | optional extension->mime-type mapping, defaults to `reitit.ring.mime/default-types` +| :path | optional path to mount the handler to. Works only if mounted outside of a router. + +### TODO + +* support for things like `:cache`, `:last-modified?` and `:index-files` + +## Performance + +Thanks to NIO-support, serving files is quite fast. With late2015 Macbook PRO and `[ikitommi/immutant "3.0.0-alpha1"]` here are some numbers: + +##### Small file (17 bytes) + +``` +wrk -t2 -c100 -d2s http://localhost:3000/files/hello.json +34055 requests/sec +4.64MB / sec +``` + +##### large file (406kB) + +``` +wrk -t2 -c10 -d10s http://localhost:3000/files/image.jpg +2798 request/sec +1.08GB / sec +``` + +##### single huge file (775Mb) + +``` +wget http://localhost:3000/files/LilaBali2.pptx +315 MB/s +``` diff --git a/doc/swagger.md b/doc/swagger.md new file mode 100644 index 00000000..2ddc9167 --- /dev/null +++ b/doc/swagger.md @@ -0,0 +1,59 @@ +# Swagger + +Reitit supports [Swagger](https://swagger.io/) to generate route documentation. Documentation is extracted from existing coercion definitions `:parameters`, `:responses` and from a set of new doumentation keys. + +### Example + +Current `reitit-swagger` draft (with `reitit-ring` & data-specs): + + +```clj +(require '[reitit.ring :as ring]) +(require '[reitit.ring.swagger :as swagger]) +(require '[reitit.ring.coercion :as rrc]) +(require '[reitit.coercion.spec :as spec]) + +(def app + (ring/ring-handler + (ring/router + ["/api" + + ;; identify a swagger api + ;; there can be several in a routing tree + {:swagger {:id :math}} + + ;; the (undocumented) swagger spec endpoint + ["/swagger.json" + {:get {:no-doc true + :swagger {:info {:title "my-api"}} + :handler (swagger/create-swagger-handler)}}] + + ;; the (undocumented) swagger-ui + ;; [org.webjars/swagger-ui "3.13.4"] + ["/docs/*" + {:get {:no-doc true + :handler (ring/create-resource-handler + {:root "META-INF/resources/webjars/swagger-ui"})}}] + + ["/minus" + {:get {:summary "minus" + :parameters {:query {:x int?, :y int?}} + :responses {200 {:body {:total int?}}} + :handler (fn [{{{:keys [x y]} :query} :parameters}] + {:status 200, :body {:total (- x y)}})}}] + + ["/plus" + {:get {:summary "plus" + :parameters {:query {:x int?, :y int?}} + :responses {200 {:body {:total int?}}} + :handler (fn [{{{:keys [x y]} :query} :parameters}] + {:status 200, :body {:total (+ x y)}})}}]] + + {:data {:middleware [;; does not particiate in request processing + ;; just defines specs for the extra keys + swagger/swagger-middleware + rrc/coerce-exceptions-middleware + rrc/coerce-request-middleware + rrc/coerce-response-middleware] + :coercion spec/coercion}}))) +``` diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc index be7ba179..edea424c 100644 --- a/modules/reitit-ring/src/reitit/ring.cljc +++ b/modules/reitit-ring/src/reitit/ring.cljc @@ -69,16 +69,14 @@ #?(:clj (defn create-resource-handler - "A ring handler for handling classpath resources, - configured via options: + "A ring handler for serving classpath resources, configured via options: - | key | description | - | -------------|-------------| - | :parameter | optional name of the wildcard parameter, defaults to `:` - | :root | optional resource root, defaults to `public` - | :mime-types | optional extension->mime-type mapping, defaults to `reitit.ring.mime/default-types` - | :path | optional path to mount the handler to. Works only outside of a router - " + | key | description | + | -------------|-------------| + | :parameter | optional name of the wildcard parameter, defaults to unnamed keyword `:` + | :root | optional resource root, defaults to `\"public\"` + | :mime-types | optional extension->mime-type mapping, defaults to `reitit.ring.mime/default-types` + | :path | optional path to mount the handler to. Works only if mounted outside of a router." ([] (create-resource-handler nil)) ([{:keys [parameter root mime-types path] diff --git a/perf-test/clj/reitit/static_perf_test.clj b/perf-test/clj/reitit/static_perf_test.clj new file mode 100644 index 00000000..bce93d0f --- /dev/null +++ b/perf-test/clj/reitit/static_perf_test.clj @@ -0,0 +1,54 @@ +(ns reitit.static-perf-test + (:require [reitit.perf-utils :refer :all] + [immutant.web :as web] + [reitit.ring :as ring] + [clojure.java.io :as io] + [criterium.core :as cc] + [ring.util.mime-type :as mime])) + +;; +;; start repl with `lein perf repl` +;; perf measured with the following setup: +;; +;; Model Name: MacBook Pro +;; Model Identifier: MacBookPro113 +;; Processor Name: Intel Core i7 +;; Processor Speed: 2,5 GHz +;; Number of Processors: 1 +;; Total Number of Cores: 4 +;; L2 Cache (per Core): 256 KB +;; L3 Cache: 6 MB +;; Memory: 16 GB +;; + +(def app + (ring/ring-handler + (ring/router + [["/ping" (constantly {:status 200, :body "pong"})] + ["/files/*" (ring/create-resource-handler)]]) + (ring/create-default-handler))) + +(def app + (ring/ring-handler + (ring/router + ["/ping" (constantly {:status 200, :body "pong"})]) + (some-fn + (ring/create-resource-handler {:path "/files"}) + (ring/create-default-handler)))) + +(comment + (def server (web/run #'app {:port 3000, :dispatch? false, :server {:always-set-keep-alive false}})) + (routing-test)) + +(comment + (let [file (-> "logback.xml" io/resource io/file) + name (.getName file)] + + ;; 639ns + (cc/quick-bench + (mime/ext-mime-type name)) + + + ;; 106ns + (cc/quick-bench + (ext-mime-type name))))