Merge branch 'master' into feature/openapi

This commit is contained in:
Tommi Reiman 2023-01-22 14:29:22 +02:00 committed by GitHub
commit 0648296315
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 596 additions and 272 deletions

View file

@ -12,6 +12,29 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md [breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
## UNRELEASED
* Remove redundant s/and [#552](https://github.com/metosin/reitit/pull/552)
* FIX: redirect-trailing-slash-handler strips query-params [#565](https://github.com/metosin/reitit/issues/565)
* **BREAKING**: Drop tests for Clojure 1.9, run tests with 1.10 & 1.11
* NEW option `:meta-merge` on a router for custom merge strategy on route data
* Swagger: support operationId in generated swagger json [#452](https://github.com/metosin/reitit/pull/452) & [#569](https://github.com/metosin/reitit/pull/569)
* Update documentation and link to the startrek project [#578](https://github.com/metosin/reitit/pull/578)
* Upgrade jackson for CVE-2022-42003 and CVE-2022-42004 [#577](https://github.com/metosin/reitit/pull/577)
* Improved coercion errors perf [#576](https://github.com/metosin/reitit/pull/576)
* Add example for Reitit + Pedestal + Malli coercion [#572](https://github.com/metosin/reitit/pull/572)
* Handle empty seq as empty string in query-string [#566](https://github.com/metosin/reitit/pull/566)
* Polish pedestal chains when printing context diffs [#557](https://github.com/metosin/reitit/pull/557)
* Updated dependencies:
```clojure
[metosin/ring-swagger-ui "4.15.5"] is available but we use "4.3.0"
[metosin/jsonista "0.3.7"] is available but we use "0.3.5"
[metosin/malli "0.10.1"] is available but we use "0.8.2"
[fipp "0.6.26"] is available but we use "0.6.25"
[ring/ring-core "1.9.6"] is available but we use "1.9.5"
```
## 0.5.18 (2022-04-05) ## 0.5.18 (2022-04-05)
* FIX [#334](https://github.com/metosin/reitit/pull/334) - Frontend: there is no way to catch the exception if coercion fails (via [#549](https://github.com/metosin/reitit/pull/549)) * FIX [#334](https://github.com/metosin/reitit/pull/334) - Frontend: there is no way to catch the exception if coercion fails (via [#549](https://github.com/metosin/reitit/pull/549))

View file

@ -153,9 +153,13 @@ All examples are in https://github.com/metosin/reitit/tree/master/examples
## External resources ## External resources
* Simple web application using Ring/Reitit and Integrant: https://github.com/PrestanceDesign/usermanager-reitit-integrant-example * Simple web application using Ring/Reitit and Integrant: https://github.com/PrestanceDesign/usermanager-reitit-integrant-example
* A simple [ClojureScript](https://clojurescript.org/) frontend and Clojure backend using Reitit, [JUXT Clip](https://github.com/juxt/clip), [next.jdbc](https://github.com/seancorfield/next-jdbc) and other bits and bobs... * A simple Clojure backend using Reitit to serve up a RESTful API: [startrek](https://github.com/dharrigan/startrek). Technologies include:
* [startrek](https://git.sr.ht/~dharrigan/startrek) * [Donut System](https://github.com/donut-party/system)
* [startrek-ui](https://git.sr.ht/~dharrigan/startrek-ui) * [next-jdbc](https://github.com/seancorfield/next-jdbc)
* [JUXT Clip](https://github.com/juxt/clip)
* [Flyway](https://github.com/flyway/flyway)
* [HoneySQL](https://github.com/seancorfield/honeysql)
* [Babashka](https://babashka.org)
* https://www.learnreitit.com/ * https://www.learnreitit.com/
* Lipas, liikuntapalvelut: https://github.com/lipas-liikuntapaikat/lipas * Lipas, liikuntapalvelut: https://github.com/lipas-liikuntapaikat/lipas
* Implementation of the Todo-Backend API spec, using Clojure, Ring/Reitit and next-jdbc: https://github.com/PrestanceDesign/todo-backend-clojure-reitit * Implementation of the Todo-Backend API spec, using Clojure, Ring/Reitit and next-jdbc: https://github.com/PrestanceDesign/todo-backend-clojure-reitit
@ -180,6 +184,6 @@ Roadmap is mostly written in [issues](https://github.com/metosin/reitit/issues).
## License ## License
Copyright © 2017-2021 [Metosin Oy](http://www.metosin.fi) Copyright © 2017-2023 [Metosin Oy](http://www.metosin.fi)
Distributed under the Eclipse Public License, the same as Clojure. Distributed under the Eclipse Public License, the same as Clojure.

View file

@ -3,7 +3,7 @@
Routers can be configured via options. The following options are available for the `reitit.core/router`: Routers can be configured via options. The following options are available for the `reitit.core/router`:
| key | description | key | description
|--------------|------------- |---------------|-------------
| `:path` | Base-path for routes | `:path` | Base-path for routes
| `:routes` | Initial resolved routes (default `[]`) | `:routes` | Initial resolved routes (default `[]`)
| `:data` | Initial route data (default `{}`) | `:data` | Initial route data (default `{}`)
@ -11,6 +11,7 @@ Routers can be configured via options. The following options are available for t
| `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon}) | `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon})
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`) | `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil` | `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
| `:meta-merge` | Function which follows the signature of `meta-merge.core/meta-merge`, useful for when you want to have more control over the meta merging
| `:compile` | Function of `route opts => result` to compile a route handler | `:compile` | Function of `route opts => result` to compile a route handler
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects | `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes | `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes

View file

@ -23,6 +23,7 @@ The following route data keys contribute to the generated swagger specification:
| :tags | optional set of string or keyword tags for an endpoint api docs | :tags | optional set of string or keyword tags for an endpoint api docs
| :summary | optional short string summary of an endpoint | :summary | optional short string summary of an endpoint
| :description | optional long description of an endpoint. Supports http://spec.commonmark.org/ | :description | optional long description of an endpoint. Supports http://spec.commonmark.org/
| :operationId | optional string specifying the unique ID of an Operation
Coercion keys also contribute to the docs: Coercion keys also contribute to the docs:

View file

@ -0,0 +1,11 @@
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.hgignore
.hg/

View file

@ -0,0 +1,9 @@
(defproject pedestal-malli-swagger-example "0.1.0-SNAPSHOT"
:description "Reitit-http with pedestal"
:dependencies [[org.clojure/clojure "1.10.0"]
[io.pedestal/pedestal.service "0.5.5"]
[io.pedestal/pedestal.jetty "0.5.5"]
[metosin/reitit-malli "0.5.18"]
[metosin/reitit-pedestal "0.5.18"]
[metosin/reitit "0.5.18"]]
:repl-options {:init-ns server})

View file

@ -0,0 +1,164 @@
(ns example.server
(:require [clojure.java.io :as io]
[io.pedestal.http.route]
[reitit.interceptor]
[reitit.dev.pretty :as pretty]
[reitit.coercion.malli]
[io.pedestal.http]
[reitit.ring]
[reitit.ring.malli]
[reitit.http]
[reitit.pedestal]
[reitit.swagger :as swagger]
[reitit.swagger-ui :as swagger-ui]
[reitit.http.coercion :as coercion]
[reitit.http.interceptors.parameters :as parameters]
[reitit.http.interceptors.muuntaja :as muuntaja]
[reitit.http.interceptors.multipart :as multipart]
[muuntaja.core]
[malli.util :as mu]))
(defn reitit-routes
[_config]
[["/swagger.json" {:get {:no-doc true
:swagger {:info {:title "my-api"
:description "with [malli](https://github.com/metosin/malli) and reitit-ring"}
:tags [{:name "files",
:description "file api"}
{:name "math",
:description "math api"}]}
:handler (swagger/create-swagger-handler)}}]
["/files" {:swagger {:tags ["files"]}}
["/upload"
{:post {:summary "upload a file"
:parameters {:multipart [:map [:file reitit.ring.malli/temp-file-part]]}
:responses {200 {:body [:map
[:name string?]
[:size int?]]}}
:handler (fn [{{{{:keys [filename
size]} :file}
:multipart}
:parameters}]
{:status 200
:body {:name filename
:size size}})}}]
["/download" {:get {:summary "downloads a file"
:swagger {:produces ["image/png"]}
:handler (fn [_]
{:status 200
:headers {"Content-Type" "image/png"}
:body (-> "reitit.png"
(io/resource)
(io/input-stream))})}}]]
["/math" {:swagger {:tags ["math"]}}
["/plus"
{:get {:summary "plus with malli query parameters"
:parameters {:query [:map
[:x
{:title "X parameter"
:description "Description for X parameter"
:json-schema/default 42}
int?]
[:y int?]]}
:responses {200 {:body [:map [:total int?]]}}
:handler (fn [{{{:keys [x
y]}
:query}
:parameters}]
{:status 200
:body {:total (+ x y)}})}
:post {:summary "plus with malli body parameters"
:parameters {:body [:map
[:x
{:title "X parameter"
:description "Description for X parameter"
:json-schema/default 42}
int?]
[:y int?]]}
:responses {200 {:body [:map [:total int?]]}}
:handler (fn [{{{:keys [x
y]}
:body}
:parameters}]
{:status 200
:body {:total (+ x y)}})}}]]])
(defn reitit-ring-routes
[_config]
[(swagger-ui/create-swagger-ui-handler
{:path "/"
:config {:validatorUrl nil
:operationsSorter "alpha"}})
(reitit.ring/create-resource-handler)
(reitit.ring/create-default-handler)])
(defn reitit-router-config
[_config]
{:exception pretty/exception
:data {:coercion (reitit.coercion.malli/create
{:error-keys #{:coercion
:in
:schema
:value
:errors
:humanized}
:compile mu/closed-schema
:strip-extra-keys true
:default-values true
:options nil})
:muuntaja muuntaja.core/instance
:interceptors [swagger/swagger-feature
(parameters/parameters-interceptor)
(muuntaja/format-negotiate-interceptor)
(muuntaja/format-response-interceptor)
(muuntaja/format-request-interceptor)
(coercion/coerce-response-interceptor)
(coercion/coerce-request-interceptor)
(multipart/multipart-interceptor)]}})
(def config
{:env :dev
:io.pedestal.http/routes []
:io.pedestal.http/type :jetty
:io.pedestal.http/port 3000
:io.pedestal.http/join? false
:io.pedestal.http/secure-headers {:content-security-policy-settings
{:default-src "'self'"
:style-src "'self' 'unsafe-inline'"
:script-src "'self' 'unsafe-inline'"}}
::reitit-routes reitit-routes
::reitit-ring-routes reitit-ring-routes
::reitit-router-config reitit-router-config})
(defn reitit-http-router
[{::keys [reitit-routes
reitit-ring-routes
reitit-router-config]
:as config}]
(reitit.pedestal/routing-interceptor
(reitit.http/router
(reitit-routes config)
(reitit-router-config config))
(->> config
reitit-ring-routes
(apply reitit.ring/routes))))
(defonce server (atom nil))
(defn start
[server
config]
(when @server
(io.pedestal.http/stop @server)
(println "server stopped"))
(-> config
io.pedestal.http/default-interceptors
(reitit.pedestal/replace-last-interceptor (reitit-http-router config))
io.pedestal.http/dev-interceptors
io.pedestal.http/create-server
io.pedestal.http/start
(->> (reset! server)))
(println "server running in port 3000"))
#_(start server config)

View file

@ -41,36 +41,44 @@
:header (->ParameterCoercion :headers :string true true) :header (->ParameterCoercion :headers :string true true)
:path (->ParameterCoercion :path-params :string true true)}) :path (->ParameterCoercion :path-params :string true true)})
(defn ^:no-doc request-coercion-failed! [result coercion value in request] (defn ^:no-doc request-coercion-failed! [result coercion value in request serialize-failed-result]
(throw (throw
(ex-info (ex-info
(if serialize-failed-result
(str "Request coercion failed: " (pr-str result)) (str "Request coercion failed: " (pr-str result))
(merge "Request coercion failed")
(into {} result) (-> {}
{:type ::request-coercion transient
:coercion coercion (as-> $ (reduce conj! $ result))
:value value (assoc! :type ::request-coercion)
:in [:request in] (assoc! :coercion coercion)
:request request})))) (assoc! :value value)
(assoc! :in [:request in])
(assoc! :request request)
persistent!))))
(defn ^:no-doc response-coercion-failed! [result coercion value request response] (defn ^:no-doc response-coercion-failed! [result coercion value request response serialize-failed-result]
(throw (throw
(ex-info (ex-info
(if serialize-failed-result
(str "Response coercion failed: " (pr-str result)) (str "Response coercion failed: " (pr-str result))
(merge "Response coercion failed")
(into {} result) (-> {}
{:type ::response-coercion transient
:coercion coercion (as-> $ (reduce conj! $ result))
:value value (assoc! :type ::response-coercion)
:in [:response :body] (assoc! :coercion coercion)
:request request (assoc! :value value)
:response response})))) (assoc! :in [:response :body])
(assoc! :request request)
(assoc! :response response)
persistent!))))
(defn extract-request-format-default [request] (defn extract-request-format-default [request]
(-> request :muuntaja/request :format)) (-> request :muuntaja/request :format))
;; TODO: support faster key walking, walk/keywordize-keys is quite slow... ;; TODO: support faster key walking, walk/keywordize-keys is quite slow...
(defn request-coercer [coercion type model {::keys [extract-request-format parameter-coercion] (defn request-coercer [coercion type model {::keys [extract-request-format parameter-coercion serialize-failed-result]
:or {extract-request-format extract-request-format-default :or {extract-request-format extract-request-format-default
parameter-coercion default-parameter-coercion}}] parameter-coercion default-parameter-coercion}}]
(if coercion (if coercion
@ -83,13 +91,13 @@
format (extract-request-format request) format (extract-request-format request)
result (coercer value format)] result (coercer value format)]
(if (error? result) (if (error? result)
(request-coercion-failed! result coercion value in request) (request-coercion-failed! result coercion value in request serialize-failed-result)
result)))))))) result))))))))
(defn extract-response-format-default [request _] (defn extract-response-format-default [request _]
(-> request :muuntaja/response :format)) (-> request :muuntaja/response :format))
(defn response-coercer [coercion body {:keys [extract-response-format] (defn response-coercer [coercion body {:keys [extract-response-format serialize-failed-result]
:or {extract-response-format extract-response-format-default}}] :or {extract-response-format extract-response-format-default}}]
(if coercion (if coercion
(if-let [coercer (-response-coercer coercion body)] (if-let [coercer (-response-coercer coercion body)]
@ -98,7 +106,7 @@
value (:body response) value (:body response)
result (coercer value format)] result (coercer value format)]
(if (error? result) (if (error? result)
(response-coercion-failed! result coercion value request response) (response-coercion-failed! result coercion value request response serialize-failed-result)
result)))))) result))))))
(defn encode-error [data] (defn encode-error [data]

View file

@ -60,17 +60,20 @@
(defn map-data [f routes] (defn map-data [f routes]
(mapv (fn [[p ds]] [p (f p ds)]) routes)) (mapv (fn [[p ds]] [p (f p ds)]) routes))
(defn merge-data [p x] (defn meta-merge [left right opts]
((or (:meta-merge opts) mm/meta-merge) left right))
(defn merge-data [opts p x]
(reduce (reduce
(fn [acc [k v]] (fn [acc [k v]]
(try (try
(mm/meta-merge acc {k v}) (meta-merge acc {k v} opts)
(catch #?(:clj Exception, :cljs js/Error) e (catch #?(:clj Exception, :cljs js/Error) e
(ex/fail! ::merge-data {:path p, :left acc, :right {k v}, :exception e})))) (ex/fail! ::merge-data {:path p, :left acc, :right {k v}, :exception e}))))
{} x)) {} x))
(defn resolve-routes [raw-routes {:keys [coerce] :as opts}] (defn resolve-routes [raw-routes {:keys [coerce] :as opts}]
(cond->> (->> (walk raw-routes opts) (map-data merge-data)) (cond->> (->> (walk raw-routes opts) (map-data #(merge-data opts %1 %2)))
coerce (into [] (keep #(coerce % opts))))) coerce (into [] (keep #(coerce % opts)))))
(defn path-conflicting-routes [routes opts] (defn path-conflicting-routes [routes opts]
@ -249,6 +252,10 @@
(->> params (->> params
(map (fn [[k v]] (map (fn [[k v]]
(if (or (sequential? v) (set? v)) (if (or (sequential? v) (set? v))
(if (seq v)
(str/join "&" (map query-parameter (repeat k) v)) (str/join "&" (map query-parameter (repeat k) v))
;; Empty seq results in single & character in the query string.
;; Handle as empty string to behave similarly as when the value is nil.
(query-parameter k ""))
(query-parameter k v)))) (query-parameter k v))))
(str/join "&"))) (str/join "&")))

View file

@ -1,6 +1,5 @@
(ns reitit.interceptor (ns reitit.interceptor
(:require [clojure.pprint :as pprint] (:require [clojure.pprint :as pprint]
[meta-merge.core :refer [meta-merge]]
[reitit.core :as r] [reitit.core :as r]
[reitit.exception :as exception] [reitit.exception :as exception]
[reitit.impl :as impl])) [reitit.impl :as impl]))
@ -156,7 +155,7 @@
([data] ([data]
(router data nil)) (router data nil))
([data opts] ([data opts]
(let [opts (meta-merge {:compile compile-result} opts)] (let [opts (impl/meta-merge {:compile compile-result} opts opts)]
(r/router data opts)))) (r/router data opts))))
(defn interceptor-handler [router] (defn interceptor-handler [router]

View file

@ -1,6 +1,5 @@
(ns reitit.middleware (ns reitit.middleware
(:require [clojure.pprint :as pprint] (:require [clojure.pprint :as pprint]
[meta-merge.core :refer [meta-merge]]
[reitit.core :as r] [reitit.core :as r]
[reitit.exception :as exception] [reitit.exception :as exception]
[reitit.impl :as impl])) [reitit.impl :as impl]))
@ -139,7 +138,7 @@
([data] ([data]
(router data nil)) (router data nil))
([data opts] ([data opts]
(let [opts (meta-merge {:compile compile-result} opts)] (let [opts (impl/meta-merge {:compile compile-result} opts opts)]
(r/router data opts)))) (r/router data opts))))
(defn middleware-handler [router] (defn middleware-handler [router]

View file

@ -18,7 +18,7 @@
(s/nilable (s/nilable
(s/cat :path ::path (s/cat :path ::path
:arg (s/? ::arg) :arg (s/? ::arg)
:childs (s/* (s/and (s/nilable ::raw-routes)))))) :childs (s/* (s/nilable ::raw-routes)))))
(s/def ::raw-routes (s/def ::raw-routes
(s/or :route ::raw-route (s/or :route ::raw-route

View file

@ -9,8 +9,7 @@
[fipp.engine] [fipp.engine]
[fipp.visit] [fipp.visit]
[reitit.exception :as exception] [reitit.exception :as exception]
[spell-spec.expound] ;; expound [spell-spec.expound])) ;; expound
))
;; ;;
;; colors ;; colors

View file

@ -1,7 +1,7 @@
(ns reitit.http (ns reitit.http
(:require [meta-merge.core :refer [meta-merge]] (:require [reitit.core :as r]
[reitit.core :as r]
[reitit.exception :as ex] [reitit.exception :as ex]
[reitit.impl :as impl]
[reitit.interceptor :as interceptor] [reitit.interceptor :as interceptor]
[reitit.ring :as ring])) [reitit.ring :as ring]))
@ -38,7 +38,7 @@
(->methods true top) (->methods true top)
(reduce-kv (reduce-kv
(fn [acc method data] (fn [acc method data]
(let [data (meta-merge top data)] (let [data (impl/meta-merge top data opts)]
(assoc acc method (->endpoint path data method method)))) (assoc acc method (->endpoint path data method method))))
(->methods (:handler top) data) (->methods (:handler top) data)
childs)))) childs))))

View file

@ -20,7 +20,9 @@
(defn- polish [ctx] (defn- polish [ctx]
(-> ctx (-> ctx
(dissoc ::original ::previous :stack :queue) (dissoc ::original ::previous :stack :queue
:io.pedestal.interceptor.chain/stack
:io.pedestal.interceptor.chain/queue)
(update :request dissoc ::r/match ::r/router))) (update :request dissoc ::r/match ::r/router)))
(defn- handle [name stage] (defn- handle [name stage]

View file

@ -115,7 +115,7 @@
:response {:default default-transformer-provider :response {:default default-transformer-provider
:formats {"application/json" json-transformer-provider}}} :formats {"application/json" json-transformer-provider}}}
;; set of keys to include in error messages ;; set of keys to include in error messages
:error-keys #{:type :coercion :in :schema :value :errors :humanized #_:transformed} :error-keys #{:type :coercion :in #_:schema :value #_:errors :humanized #_:transformed}
;; support lite syntax? ;; support lite syntax?
:lite true :lite true
;; schema identity function (default: close all map schemas) ;; schema identity function (default: close all map schemas)

View file

@ -1,6 +1,5 @@
(ns reitit.ring (ns reitit.ring
(:require [clojure.string :as str] (:require [clojure.string :as str]
[meta-merge.core :refer [meta-merge]]
#?@(:clj [[ring.util.mime-type :as mime-type] #?@(:clj [[ring.util.mime-type :as mime-type]
[ring.util.response :as response]]) [ring.util.response :as response]])
[reitit.core :as r] [reitit.core :as r]
@ -50,21 +49,21 @@
(->methods true top) (->methods true top)
(reduce-kv (reduce-kv
(fn [acc method data] (fn [acc method data]
(let [data (meta-merge top data)] (let [data (impl/meta-merge top data opts)]
(assoc acc method (->endpoint path data method method)))) (assoc acc method (->endpoint path data method method))))
(->methods (:handler top) data) (->methods (:handler top) data)
childs)))) childs))))
(def default-options-handler (def default-options-handler
(let [handle (fn [request] (let [handler (fn [request]
(let [methods (->> request get-match :result (keep (fn [[k v]] (if v k)))) (let [methods (->> request get-match :result (keep (fn [[k v]] (if v k))))
allow (->> methods (map (comp str/upper-case name)) (str/join ","))] allow (->> methods (map (comp str/upper-case name)) (str/join ","))]
{:status 200, :body "", :headers {"Allow" allow}}))] {:status 200, :body "", :headers {"Allow" allow}}))]
(fn (fn
([request] ([request]
(handle request)) (handler request))
([request respond _] ([request respond _]
(respond (handle request)))))) (respond (handler request))))))
(def default-options-endpoint (def default-options-endpoint
{:no-doc true {:no-doc true
@ -133,10 +132,10 @@
" "
([] (redirect-trailing-slash-handler {:method :both})) ([] (redirect-trailing-slash-handler {:method :both}))
([{:keys [method]}] ([{:keys [method]}]
(letfn [(maybe-redirect [request path] (letfn [(maybe-redirect [{:keys [query-string] :as request} path]
(if (and (seq path) (r/match-by-path (::r/router request) path)) (if (and (seq path) (r/match-by-path (::r/router request) path))
{:status (if (= (:request-method request) :get) 301 308) {:status (if (= (:request-method request) :get) 301 308)
:headers {"Location" path} :headers {"Location" (if query-string (str path "?" query-string) path)}
:body ""})) :body ""}))
(redirect-handler [request] (redirect-handler [request]
(let [uri (:uri request)] (let [uri (:uri request)]

View file

@ -10,11 +10,13 @@
(s/def ::id (s/or :keyword keyword? :set (s/coll-of keyword? :into #{}))) (s/def ::id (s/or :keyword keyword? :set (s/coll-of keyword? :into #{})))
(s/def ::no-doc boolean?) (s/def ::no-doc boolean?)
(s/def ::tags (s/coll-of (s/or :keyword keyword? :string string?) :kind #{})) (s/def ::tags (s/coll-of (s/or :keyword keyword? :string string?) :kind #{}))
(s/def ::operationId string?)
(s/def ::summary string?) (s/def ::summary string?)
(s/def ::description string?) (s/def ::description string?)
(s/def ::operationId string?)
(s/def ::swagger (s/keys :opt-un [::id])) (s/def ::swagger (s/keys :opt-un [::id]))
(s/def ::spec (s/keys :opt-un [::swagger ::no-doc ::tags ::summary ::description])) (s/def ::spec (s/keys :opt-un [::swagger ::no-doc ::tags ::summary ::description ::operationId]))
(def swagger-feature (def swagger-feature
"Feature for handling swagger-documentation for routes. "Feature for handling swagger-documentation for routes.
@ -52,6 +54,7 @@
[\"/plus\" [\"/plus\"
{:get {:swagger {:tags \"math\"} {:get {:swagger {:tags \"math\"}
:operationId \"addTwoNumbers\"
:summary \"adds numbers together\" :summary \"adds numbers together\"
:description \"takes `x` and `y` query-params and adds them together\" :description \"takes `x` and `y` query-params and adds them together\"
:parameters {:query {:x int?, :y int?}} :parameters {:query {:x int?, :y int?}}
@ -75,7 +78,7 @@
(let [{:keys [id] :or {id ::default} :as swagger} (-> match :result request-method :data :swagger) (let [{:keys [id] :or {id ::default} :as swagger} (-> match :result request-method :data :swagger)
ids (trie/into-set id) ids (trie/into-set id)
strip-top-level-keys #(dissoc % :id :info :host :basePath :definitions :securityDefinitions) strip-top-level-keys #(dissoc % :id :info :host :basePath :definitions :securityDefinitions)
strip-endpoint-keys #(dissoc % :id :parameters :responses :summary :description) strip-endpoint-keys #(dissoc % :id :parameters :responses :summary :description :operationId)
swagger (->> (strip-endpoint-keys swagger) swagger (->> (strip-endpoint-keys swagger)
(merge {:swagger "2.0" (merge {:swagger "2.0"
:x-id ids})) :x-id ids}))
@ -93,7 +96,7 @@
(apply meta-merge (keep (comp :swagger :data) interceptors)) (apply meta-merge (keep (comp :swagger :data) interceptors))
(if coercion (if coercion
(coercion/get-apidocs coercion :swagger data)) (coercion/get-apidocs coercion :swagger data))
(select-keys data [:tags :summary :description]) (select-keys data [:tags :summary :description :operationId])
(strip-top-level-keys swagger))])) (strip-top-level-keys swagger))]))
transform-path (fn [[p _ c]] transform-path (fn [[p _ c]]
(if-let [endpoint (some->> c (keep transform-endpoint) (seq) (into {}))] (if-let [endpoint (some->> c (keep transform-endpoint) (seq) (into {}))]

View file

@ -27,24 +27,24 @@
[metosin/reitit-frontend "0.5.18"] [metosin/reitit-frontend "0.5.18"]
[metosin/reitit-sieppari "0.5.18"] [metosin/reitit-sieppari "0.5.18"]
[metosin/reitit-pedestal "0.5.18"] [metosin/reitit-pedestal "0.5.18"]
[metosin/ring-swagger-ui "4.3.0"] [metosin/ring-swagger-ui "4.15.5"]
[metosin/spec-tools "0.10.5"] [metosin/spec-tools "0.10.5"]
[metosin/schema-tools "0.12.3"] [metosin/schema-tools "0.12.3"]
[metosin/muuntaja "0.6.8"] [metosin/muuntaja "0.6.8"]
[metosin/jsonista "0.3.5"] [metosin/jsonista "0.3.7"]
[metosin/sieppari "0.0.0-alpha13"] [metosin/sieppari "0.0.0-alpha13"]
[metosin/malli "0.8.2"] [metosin/malli "0.10.1"]
;; https://clojureverse.org/t/depending-on-the-right-versions-of-jackson-libraries/5111 ;; https://clojureverse.org/t/depending-on-the-right-versions-of-jackson-libraries/5111
[com.fasterxml.jackson.core/jackson-core "2.13.2"] [com.fasterxml.jackson.core/jackson-core "2.14.1"]
[com.fasterxml.jackson.core/jackson-databind "2.13.2.2"] [com.fasterxml.jackson.core/jackson-databind "2.14.1"]
[meta-merge "1.0.0"] [meta-merge "1.0.0"]
[fipp "0.6.25" :exclusions [org.clojure/core.rrb-vector]] [fipp "0.6.26" :exclusions [org.clojure/core.rrb-vector]]
[expound "0.9.0"] [expound "0.9.0"]
[lambdaisland/deep-diff "0.0-47"] [lambdaisland/deep-diff "0.0-47"]
[com.bhauman/spell-spec "0.1.2"] [com.bhauman/spell-spec "0.1.2"]
[ring/ring-core "1.9.5"] [ring/ring-core "1.9.6"]
[io.pedestal/pedestal.service "0.5.10"]] [io.pedestal/pedestal.service "0.5.10"]]
@ -78,7 +78,7 @@
:java-source-paths ["modules/reitit-core/java-src"] :java-source-paths ["modules/reitit-core/java-src"]
:dependencies [[org.clojure/clojure "1.10.2"] :dependencies [[org.clojure/clojure "1.11.1"]
[org.clojure/clojurescript "1.10.773"] [org.clojure/clojurescript "1.10.773"]
;; modules dependencies ;; modules dependencies
@ -86,55 +86,55 @@
[metosin/spec-tools "0.10.5"] [metosin/spec-tools "0.10.5"]
[metosin/muuntaja "0.6.8"] [metosin/muuntaja "0.6.8"]
[metosin/sieppari "0.0.0-alpha13"] [metosin/sieppari "0.0.0-alpha13"]
[metosin/jsonista "0.3.5"] [metosin/jsonista "0.3.7"]
[metosin/malli "0.8.9"] [metosin/malli "0.10.1"]
[lambdaisland/deep-diff "0.0-47"] [lambdaisland/deep-diff "0.0-47"]
[meta-merge "1.0.0"] [meta-merge "1.0.0"]
[com.bhauman/spell-spec "0.1.2"] [com.bhauman/spell-spec "0.1.2"]
[expound "0.9.0"] [expound "0.9.0"]
[fipp "0.6.25"] [fipp "0.6.26"]
[orchestra "2021.01.01-1"] [orchestra "2021.01.01-1"]
[ring "1.9.5"] [ring "1.9.6"]
[ikitommi/immutant-web "3.0.0-alpha1"] [ikitommi/immutant-web "3.0.0-alpha1"]
[metosin/ring-http-response "0.9.3"] [metosin/ring-http-response "0.9.3"]
[metosin/ring-swagger-ui "4.3.0"] [metosin/ring-swagger-ui "4.15.5"]
[criterium "0.4.6"] [criterium "0.4.6"]
[org.clojure/test.check "1.1.1"] [org.clojure/test.check "1.1.1"]
[org.clojure/tools.namespace "1.2.0"] [org.clojure/tools.namespace "1.3.0"]
[com.gfredericks/test.chuck "0.2.13"] [com.gfredericks/test.chuck "0.2.13"]
[io.pedestal/pedestal.service "0.5.10"] [io.pedestal/pedestal.service "0.5.10"]
[org.clojure/core.async "1.5.648"] [org.clojure/core.async "1.6.673"]
[manifold "0.2.3"] [manifold "0.3.0"]
[funcool/promesa "6.1.434"] [funcool/promesa "10.0.594"]
[com.clojure-goes-fast/clj-async-profiler "0.5.1"] [com.clojure-goes-fast/clj-async-profiler "1.0.3"]
[ring-cors "0.1.13"] [ring-cors "0.1.13"]
[com.bhauman/rebel-readline "0.1.4"]]} [com.bhauman/rebel-readline "0.1.4"]]}
:1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]} :1.10 {:dependencies [[org.clojure/clojure "1.10.3"]]}
:perf {:jvm-opts ^:replace ["-server" :perf {:jvm-opts ^:replace ["-server"
"-Xmx4096m" "-Xmx4096m"
"-Dclojure.compiler.direct-linking=true"] "-Dclojure.compiler.direct-linking=true"]
:test-paths ["perf-test/clj"] :test-paths ["perf-test/clj"]
:dependencies [[compojure "1.6.2"] :dependencies [[compojure "1.7.0"]
[ring/ring-defaults "0.3.3"] [ring/ring-defaults "0.3.4"]
[ikitommi/immutant-web "3.0.0-alpha1"] [ikitommi/immutant-web "3.0.0-alpha1"]
[io.pedestal/pedestal.service "0.5.10"] [io.pedestal/pedestal.service "0.5.10"]
[io.pedestal/pedestal.jetty "0.5.10"] [io.pedestal/pedestal.jetty "0.5.10"]
[calfpath "0.8.1"] [calfpath "0.8.1"]
[org.clojure/core.async "1.5.648"] [org.clojure/core.async "1.6.673"]
[manifold "0.2.3"] [manifold "0.3.0"]
[funcool/promesa "6.1.434"] [funcool/promesa "10.0.594"]
[metosin/sieppari] [metosin/sieppari]
[yada "1.2.16"] [yada "1.2.16"]
[aleph "0.4.6"] [aleph "0.6.0"]
[ring/ring-defaults "0.3.3"] [ring/ring-defaults "0.3.4"]
[ataraxy "0.4.2"] [ataraxy "0.4.3"]
[bidi "2.1.6"] [bidi "2.1.6"]
[janus "1.3.2"]]} [janus "1.3.2"]]}
:analyze {:jvm-opts ^:replace ["-server" :analyze {:jvm-opts ^:replace ["-server"
@ -142,7 +142,7 @@
"-XX:+PrintCompilation" "-XX:+PrintCompilation"
"-XX:+UnlockDiagnosticVMOptions" "-XX:+UnlockDiagnosticVMOptions"
"-XX:+PrintInlining"]}} "-XX:+PrintInlining"]}}
:aliases {"all" ["with-profile" "dev,default:dev,default,1.9"] :aliases {"all" ["with-profile" "dev,default:dev,default,1.10"]
"perf" ["with-profile" "default,dev,perf"] "perf" ["with-profile" "default,dev,perf"]
"test-clj" ["all" "do" ["bat-test"] ["check"]] "test-clj" ["all" "do" ["bat-test"] ["check"]]
"test-browser" ["doo" "chrome-headless" "test"] "test-browser" ["doo" "chrome-headless" "test"]

View file

@ -1,6 +1,6 @@
(ns reitit.exception-test (ns reitit.exception-test
(:require [clojure.spec.alpha :as s] (:require [clojure.spec.alpha :as s]
[clojure.test :refer [are deftest is testing]] [clojure.test :refer [are deftest is]]
[reitit.core :as r] [reitit.core :as r]
[reitit.dev.pretty :as pretty] [reitit.dev.pretty :as pretty]
[reitit.exception :as exception] [reitit.exception :as exception]

View file

@ -1,5 +1,5 @@
(ns reitit.impl-test (ns reitit.impl-test
(:require [clojure.test :refer [are deftest is testing]] (:require [clojure.test :refer [are deftest is]]
[reitit.impl :as impl])) [reitit.impl :as impl]))
(deftest strip-nils-test (deftest strip-nils-test
@ -50,6 +50,8 @@
{"a" "b"} "a=b" {"a" "b"} "a=b"
{:a 1} "a=1" {:a 1} "a=1"
{:a nil} "a=" {:a nil} "a="
{:a []} "a="
{:a '()} "a="
{:a :b :c "d"} "a=b&c=d" {:a :b :c "d"} "a=b&c=d"
{:a "b c"} "a=b+c" {:a "b c"} "a=b+c"
{:a ["b" "c"]} "a=b&a=c" {:a ["b" "c"]} "a=b&a=c"

View file

@ -1,5 +1,5 @@
(ns reitit.interceptor-test (ns reitit.interceptor-test
(:require [clojure.test :refer [are deftest is testing]] (:require [clojure.test :refer [deftest is testing]]
[reitit.core :as r] [reitit.core :as r]
[reitit.interceptor :as interceptor]) [reitit.interceptor :as interceptor])
#?(:clj #?(:clj

View file

@ -1,5 +1,5 @@
(ns reitit.middleware-test (ns reitit.middleware-test
(:require [clojure.test :refer [are deftest is testing]] (:require [clojure.test :refer [deftest is testing]]
[reitit.core :as r] [reitit.core :as r]
[reitit.middleware :as middleware]) [reitit.middleware :as middleware])
#?(:clj #?(:clj

View file

@ -3,6 +3,9 @@
[malli.experimental.lite :as l] [malli.experimental.lite :as l]
#?@(:clj [[muuntaja.middleware] #?@(:clj [[muuntaja.middleware]
[jsonista.core :as j]]) [jsonista.core :as j]])
[malli.core :as m]
[malli.util :as mu]
[meta-merge.core :refer [meta-merge]]
[reitit.coercion.malli :as malli] [reitit.coercion.malli :as malli]
[reitit.coercion.schema :as schema] [reitit.coercion.schema :as schema]
[reitit.coercion.spec :as spec] [reitit.coercion.spec :as spec]
@ -208,6 +211,38 @@
(let [{:keys [status]} (app invalid-request2)] (let [{:keys [status]} (app invalid-request2)]
(is (= 500 status)))))))) (is (= 500 status))))))))
(defn- custom-meta-merge-checking-schema
([] {})
([left] left)
([left right]
(cond
(and (map? left) (map? right))
(merge-with custom-meta-merge-checking-schema left right)
(and (m/schema? left)
(m/schema? right))
(mu/merge left right)
:else
(meta-merge left right)))
([left right & more]
(reduce custom-meta-merge-checking-schema left (cons right more))))
(defn- custom-meta-merge-checking-parameters
([] {})
([left] left)
([left right]
(if (and (map? left) (map? right)
(contains? left :parameters)
(contains? right :parameters))
(-> (merge-with custom-meta-merge-checking-parameters left right)
(assoc :parameters (merge-with mu/merge
(:parameters left)
(:parameters right))))
(meta-merge left right)))
([left right & more]
(reduce custom-meta-merge-checking-parameters left (cons right more))))
(deftest malli-coercion-test (deftest malli-coercion-test
(let [create (fn [middleware routes] (let [create (fn [middleware routes]
(ring/ring-handler (ring/ring-handler
@ -524,7 +559,28 @@
(is (= {:status 200, :body {:total -4}} (call "application/json" [:int {:encode/json -}])))) (is (= {:status 200, :body {:total -4}} (call "application/json" [:int {:encode/json -}]))))
(testing "edn encoding (nada)" (testing "edn encoding (nada)"
(is (= {:status 200, :body {:total +4}} (call "application/edn" [:int {:encode/json -}])))))))) (is (= {:status 200, :body {:total +4}} (call "application/edn" [:int {:encode/json -}]))))))
(testing "using custom meta-merge function"
(let [->app (fn [schema-fn meta-merge]
(ring/ring-handler
(ring/router
["/merging-params/:foo" {:parameters {:path (schema-fn [:map [:foo :string]])}}
["/:bar" {:parameters {:path (schema-fn [:map [:bar :string]])}
:get {:handler (fn [{{{:keys [foo bar]} :path} :parameters}]
{:status 200
:body {:total (str "FOO: " foo ", "
"BAR: " bar)}})}}]]
{:data {:middleware [rrc/coerce-request-middleware
rrc/coerce-response-middleware]
:coercion malli/coercion}
:meta-merge meta-merge})))
call (fn [schema-fn meta-merge]
((->app schema-fn meta-merge) {:uri "/merging-params/this/that"
:request-method :get}))]
(is (= {:status 200, :body {:total "FOO: this, BAR: that"}} (call m/schema custom-meta-merge-checking-schema)))
(is (= {:status 200, :body {:total "FOO: this, BAR: that"}} (call identity custom-meta-merge-checking-parameters)))))))
#?(:clj #?(:clj
(deftest muuntaja-test (deftest muuntaja-test

View file

@ -1,6 +1,6 @@
(ns reitit.ring-test (ns reitit.ring-test
(:require [clojure.set :as set] (:require [clojure.set :as set]
[clojure.test :refer [deftest is testing]] [clojure.test :refer [are deftest is testing]]
[reitit.core :as r] [reitit.core :as r]
[reitit.middleware :as middleware] [reitit.middleware :as middleware]
[reitit.ring :as ring] [reitit.ring :as ring]
@ -312,7 +312,15 @@
(testing "does not strip slashes" (testing "does not strip slashes"
(is (= nil (app {:request-method :get, :uri "/slash-less/"}))) (is (= nil (app {:request-method :get, :uri "/slash-less/"})))
(is (= nil (app {:request-method :post, :uri "/slash-less/"})))))) (is (= nil (app {:request-method :post, :uri "/slash-less/"}))))
(testing "retains query-string in location header"
(are [method uri]
(is (= "/with-slash/?kikka=kukka"
(get-in (app {:request-method method :uri uri :query-string "kikka=kukka"})
[:headers "Location"])))
:get "/with-slash"
:post "/with-slash"))))
(testing "using :method :strip" (testing "using :method :strip"
(let [app (ring/ring-handler (let [app (ring/ring-handler
@ -338,7 +346,17 @@
(testing "strips multiple slashes" (testing "strips multiple slashes"
(is (= 301 (:status (app {:request-method :get, :uri "/slash-less/////"})))) (is (= 301 (:status (app {:request-method :get, :uri "/slash-less/////"}))))
(is (= 308 (:status (app {:request-method :post, :uri "/slash-less//"}))))))) (is (= 308 (:status (app {:request-method :post, :uri "/slash-less//"})))))
(testing "retains query-string in location header"
(are [method uri]
(is (= "/slash-less?kikka=kukka"
(get-in (app {:request-method method :uri uri :query-string "kikka=kukka"})
[:headers "Location"])))
:get "/slash-less/"
:get "/slash-less//"
:post "/slash-less/"
:post "/slash-less//"))))
(testing "without option (equivalent to using :method :both)" (testing "without option (equivalent to using :method :both)"
(let [app (ring/ring-handler (let [app (ring/ring-handler
@ -361,7 +379,19 @@
(testing "strips multiple slashes" (testing "strips multiple slashes"
(is (= 301 (:status (app {:request-method :get, :uri "/slash-less/////"})))) (is (= 301 (:status (app {:request-method :get, :uri "/slash-less/////"}))))
(is (= 308 (:status (app {:request-method :post, :uri "/slash-less//"}))))))))) (is (= 308 (:status (app {:request-method :post, :uri "/slash-less//"})))))
(testing "retains query-string in location header"
(are [method uri expected-location]
(is (= expected-location
(get-in (app {:request-method method :uri uri :query-string "kikka=kukka"})
[:headers "Location"])))
:get "/with-slash" "/with-slash/?kikka=kukka"
:get "/slash-less/" "/slash-less?kikka=kukka"
:get "/slash-less//" "/slash-less?kikka=kukka"
:post "/with-slash" "/with-slash/?kikka=kukka"
:post "/slash-less/" "/slash-less?kikka=kukka"
:post "/slash-less//" "/slash-less?kikka=kukka"))))))
(deftest async-ring-test (deftest async-ring-test
(let [promise #(let [value (atom ::nil)] (let [promise #(let [value (atom ::nil)]

View file

@ -39,7 +39,7 @@
(are [data] (are [data]
(is (thrown-with-msg? (is (thrown-with-msg?
ExceptionInfo ExceptionInfo
#"Call to #'reitit.core/router did not conform to spec" #"Call to (#')*reitit.core/router did not conform to spec"
(r/router (r/router
data))) data)))
@ -69,7 +69,7 @@
(are [opts] (are [opts]
(is (thrown-with-msg? (is (thrown-with-msg?
ExceptionInfo ExceptionInfo
#"Call to #'reitit.core/router did not conform to spec" #"Call to (#')*reitit.core/router did not conform to spec"
(r/router (r/router
["/api"] opts))) ["/api"] opts)))

View file

@ -25,11 +25,13 @@
["/spec" {:coercion spec/coercion} ["/spec" {:coercion spec/coercion}
["/plus/:z" ["/plus/:z"
{:patch {:summary "patch" {:patch {:summary "patch"
:operationId "Patch"
:handler (constantly {:status 200})} :handler (constantly {:status 200})}
:options {:summary "options" :options {:summary "options"
:middleware [{:data {:swagger {:responses {200 {:description "200"}}}}}] :middleware [{:data {:swagger {:responses {200 {:description "200"}}}}}]
:handler (constantly {:status 200})} :handler (constantly {:status 200})}
:get {:summary "plus" :get {:summary "plus"
:operationId "GetPlus"
:parameters {:query {:x int?, :y int?} :parameters {:query {:x int?, :y int?}
:path {:z int?}} :path {:z int?}}
:swagger {:responses {400 {:schema {:type "string"} :swagger {:responses {400 {:schema {:type "string"}
@ -118,6 +120,7 @@
(app {:request-method :get (app {:request-method :get
:uri "/api/schema/plus/3" :uri "/api/schema/plus/3"
:query-params {:x "2", :y "1"}}))))) :query-params {:x "2", :y "1"}})))))
(testing "swagger-spec" (testing "swagger-spec"
(let [spec (:body (app {:request-method :get (let [spec (:body (app {:request-method :get
:uri "/api/swagger.json"})) :uri "/api/swagger.json"}))
@ -126,6 +129,7 @@
:info {:title "my-api"} :info {:title "my-api"}
:paths {"/api/spec/plus/{z}" {:patch {:parameters [] :paths {"/api/spec/plus/{z}" {:patch {:parameters []
:summary "patch" :summary "patch"
:operationId "Patch"
:responses {:default {:description ""}}} :responses {:default {:description ""}}}
:options {:parameters [] :options {:parameters []
:summary "options" :summary "options"
@ -156,7 +160,8 @@
400 {:schema {:type "string"} 400 {:schema {:type "string"}
:description "kosh"} :description "kosh"}
500 {:description "fail"}} 500 {:description "fail"}}
:summary "plus"} :summary "plus"
:operationId "GetPlus"}
:post {:parameters [{:in "body", :post {:parameters [{:in "body",
:name "body", :name "body",
:description "", :description "",
@ -201,6 +206,7 @@
:responses {200 {:schema {:type "object" :responses {200 {:schema {:type "object"
:properties {:total {:format "int64" :properties {:total {:format "int64"
:type "integer"}} :type "integer"}}
:additionalProperties false
:required [:total]} :required [:total]}
:description ""} :description ""}
400 {:schema {:type "string"} 400 {:schema {:type "string"}
@ -224,6 +230,7 @@
:responses {200 {:description "" :responses {200 {:description ""
:schema {:properties {:total {:format "int64" :schema {:properties {:total {:format "int64"
:type "integer"}} :type "integer"}}
:additionalProperties false
:required [:total] :required [:total]
:type "object"}} :type "object"}}
400 {:schema {:type "string"} 400 {:schema {:type "string"}