mirror of
https://github.com/metosin/reitit.git
synced 2025-12-22 18:41:10 +00:00
Merge pull request #341 from metosin/malli
Implement malli-based coercion
This commit is contained in:
commit
8b374678d9
23 changed files with 815 additions and 60 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -12,7 +12,7 @@ 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
|
## UNRELEASED
|
||||||
|
|
||||||
* Updated deps:
|
* Updated deps:
|
||||||
|
|
||||||
|
|
@ -29,6 +29,14 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
|
||||||
* Added ability to mark individual routes as conflicting by using `:conflicting` route data. See [documentation](https://metosin.github.io/reitit/basics/route_conflicts.html). Fixes [#324](https://github.com/metosin/reitit/issues/324)
|
* Added ability to mark individual routes as conflicting by using `:conflicting` route data. See [documentation](https://metosin.github.io/reitit/basics/route_conflicts.html). Fixes [#324](https://github.com/metosin/reitit/issues/324)
|
||||||
* Encode sequential and set values as multi-valued query params (e.g. `{:foo ["bar", "baz"]}` ↦ `foo=bar&foo=baz`).
|
* Encode sequential and set values as multi-valued query params (e.g. `{:foo ["bar", "baz"]}` ↦ `foo=bar&foo=baz`).
|
||||||
|
|
||||||
|
### `reitit-malli`
|
||||||
|
|
||||||
|
* Welcome [malli](https://github.com/metosin/malli)-based coercion! See [example project](./examples/ring-malli-swagger).
|
||||||
|
|
||||||
|
### `reitit-spec`
|
||||||
|
|
||||||
|
* `:body` coercion defaults to `spec-tools.core/strip-extra-keys-transformer`, so effectively all non-specced `s/keys` keys are stripped also for non-JSON formats.
|
||||||
|
|
||||||
### `reitit-frontend`
|
### `reitit-frontend`
|
||||||
|
|
||||||
* **BREAKING**: Decode multi-valued query params correctly into seqs (e.g. `foo=bar&foo=baz` ↦ `{:foo ["bar", "baz"]}`).
|
* **BREAKING**: Decode multi-valued query params correctly into seqs (e.g. `foo=bar&foo=baz` ↦ `{:foo ["bar", "baz"]}`).
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
# reitit [](https://circleci.com/gh/metosin/reitit) [](https://cljdoc.xyz/jump/release/metosin/reitit) [](https://clojurians.slack.com/messages/reitit/)
|
# reitit [](https://circleci.com/gh/metosin/reitit) [](https://cljdoc.xyz/jump/release/metosin/reitit) [](https://clojurians.slack.com/messages/reitit/)
|
||||||
|
|
||||||
|
|
||||||
A fast data-driven router for Clojure(Script).
|
A fast data-driven router for Clojure(Script).
|
||||||
|
|
||||||
* Simple data-driven [route syntax](https://metosin.github.io/reitit/basics/route_syntax.html)
|
* Simple data-driven [route syntax](https://metosin.github.io/reitit/basics/route_syntax.html)
|
||||||
* Route [conflict resolution](https://metosin.github.io/reitit/basics/route_conflicts.html)
|
* Route [conflict resolution](https://metosin.github.io/reitit/basics/route_conflicts.html)
|
||||||
* First-class [route data](https://metosin.github.io/reitit/basics/route_data.html)
|
* First-class [route data](https://metosin.github.io/reitit/basics/route_data.html)
|
||||||
* Bi-directional routing
|
* Bi-directional routing
|
||||||
* [Pluggable coercion](https://metosin.github.io/reitit/coercion/coercion.html) ([schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec))
|
* [Pluggable coercion](https://metosin.github.io/reitit/coercion/coercion.html) ([malli](https://github.com/metosin/malli), [schema](https://github.com/plumatic/schema) & [clojure.spec](https://clojure.org/about/spec))
|
||||||
* Helpers for [ring](https://metosin.github.io/reitit/ring/ring.html), [http](https://metosin.github.io/reitit/http/interceptors.html), [pedestal](https://metosin.github.io/reitit/http/pedestal.html) & [frontend](https://metosin.github.io/reitit/frontend/basics.html)
|
* Helpers for [ring](https://metosin.github.io/reitit/ring/ring.html), [http](https://metosin.github.io/reitit/http/interceptors.html), [pedestal](https://metosin.github.io/reitit/http/pedestal.html) & [frontend](https://metosin.github.io/reitit/frontend/basics.html)
|
||||||
* Friendly [Error Messages](https://metosin.github.io/reitit/basics/error_messages.html)
|
* Friendly [Error Messages](https://metosin.github.io/reitit/basics/error_messages.html)
|
||||||
* Extendable
|
* Extendable
|
||||||
|
|
@ -32,6 +31,7 @@ There is [#reitit](https://clojurians.slack.com/messages/reitit/) in [Clojurians
|
||||||
* `reitit-ring` - a [ring router](https://metosin.github.io/reitit/ring/ring.html)
|
* `reitit-ring` - a [ring router](https://metosin.github.io/reitit/ring/ring.html)
|
||||||
* `reitit-middleware` - [common middleware](https://metosin.github.io/reitit/ring/default_middleware.html)
|
* `reitit-middleware` - [common middleware](https://metosin.github.io/reitit/ring/default_middleware.html)
|
||||||
* `reitit-spec` [clojure.spec](https://clojure.org/about/spec) coercion
|
* `reitit-spec` [clojure.spec](https://clojure.org/about/spec) coercion
|
||||||
|
* `reitit-malli` [malli](https://github.com/metosin/malli) coercion
|
||||||
* `reitit-schema` [Schema](https://github.com/plumatic/schema) coercion
|
* `reitit-schema` [Schema](https://github.com/plumatic/schema) coercion
|
||||||
* `reitit-swagger` [Swagger2](https://swagger.io/) apidocs
|
* `reitit-swagger` [Swagger2](https://swagger.io/) apidocs
|
||||||
* `reitit-swagger-ui` Integrated [Swagger UI](https://github.com/swagger-api/swagger-ui)
|
* `reitit-swagger-ui` Integrated [Swagger UI](https://github.com/swagger-api/swagger-ui)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ To enable parameter coercion, the following things need to be done:
|
||||||
|
|
||||||
Reitit ships with the following coercion modules:
|
Reitit ships with the following coercion modules:
|
||||||
|
|
||||||
|
* `reitit.coercion.malli/coercion` for [malli](https://github.com/metosin/malli)
|
||||||
* `reitit.coercion.schema/coercion` for [plumatic schema](https://github.com/plumatic/schema)
|
* `reitit.coercion.schema/coercion` for [plumatic schema](https://github.com/plumatic/schema)
|
||||||
* `reitit.coercion.spec/coercion` for both [clojure.spec](https://clojure.org/about/spec) and [data-specs](https://github.com/metosin/spec-tools#data-specs)
|
* `reitit.coercion.spec/coercion` for both [clojure.spec](https://clojure.org/about/spec) and [data-specs](https://github.com/metosin/spec-tools#data-specs)
|
||||||
|
|
||||||
|
|
|
||||||
45
doc/coercion/malli_coercion.md
Normal file
45
doc/coercion/malli_coercion.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Malli Coercion
|
||||||
|
|
||||||
|
[Malli](https://github.com/metosin/malli) is data-driven Schema library for Clojure/Script.
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(require '[reitit.coercion.malli])
|
||||||
|
(require '[reitit.coercion :as coercion])
|
||||||
|
(require '[reitit.core :as r])
|
||||||
|
|
||||||
|
(def router
|
||||||
|
(r/router
|
||||||
|
["/:company/users/:user-id" {:name ::user-view
|
||||||
|
:coercion reitit.coercion.malli/coercion
|
||||||
|
:parameters {:path [:map
|
||||||
|
[:company string?]
|
||||||
|
[:user-id int?]]}]
|
||||||
|
{:compile coercion/compile-request-coercers}))
|
||||||
|
|
||||||
|
(defn match-by-path-and-coerce! [path]
|
||||||
|
(if-let [match (r/match-by-path router path)]
|
||||||
|
(assoc match :parameters (coercion/coerce! match))))
|
||||||
|
```
|
||||||
|
|
||||||
|
Successful coercion:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(match-by-path-and-coerce! "/metosin/users/123")
|
||||||
|
; #Match{:template "/:company/users/:user-id",
|
||||||
|
; :data {:name :user/user-view,
|
||||||
|
; :coercion <<:malli>>
|
||||||
|
; :parameters {:path [:map
|
||||||
|
; [:company string?]
|
||||||
|
; [:user-id int?]]}},
|
||||||
|
; :result {:path #object[reitit.coercion$request_coercer$]},
|
||||||
|
; :path-params {:company "metosin", :user-id "123"},
|
||||||
|
; :parameters {:path {:company "metosin", :user-id 123}}
|
||||||
|
; :path "/metosin/users/123"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Failing coercion:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(match-by-path-and-coerce! "/metosin/users/ikitommi")
|
||||||
|
; => ExceptionInfo Request coercion failed...
|
||||||
|
```
|
||||||
|
|
@ -25,6 +25,7 @@ To enable coercion, the following things need to be done:
|
||||||
|
|
||||||
Reitit ships with the following coercion modules:
|
Reitit ships with the following coercion modules:
|
||||||
|
|
||||||
|
* `reitit.coercion.malli/coercion` for [malli](https://github.com/metosin/malli)
|
||||||
* `reitit.coercion.schema/coercion` for [plumatic schema](https://github.com/plumatic/schema)
|
* `reitit.coercion.schema/coercion` for [plumatic schema](https://github.com/plumatic/schema)
|
||||||
* `reitit.coercion.spec/coercion` for both [clojure.spec](https://clojure.org/about/spec) and [data-specs](https://github.com/metosin/spec-tools#data-specs)
|
* `reitit.coercion.spec/coercion` for both [clojure.spec](https://clojure.org/about/spec) and [data-specs](https://github.com/metosin/spec-tools#data-specs)
|
||||||
|
|
||||||
|
|
|
||||||
11
examples/ring-malli-swagger/.gitignore
vendored
Normal file
11
examples/ring-malli-swagger/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
/target
|
||||||
|
/classes
|
||||||
|
/checkouts
|
||||||
|
pom.xml
|
||||||
|
pom.xml.asc
|
||||||
|
*.jar
|
||||||
|
*.class
|
||||||
|
/.lein-*
|
||||||
|
/.nrepl-port
|
||||||
|
.hgignore
|
||||||
|
.hg/
|
||||||
23
examples/ring-malli-swagger/README.md
Normal file
23
examples/ring-malli-swagger/README.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# reitit-ring, malli, swagger
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```clj
|
||||||
|
> lein repl
|
||||||
|
(start)
|
||||||
|
```
|
||||||
|
|
||||||
|
To test the endpoints using [httpie](https://httpie.org/):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
http GET :3000/math/plus x==1 y==20
|
||||||
|
http POST :3000/math/plus x:=1 y:=20
|
||||||
|
|
||||||
|
http GET :3000/swagger.json
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/metosin/reitit/master/examples/ring-spec-swagger/swagger.png" />
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright © 2017-2019 Metosin Oy
|
||||||
7
examples/ring-malli-swagger/project.clj
Normal file
7
examples/ring-malli-swagger/project.clj
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
(defproject ring-example "0.1.0-SNAPSHOT"
|
||||||
|
:description "Reitit Ring App with Swagger"
|
||||||
|
:dependencies [[org.clojure/clojure "1.10.0"]
|
||||||
|
[ring/ring-jetty-adapter "1.7.1"]
|
||||||
|
[metosin/reitit "0.3.10"]]
|
||||||
|
:repl-options {:init-ns example.server}
|
||||||
|
:profiles {:dev {:dependencies [[ring/ring-mock "0.3.2"]]}})
|
||||||
BIN
examples/ring-malli-swagger/resources/reitit.png
Normal file
BIN
examples/ring-malli-swagger/resources/reitit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 494 KiB |
115
examples/ring-malli-swagger/src/example/server.clj
Normal file
115
examples/ring-malli-swagger/src/example/server.clj
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
(ns example.server
|
||||||
|
(:require [reitit.ring :as ring]
|
||||||
|
[reitit.coercion.malli]
|
||||||
|
[reitit.ring.malli]
|
||||||
|
[reitit.swagger :as swagger]
|
||||||
|
[reitit.swagger-ui :as swagger-ui]
|
||||||
|
[reitit.ring.coercion :as coercion]
|
||||||
|
[reitit.dev.pretty :as pretty]
|
||||||
|
[reitit.ring.middleware.muuntaja :as muuntaja]
|
||||||
|
[reitit.ring.middleware.exception :as exception]
|
||||||
|
[reitit.ring.middleware.multipart :as multipart]
|
||||||
|
[reitit.ring.middleware.parameters :as parameters]
|
||||||
|
; [reitit.ring.middleware.dev :as dev]
|
||||||
|
; [reitit.ring.spec :as spec]
|
||||||
|
; [spec-tools.spell :as spell]
|
||||||
|
[ring.adapter.jetty :as jetty]
|
||||||
|
[muuntaja.core :as m]
|
||||||
|
[clojure.java.io :as io]
|
||||||
|
[malli.util :as mu]))
|
||||||
|
|
||||||
|
(def app
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
[["/swagger.json"
|
||||||
|
{:get {:no-doc true
|
||||||
|
:swagger {:info {:title "my-api"
|
||||||
|
:description "with reitit-ring"}}
|
||||||
|
: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 [file]} :multipart} :parameters}]
|
||||||
|
{:status 200
|
||||||
|
:body {:name (:filename file)
|
||||||
|
:size (:size file)}})}}]
|
||||||
|
|
||||||
|
["/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 spec query parameters"
|
||||||
|
:parameters {:query [:map [:x 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 spec body parameters"
|
||||||
|
:parameters {:body [:map [:x int?] [:y int?]]}
|
||||||
|
:responses {200 {:body [:map [:total int?]]}}
|
||||||
|
:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||||
|
{:status 200
|
||||||
|
:body {:total (+ x y)}})}}]]]
|
||||||
|
|
||||||
|
{;;:reitit.middleware/transform dev/print-request-diffs ;; pretty diffs
|
||||||
|
;;:validate spec/validate ;; enable spec validation for route data
|
||||||
|
;;:reitit.spec/wrap spell/closed ;; strict top-level validation
|
||||||
|
:exception pretty/exception
|
||||||
|
:data {:coercion (reitit.coercion.malli/create
|
||||||
|
{;; set of keys to include in error messages
|
||||||
|
:error-keys #{#_:type :coercion :in :schema :value :errors :humanized #_:transformed}
|
||||||
|
;; schema identity function (default: close all map schemas)
|
||||||
|
:compile mu/closed-schema
|
||||||
|
;; strip-extra-keys (effects only predefined transformers)
|
||||||
|
:strip-extra-keys true
|
||||||
|
;; add/set default values
|
||||||
|
:default-values true
|
||||||
|
;; malli options
|
||||||
|
:options nil})
|
||||||
|
:muuntaja m/instance
|
||||||
|
:middleware [;; swagger feature
|
||||||
|
swagger/swagger-feature
|
||||||
|
;; query-params & form-params
|
||||||
|
parameters/parameters-middleware
|
||||||
|
;; content-negotiation
|
||||||
|
muuntaja/format-negotiate-middleware
|
||||||
|
;; encoding response body
|
||||||
|
muuntaja/format-response-middleware
|
||||||
|
;; exception handling
|
||||||
|
exception/exception-middleware
|
||||||
|
;; decoding request body
|
||||||
|
muuntaja/format-request-middleware
|
||||||
|
;; coercing response bodys
|
||||||
|
coercion/coerce-response-middleware
|
||||||
|
;; coercing request parameters
|
||||||
|
coercion/coerce-request-middleware
|
||||||
|
;; multipart
|
||||||
|
multipart/multipart-middleware]}})
|
||||||
|
(ring/routes
|
||||||
|
(swagger-ui/create-swagger-ui-handler
|
||||||
|
{:path "/"
|
||||||
|
:config {:validatorUrl nil
|
||||||
|
:operationsSorter "alpha"}})
|
||||||
|
(ring/create-default-handler))))
|
||||||
|
|
||||||
|
(defn start []
|
||||||
|
(jetty/run-jetty #'app {:port 3000, :join? false})
|
||||||
|
(println "server running in port 3000"))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(start))
|
||||||
BIN
examples/ring-malli-swagger/swagger.png
Normal file
BIN
examples/ring-malli-swagger/swagger.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
38
examples/ring-malli-swagger/test/example/server_test.clj
Normal file
38
examples/ring-malli-swagger/test/example/server_test.clj
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
(ns example.server-test
|
||||||
|
(:require [clojure.test :refer :all]
|
||||||
|
[example.server :refer [app]]
|
||||||
|
[ring.mock.request :refer [request json-body]]))
|
||||||
|
|
||||||
|
(deftest example-server
|
||||||
|
|
||||||
|
(testing "GET"
|
||||||
|
(is (= (-> (request :get "/math/plus?x=20&y=3")
|
||||||
|
app :body slurp)
|
||||||
|
(-> {:request-method :get :uri "/math/plus" :query-string "x=20&y=3"}
|
||||||
|
app :body slurp)
|
||||||
|
(-> {:request-method :get :uri "/math/plus" :query-params {:x 20 :y 3}}
|
||||||
|
app :body slurp)
|
||||||
|
"{\"total\":23}")))
|
||||||
|
|
||||||
|
(testing "POST"
|
||||||
|
(is (= (-> (request :post "/math/plus") (json-body {:x 40 :y 2})
|
||||||
|
app :body slurp)
|
||||||
|
(-> {:request-method :post :uri "/math/plus" :body-params {:x 40 :y 2}}
|
||||||
|
app :body slurp)
|
||||||
|
"{\"total\":42}")))
|
||||||
|
|
||||||
|
(testing "Download"
|
||||||
|
(is (= (-> {:request-method :get :uri "/files/download"}
|
||||||
|
app :body (#(slurp % :encoding "ascii")) count) ;; binary
|
||||||
|
(.length (clojure.java.io/file "resources/reitit.png"))
|
||||||
|
506325)))
|
||||||
|
|
||||||
|
(testing "Upload"
|
||||||
|
(let [file (clojure.java.io/file "resources/reitit.png")
|
||||||
|
multipart-temp-file-part {:tempfile file
|
||||||
|
:size (.length file)
|
||||||
|
:filename (.getName file)
|
||||||
|
:content-type "image/png;"}]
|
||||||
|
(is (= (-> {:request-method :post :uri "/files/upload" :multipart-params {:file multipart-temp-file-part}}
|
||||||
|
app :body slurp)
|
||||||
|
"{\"name\":\"reitit.png\",\"size\":506325}")))))
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(defmethod print-method ::coercion [coercion ^Writer w]
|
(defmethod print-method ::coercion [coercion ^Writer w]
|
||||||
(.write w (str "<<" (-get-name coercion) ">>"))))
|
(.write w (str "#Coercion{:name " (-get-name coercion) "}"))))
|
||||||
|
|
||||||
(defrecord CoercionError [])
|
(defrecord CoercionError [])
|
||||||
|
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
(defn response-coercer [coercion body {:keys [extract-response-format]
|
(defn response-coercer [coercion body {:keys [extract-response-format]
|
||||||
:or {extract-response-format extract-response-format-default}}]
|
:or {extract-response-format extract-response-format-default}}]
|
||||||
(if coercion
|
(if coercion
|
||||||
(let [coercer (-response-coercer coercion body)]
|
(if-let [coercer (-response-coercer coercion body)]
|
||||||
(fn [request response]
|
(fn [request response]
|
||||||
(let [format (extract-response-format request response)
|
(let [format (extract-response-format request response)
|
||||||
value (:body response)
|
value (:body response)
|
||||||
|
|
@ -130,13 +130,14 @@
|
||||||
(defn response-coercers [coercion responses opts]
|
(defn response-coercers [coercion responses opts]
|
||||||
(->> (for [[status {:keys [body]}] responses :when body]
|
(->> (for [[status {:keys [body]}] responses :when body]
|
||||||
[status (response-coercer coercion body opts)])
|
[status (response-coercer coercion body opts)])
|
||||||
|
(filter second)
|
||||||
(into {})))
|
(into {})))
|
||||||
|
|
||||||
;;
|
;;
|
||||||
;; api-docs
|
;; api-docs
|
||||||
;;
|
;;
|
||||||
|
|
||||||
(defn get-apidocs [this specification data]
|
(defn get-apidocs [coercion specification data]
|
||||||
(let [swagger-parameter {:query :query
|
(let [swagger-parameter {:query :query
|
||||||
:body :body
|
:body :body
|
||||||
:form :formData
|
:form :formData
|
||||||
|
|
@ -152,7 +153,7 @@
|
||||||
(map (fn [[k v]] [(swagger-parameter k) v]))
|
(map (fn [[k v]] [(swagger-parameter k) v]))
|
||||||
(filter first)
|
(filter first)
|
||||||
(into {}))))
|
(into {}))))
|
||||||
(-get-apidocs this specification)))))
|
(-get-apidocs coercion specification)))))
|
||||||
|
|
||||||
;;
|
;;
|
||||||
;; integration
|
;; integration
|
||||||
|
|
|
||||||
13
modules/reitit-malli/project.clj
Normal file
13
modules/reitit-malli/project.clj
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
(defproject metosin/reitit-malli "0.3.10"
|
||||||
|
:description "Reitit: Malli coercion"
|
||||||
|
:url "https://github.com/metosin/reitit"
|
||||||
|
:license {:name "Eclipse Public License"
|
||||||
|
:url "http://www.eclipse.org/legal/epl-v10.html"}
|
||||||
|
:scm {:name "git"
|
||||||
|
:url "https://github.com/metosin/reitit"
|
||||||
|
:dir "../.."}
|
||||||
|
:plugins [[lein-parent "0.3.2"]]
|
||||||
|
:parent-project {:path "../../project.clj"
|
||||||
|
:inherit [:deploy-repositories :managed-dependencies]}
|
||||||
|
:dependencies [[metosin/reitit-core]
|
||||||
|
[metosin/malli]])
|
||||||
174
modules/reitit-malli/src/reitit/coercion/malli.cljc
Normal file
174
modules/reitit-malli/src/reitit/coercion/malli.cljc
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
(ns reitit.coercion.malli
|
||||||
|
(:require [reitit.coercion :as coercion]
|
||||||
|
[malli.transform :as mt]
|
||||||
|
[malli.edn :as edn]
|
||||||
|
[malli.error :as me]
|
||||||
|
[malli.util :as mu]
|
||||||
|
[malli.swagger :as swagger]
|
||||||
|
[malli.core :as m]
|
||||||
|
[clojure.set :as set]
|
||||||
|
[clojure.walk :as walk]))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; coercion
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defrecord Coercer [decoder encoder validator explainer])
|
||||||
|
|
||||||
|
(defprotocol TransformationProvider
|
||||||
|
(-transformer [this options]))
|
||||||
|
|
||||||
|
(defn- -provider [transformer]
|
||||||
|
(reify TransformationProvider
|
||||||
|
(-transformer [_ {:keys [strip-extra-keys default-values]}]
|
||||||
|
(mt/transformer
|
||||||
|
(if strip-extra-keys (mt/strip-extra-keys-transformer))
|
||||||
|
transformer
|
||||||
|
(if default-values (mt/default-value-transformer))))))
|
||||||
|
|
||||||
|
(def string-transformer-provider (-provider (mt/string-transformer)))
|
||||||
|
(def json-transformer-provider (-provider (mt/json-transformer)))
|
||||||
|
(def default-transformer-provider (-provider nil))
|
||||||
|
|
||||||
|
(defn- -coercer [schema type transformers f encoder opts]
|
||||||
|
(if schema
|
||||||
|
(let [->coercer (fn [t] (if t (->Coercer (m/decoder schema opts t)
|
||||||
|
(m/encoder schema opts t)
|
||||||
|
(m/validator schema opts)
|
||||||
|
(m/explainer schema opts))))
|
||||||
|
{:keys [formats default]} (transformers type)
|
||||||
|
default-coercer (->coercer default)
|
||||||
|
encode (or encoder (fn [value _format] value))
|
||||||
|
format-coercers (some->> (for [[f t] formats] [f (->coercer t)]) (filter second) (seq) (into {}))
|
||||||
|
get-coercer (cond format-coercers (fn [format] (or (get format-coercers format) default-coercer))
|
||||||
|
default-coercer (constantly default-coercer))]
|
||||||
|
(if get-coercer
|
||||||
|
(if (= f :decode)
|
||||||
|
;; decode -> validate
|
||||||
|
(fn [value format]
|
||||||
|
(if-let [coercer (get-coercer format)]
|
||||||
|
(let [decoder (:decoder coercer)
|
||||||
|
validator (:validator coercer)
|
||||||
|
transformed (decoder value)]
|
||||||
|
(if (validator transformed)
|
||||||
|
transformed
|
||||||
|
(let [explainer (:explainer coercer)
|
||||||
|
error (explainer transformed)]
|
||||||
|
(coercion/map->CoercionError
|
||||||
|
(assoc error :transformed transformed)))))
|
||||||
|
value))
|
||||||
|
;; decode -> validate -> encode
|
||||||
|
(fn [value format]
|
||||||
|
(if-let [coercer (get-coercer format)]
|
||||||
|
(let [decoder (:decoder coercer)
|
||||||
|
validator (:validator coercer)
|
||||||
|
transformed (decoder value)]
|
||||||
|
(if (validator transformed)
|
||||||
|
(encode transformed format)
|
||||||
|
(let [explainer (:explainer coercer)
|
||||||
|
error (explainer transformed)]
|
||||||
|
(coercion/map->CoercionError
|
||||||
|
(assoc error :transformed transformed)))))
|
||||||
|
value)))))))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; swagger
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defmulti extract-parameter (fn [in _] in))
|
||||||
|
|
||||||
|
(defmethod extract-parameter :body [_ schema]
|
||||||
|
(let [swagger-schema (swagger/transform schema {:in :body, :type :parameter})]
|
||||||
|
[{:in "body"
|
||||||
|
:name (:title swagger-schema "")
|
||||||
|
:description (:description swagger-schema "")
|
||||||
|
:required (not= :maybe (m/name schema))
|
||||||
|
:schema swagger-schema}]))
|
||||||
|
|
||||||
|
(defmethod extract-parameter :default [in schema]
|
||||||
|
(let [{:keys [properties required]} (swagger/transform schema {:in in, :type :parameter})]
|
||||||
|
(mapv
|
||||||
|
(fn [[k {:keys [type] :as schema}]]
|
||||||
|
(merge
|
||||||
|
{:in (name in)
|
||||||
|
:name k
|
||||||
|
:description (:description schema "")
|
||||||
|
:type type
|
||||||
|
:required (contains? (set required) k)}
|
||||||
|
schema))
|
||||||
|
properties)))
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; public api
|
||||||
|
;;
|
||||||
|
|
||||||
|
(def default-options
|
||||||
|
{:transformers {:body {:default default-transformer-provider
|
||||||
|
:formats {"application/json" json-transformer-provider}}
|
||||||
|
:string {:default string-transformer-provider}
|
||||||
|
:response {:default default-transformer-provider}}
|
||||||
|
;; set of keys to include in error messages
|
||||||
|
:error-keys #{:type :coercion :in :schema :value :errors :humanized #_:transformed}
|
||||||
|
;; schema identity function (default: close all map schemas)
|
||||||
|
:compile mu/closed-schema
|
||||||
|
;; strip-extra-keys (effects only predefined transformers)
|
||||||
|
:strip-extra-keys true
|
||||||
|
;; add/set default values
|
||||||
|
:default-values true
|
||||||
|
;; malli options
|
||||||
|
:options nil})
|
||||||
|
|
||||||
|
(defn create
|
||||||
|
([]
|
||||||
|
(create nil))
|
||||||
|
([opts]
|
||||||
|
(let [{:keys [transformers compile options error-keys] :as opts} (merge default-options opts)
|
||||||
|
show? (fn [key] (contains? error-keys key))
|
||||||
|
transformers (walk/prewalk #(if (satisfies? TransformationProvider %) (-transformer % opts) %) transformers)]
|
||||||
|
^{:type ::coercion/coercion}
|
||||||
|
(reify coercion/Coercion
|
||||||
|
(-get-name [_] :malli)
|
||||||
|
(-get-options [_] opts)
|
||||||
|
(-get-apidocs [_ specification {:keys [parameters responses]}]
|
||||||
|
(case specification
|
||||||
|
:swagger (merge
|
||||||
|
(if parameters
|
||||||
|
{:parameters
|
||||||
|
(->> (for [[in schema] parameters
|
||||||
|
parameter (extract-parameter in (compile schema))]
|
||||||
|
parameter)
|
||||||
|
(into []))})
|
||||||
|
(if responses
|
||||||
|
{:responses
|
||||||
|
(into
|
||||||
|
(empty responses)
|
||||||
|
(for [[status response] responses]
|
||||||
|
[status (as-> response $
|
||||||
|
(set/rename-keys $ {:body :schema})
|
||||||
|
(update $ :description (fnil identity ""))
|
||||||
|
(if (:schema $)
|
||||||
|
(-> $
|
||||||
|
(update :schema compile)
|
||||||
|
(update :schema swagger/transform {:type :schema}))
|
||||||
|
$))]))}))
|
||||||
|
(throw
|
||||||
|
(ex-info
|
||||||
|
(str "Can't produce Schema apidocs for " specification)
|
||||||
|
{:type specification, :coercion :schema}))))
|
||||||
|
(-compile-model [_ model _] (compile model))
|
||||||
|
(-open-model [_ schema] schema)
|
||||||
|
(-encode-error [_ error]
|
||||||
|
(cond-> error
|
||||||
|
(show? :humanized) (assoc :humanized (me/humanize error {:wrap :message}))
|
||||||
|
(show? :schema) (update :schema edn/write-string opts)
|
||||||
|
(show? :errors) (-> (me/with-error-messages opts)
|
||||||
|
(update :errors (partial map #(update % :schema edn/write-string opts))))
|
||||||
|
(seq error-keys) (select-keys error-keys)))
|
||||||
|
(-request-coercer [_ type schema]
|
||||||
|
(-coercer (compile schema) type transformers :decode nil options))
|
||||||
|
(-response-coercer [_ schema]
|
||||||
|
(let [schema (compile schema)
|
||||||
|
encoder (-coercer schema :body transformers :encode nil options)]
|
||||||
|
(-coercer schema :response transformers :encode encoder options)))))))
|
||||||
|
|
||||||
|
(def coercion (create default-options))
|
||||||
19
modules/reitit-malli/src/reitit/ring/malli.cljc
Normal file
19
modules/reitit-malli/src/reitit/ring/malli.cljc
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
(ns reitit.ring.malli
|
||||||
|
#?(:clj (:import (java.io File))))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(def temp-file-part
|
||||||
|
"Schema for file param created by ring.middleware.multipart-params.temp-file store."
|
||||||
|
[:map {:json-schema {:type "file"}}
|
||||||
|
[:filename string?]
|
||||||
|
[:content-type string?]
|
||||||
|
[:size int?]
|
||||||
|
[:tempfile [:fn (partial instance? File)]]]))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(def bytes-part
|
||||||
|
"Schema for file param created by ring.middleware.multipart-params.byte-array store."
|
||||||
|
[:map {:json-schema {:type "file"}}
|
||||||
|
[:filename string?]
|
||||||
|
[:content-type string?]
|
||||||
|
[:bytes bytes?]]))
|
||||||
|
|
@ -19,6 +19,9 @@
|
||||||
st/strip-extra-keys-transformer
|
st/strip-extra-keys-transformer
|
||||||
st/json-transformer))
|
st/json-transformer))
|
||||||
|
|
||||||
|
(def strip-extra-keys-transformer
|
||||||
|
st/strip-extra-keys-transformer)
|
||||||
|
|
||||||
(def no-op-transformer
|
(def no-op-transformer
|
||||||
(reify
|
(reify
|
||||||
st/Transformer
|
st/Transformer
|
||||||
|
|
@ -72,7 +75,7 @@
|
||||||
|
|
||||||
(def default-options
|
(def default-options
|
||||||
{:coerce-response? coerce-response?
|
{:coerce-response? coerce-response?
|
||||||
:transformers {:body {:default no-op-transformer
|
:transformers {:body {:default strip-extra-keys-transformer
|
||||||
:formats {"application/json" json-transformer}}
|
:formats {"application/json" json-transformer}}
|
||||||
:string {:default string-transformer}
|
:string {:default string-transformer}
|
||||||
:response {:default no-op-transformer}}})
|
:response {:default no-op-transformer}}})
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@
|
||||||
[spec-tools.core :as st]
|
[spec-tools.core :as st]
|
||||||
[muuntaja.middleware :as mm]
|
[muuntaja.middleware :as mm]
|
||||||
[muuntaja.core :as m]
|
[muuntaja.core :as m]
|
||||||
[muuntaja.format.jsonista :as jsonista-format]
|
|
||||||
[jsonista.core :as j]
|
[jsonista.core :as j]
|
||||||
[reitit.ring.coercion :as rrc]
|
[reitit.ring.coercion :as rrc]
|
||||||
[reitit.coercion.spec :as spec]
|
[reitit.coercion.spec :as spec]
|
||||||
[reitit.coercion.schema :as schema]
|
[reitit.coercion.schema :as schema]
|
||||||
|
[reitit.coercion.malli :as malli]
|
||||||
[reitit.coercion :as coercion]
|
[reitit.coercion :as coercion]
|
||||||
[reitit.ring :as ring]))
|
[reitit.ring :as ring]))
|
||||||
|
|
||||||
|
|
@ -173,15 +173,14 @@
|
||||||
|
|
||||||
(defn json-perf-test []
|
(defn json-perf-test []
|
||||||
(title "json")
|
(title "json")
|
||||||
(let [m (m/create (jsonista-format/with-json-format m/default-options))
|
(let [app (ring/ring-handler
|
||||||
app (ring/ring-handler
|
|
||||||
(ring/router
|
(ring/router
|
||||||
["/plus" {:post {:handler (fn [request]
|
["/plus" {:post {:handler (fn [request]
|
||||||
(let [body (:body-params request)
|
(let [body (:body-params request)
|
||||||
x (:x body)
|
x (:x body)
|
||||||
y (:y body)]
|
y (:y body)]
|
||||||
{:status 200, :body {:result (+ x y)}}))}}]
|
{:status 200, :body {:result (+ x y)}}))}}]
|
||||||
{:data {:middleware [[mm/wrap-format m]]}}))
|
{:data {:middleware [mm/wrap-format]}}))
|
||||||
request {:request-method :post
|
request {:request-method :post
|
||||||
:uri "/plus"
|
:uri "/plus"
|
||||||
:headers {"content-type" "application/json"}
|
:headers {"content-type" "application/json"}
|
||||||
|
|
@ -196,15 +195,14 @@
|
||||||
|
|
||||||
(defn schema-json-perf-test []
|
(defn schema-json-perf-test []
|
||||||
(title "schema-json")
|
(title "schema-json")
|
||||||
(let [m (m/create (jsonista-format/with-json-format m/default-options))
|
(let [app (ring/ring-handler
|
||||||
app (ring/ring-handler
|
|
||||||
(ring/router
|
(ring/router
|
||||||
["/plus" {:post {:responses {200 {:body {:result Long}}}
|
["/plus" {:post {:responses {200 {:body {:result Long}}}
|
||||||
:parameters {:body {:x Long, :y Long}}
|
:parameters {:body {:x Long, :y Long}}
|
||||||
:handler (fn [request]
|
:handler (fn [request]
|
||||||
(let [body (-> request :parameters :body)]
|
(let [body (-> request :parameters :body)]
|
||||||
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
|
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
|
||||||
{:data {:middleware [[mm/wrap-format m]
|
{:data {:middleware [mm/wrap-format
|
||||||
rrc/coerce-request-middleware
|
rrc/coerce-request-middleware
|
||||||
rrc/coerce-response-middleware]
|
rrc/coerce-response-middleware]
|
||||||
:coercion schema/coercion}}))
|
:coercion schema/coercion}}))
|
||||||
|
|
@ -234,6 +232,7 @@
|
||||||
:coercion schema/coercion}}))
|
:coercion schema/coercion}}))
|
||||||
request {:request-method :post
|
request {:request-method :post
|
||||||
:uri "/plus"
|
:uri "/plus"
|
||||||
|
:muuntaja/request {:format "application/json"}
|
||||||
:body-params {:x 1, :y 2}}
|
:body-params {:x 1, :y 2}}
|
||||||
call (fn [] (-> request app :body))]
|
call (fn [] (-> request app :body))]
|
||||||
(assert (= {:result 3} (call)))
|
(assert (= {:result 3} (call)))
|
||||||
|
|
@ -241,6 +240,7 @@
|
||||||
;; 0.23µs (no coercion)
|
;; 0.23µs (no coercion)
|
||||||
;; 12.8µs
|
;; 12.8µs
|
||||||
;; 1.9µs (cached coercers)
|
;; 1.9µs (cached coercers)
|
||||||
|
;; 2.5µs (real json)
|
||||||
(cc/quick-bench
|
(cc/quick-bench
|
||||||
(call))))
|
(call))))
|
||||||
|
|
||||||
|
|
@ -258,11 +258,13 @@
|
||||||
:coercion spec/coercion}}))
|
:coercion spec/coercion}}))
|
||||||
request {:request-method :post
|
request {:request-method :post
|
||||||
:uri "/plus"
|
:uri "/plus"
|
||||||
|
:muuntaja/request {:format "application/json"}
|
||||||
:body-params {:x 1, :y 2}}
|
:body-params {:x 1, :y 2}}
|
||||||
call (fn [] (-> request app :body))]
|
call (fn [] (-> request app :body))]
|
||||||
(assert (= {:result 3} (call)))
|
(assert (= {:result 3} (call)))
|
||||||
|
|
||||||
;; 6.0µs
|
;; 6.0µs
|
||||||
|
;; 30.0µs (real json)
|
||||||
(cc/quick-bench
|
(cc/quick-bench
|
||||||
(call))))
|
(call))))
|
||||||
|
|
||||||
|
|
@ -287,17 +289,45 @@
|
||||||
:coercion spec/coercion}}))
|
:coercion spec/coercion}}))
|
||||||
request {:request-method :post
|
request {:request-method :post
|
||||||
:uri "/plus"
|
:uri "/plus"
|
||||||
|
:muuntaja/request {:format "application/json"}
|
||||||
:body-params {:x 1, :y 2}}
|
:body-params {:x 1, :y 2}}
|
||||||
call (fn [] (-> request app :body))]
|
call (fn [] (-> request app :body))]
|
||||||
(assert (= {:result 3} (call)))
|
(assert (= {:result 3} (call)))
|
||||||
|
|
||||||
;; 3.2µs
|
;; 3.2µs
|
||||||
|
;; 13.0µs (real json)
|
||||||
|
(cc/quick-bench
|
||||||
|
(call))))
|
||||||
|
|
||||||
|
(defn malli-perf-test []
|
||||||
|
(title "malli")
|
||||||
|
(let [app (ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
["/plus" {:post {:responses {200 {:body [:map [:result int?]]}}
|
||||||
|
:parameters {:body [:map [:x int?] [:y int?]]}
|
||||||
|
:handler (fn [request]
|
||||||
|
(let [body (-> request :parameters :body)]
|
||||||
|
{:status 200, :body {:result (+ (:x body) (:y body))}}))}}]
|
||||||
|
{:data {:middleware [rrc/coerce-request-middleware
|
||||||
|
rrc/coerce-response-middleware]
|
||||||
|
:coercion malli/coercion}}))
|
||||||
|
request {:request-method :post
|
||||||
|
:uri "/plus"
|
||||||
|
:muuntaja/request {:format "application/json"}
|
||||||
|
:body-params {:x 1, :y 2}}
|
||||||
|
call (fn [] (-> request app :body))]
|
||||||
|
(assert (= {:result 3} (call)))
|
||||||
|
|
||||||
|
;; 1.2µs (real json)
|
||||||
(cc/quick-bench
|
(cc/quick-bench
|
||||||
(call))))
|
(call))))
|
||||||
|
|
||||||
(comment
|
(comment
|
||||||
(json-perf-test)
|
(json-perf-test)
|
||||||
(schema-json-perf-test)
|
(schema-json-perf-test)
|
||||||
(schema-perf-test)
|
|
||||||
(data-spec-perf-test)
|
(do
|
||||||
(spec-perf-test))
|
(schema-perf-test)
|
||||||
|
(data-spec-perf-test)
|
||||||
|
(spec-perf-test)
|
||||||
|
(malli-perf-test)))
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
[metosin/reitit-core "0.3.10"]
|
[metosin/reitit-core "0.3.10"]
|
||||||
[metosin/reitit-dev "0.3.10"]
|
[metosin/reitit-dev "0.3.10"]
|
||||||
[metosin/reitit-spec "0.3.10"]
|
[metosin/reitit-spec "0.3.10"]
|
||||||
|
[metosin/reitit-malli "0.3.10"]
|
||||||
[metosin/reitit-schema "0.3.10"]
|
[metosin/reitit-schema "0.3.10"]
|
||||||
[metosin/reitit-ring "0.3.10"]
|
[metosin/reitit-ring "0.3.10"]
|
||||||
[metosin/reitit-middleware "0.3.10"]
|
[metosin/reitit-middleware "0.3.10"]
|
||||||
|
|
@ -32,6 +33,7 @@
|
||||||
[metosin/muuntaja "0.6.6"]
|
[metosin/muuntaja "0.6.6"]
|
||||||
[metosin/jsonista "0.2.5"]
|
[metosin/jsonista "0.2.5"]
|
||||||
[metosin/sieppari "0.0.0-alpha7"]
|
[metosin/sieppari "0.0.0-alpha7"]
|
||||||
|
[metosin/malli "0.0.1-20200108.194558-11"]
|
||||||
|
|
||||||
[meta-merge "1.0.0"]
|
[meta-merge "1.0.0"]
|
||||||
[fipp "0.6.22" :exclusions [org.clojure/core.rrb-vector]]
|
[fipp "0.6.22" :exclusions [org.clojure/core.rrb-vector]]
|
||||||
|
|
@ -60,6 +62,7 @@
|
||||||
"modules/reitit-http/src"
|
"modules/reitit-http/src"
|
||||||
"modules/reitit-middleware/src"
|
"modules/reitit-middleware/src"
|
||||||
"modules/reitit-interceptors/src"
|
"modules/reitit-interceptors/src"
|
||||||
|
"modules/reitit-malli/src"
|
||||||
"modules/reitit-spec/src"
|
"modules/reitit-spec/src"
|
||||||
"modules/reitit-schema/src"
|
"modules/reitit-schema/src"
|
||||||
"modules/reitit-swagger/src"
|
"modules/reitit-swagger/src"
|
||||||
|
|
@ -79,6 +82,7 @@
|
||||||
[metosin/muuntaja]
|
[metosin/muuntaja]
|
||||||
[metosin/sieppari]
|
[metosin/sieppari]
|
||||||
[metosin/jsonista]
|
[metosin/jsonista]
|
||||||
|
[metosin/malli]
|
||||||
[lambdaisland/deep-diff]
|
[lambdaisland/deep-diff]
|
||||||
[meta-merge]
|
[meta-merge]
|
||||||
[com.bhauman/spell-spec]
|
[com.bhauman/spell-spec]
|
||||||
|
|
@ -91,9 +95,6 @@
|
||||||
[ikitommi/immutant-web "3.0.0-alpha1"]
|
[ikitommi/immutant-web "3.0.0-alpha1"]
|
||||||
[metosin/ring-http-response "0.9.1"]
|
[metosin/ring-http-response "0.9.1"]
|
||||||
[metosin/ring-swagger-ui "2.2.10"]
|
[metosin/ring-swagger-ui "2.2.10"]
|
||||||
[metosin/muuntaja]
|
|
||||||
[metosin/sieppari]
|
|
||||||
[metosin/jsonista]
|
|
||||||
|
|
||||||
[criterium "0.4.5"]
|
[criterium "0.4.5"]
|
||||||
[org.clojure/test.check "0.10.0"]
|
[org.clojure/test.check "0.10.0"]
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ for ext in \
|
||||||
reitit-core \
|
reitit-core \
|
||||||
reitit-dev \
|
reitit-dev \
|
||||||
reitit-spec \
|
reitit-spec \
|
||||||
|
reitit-malli \
|
||||||
reitit-schema \
|
reitit-schema \
|
||||||
reitit-ring \
|
reitit-ring \
|
||||||
reitit-middleware \
|
reitit-middleware \
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
[reitit.core :as r]
|
[reitit.core :as r]
|
||||||
[reitit.coercion :as coercion]
|
[reitit.coercion :as coercion]
|
||||||
[reitit.coercion.spec]
|
[reitit.coercion.spec]
|
||||||
|
[reitit.coercion.malli]
|
||||||
[reitit.coercion.schema])
|
[reitit.coercion.schema])
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(:import (clojure.lang ExceptionInfo))))
|
(:import (clojure.lang ExceptionInfo))))
|
||||||
|
|
@ -15,6 +16,11 @@
|
||||||
["/:number/:keyword" {:parameters {:path {:number s/Int
|
["/:number/:keyword" {:parameters {:path {:number s/Int
|
||||||
:keyword s/Keyword}
|
:keyword s/Keyword}
|
||||||
:query (s/maybe {:int s/Int, :ints [s/Int], :map {s/Int s/Int}})}}]]
|
:query (s/maybe {:int s/Int, :ints [s/Int], :map {s/Int s/Int}})}}]]
|
||||||
|
["/malli" {:coercion reitit.coercion.malli/coercion}
|
||||||
|
["/:number/:keyword" {:parameters {:path [:map [:number int?] [:keyword keyword?]]
|
||||||
|
:query [:maybe [:map [:int int?]
|
||||||
|
[:ints [:vector int?]]
|
||||||
|
[:map [:map-of int? int?]]]]}}]]
|
||||||
["/spec" {:coercion reitit.coercion.spec/coercion}
|
["/spec" {:coercion reitit.coercion.spec/coercion}
|
||||||
["/:number/:keyword" {:parameters {:path {:number int?
|
["/:number/:keyword" {:parameters {:path {:number int?
|
||||||
:keyword keyword?}
|
:keyword keyword?}
|
||||||
|
|
@ -30,20 +36,33 @@
|
||||||
(is (= {:path {:keyword :abba, :number 1}, :query nil}
|
(is (= {:path {:keyword :abba, :number 1}, :query nil}
|
||||||
(coercion/coerce! m))))
|
(coercion/coerce! m))))
|
||||||
(let [m (r/match-by-path r "/schema/1/abba")]
|
(let [m (r/match-by-path r "/schema/1/abba")]
|
||||||
(is (= {:path {:keyword :abba, :number 1}, :query {:int 10, :ints [1,2,3], :map {1 1}}}
|
(is (= {:path {:keyword :abba, :number 1}, :query {:int 10, :ints [1, 2, 3], :map {1 1, 2 2}}}
|
||||||
(coercion/coerce! (assoc m :query-params {"int" "10", "ints" ["1" "2" "3"], "map" {:1 :1}}))))))
|
(coercion/coerce! (assoc m :query-params {"int" "10", "ints" ["1" "2" "3"], "map" {:1 "1", "2" "2"}}))))))
|
||||||
(testing "throws with invalid input"
|
(testing "throws with invalid input"
|
||||||
(let [m (r/match-by-path r "/schema/kikka/abba")]
|
(let [m (r/match-by-path r "/schema/kikka/abba")]
|
||||||
(is (thrown? ExceptionInfo (coercion/coerce! m))))))
|
(is (thrown? ExceptionInfo (coercion/coerce! m))))))
|
||||||
|
|
||||||
|
(testing "malli-coercion"
|
||||||
|
(testing "succeeds"
|
||||||
|
(let [m (r/match-by-path r "/malli/1/abba")]
|
||||||
|
(is (= {:path {:keyword :abba, :number 1}, :query nil}
|
||||||
|
(coercion/coerce! m))))
|
||||||
|
(let [m (r/match-by-path r "/malli/1/abba")]
|
||||||
|
(is (= {:path {:keyword :abba, :number 1}, :query {:int 10, :ints [1, 2, 3], :map {1 1, 2 2}}}
|
||||||
|
(coercion/coerce! (assoc m :query-params {"int" "10", "ints" ["1" "2" "3"], "map" {:1 "1", "2" "2"}}))))))
|
||||||
|
(testing "throws with invalid input"
|
||||||
|
(let [m (r/match-by-path r "/malli/kikka/abba")]
|
||||||
|
(is (thrown? ExceptionInfo (coercion/coerce! m))))))
|
||||||
|
|
||||||
|
;; TODO: :map-of fails with string-keys
|
||||||
(testing "spec-coercion"
|
(testing "spec-coercion"
|
||||||
(testing "succeeds"
|
(testing "succeeds"
|
||||||
(let [m (r/match-by-path r "/spec/1/abba")]
|
(let [m (r/match-by-path r "/spec/1/abba")]
|
||||||
(is (= {:path {:keyword :abba, :number 1}, :query nil}
|
(is (= {:path {:keyword :abba, :number 1}, :query nil}
|
||||||
(coercion/coerce! m))))
|
(coercion/coerce! m))))
|
||||||
(let [m (r/match-by-path r "/schema/1/abba")]
|
(let [m (r/match-by-path r "/schema/1/abba")]
|
||||||
(is (= {:path {:keyword :abba, :number 1}, :query {:int 10, :ints [1,2,3], :map {1 1}}}
|
(is (= {:path {:keyword :abba, :number 1}, :query {:int 10, :ints [1, 2, 3], :map {1 1, #_#_2 2}}}
|
||||||
(coercion/coerce! (assoc m :query-params {"int" "10", "ints" ["1" "2" "3"], "map" {:1 :1}}))))))
|
(coercion/coerce! (assoc m :query-params {"int" "10", "ints" ["1" "2" "3"], "map" {:1 "1"}, #_#_"2" "2"}))))))
|
||||||
(testing "throws with invalid input"
|
(testing "throws with invalid input"
|
||||||
(let [m (r/match-by-path r "/spec/kikka/abba")]
|
(let [m (r/match-by-path r "/spec/kikka/abba")]
|
||||||
(is (thrown? ExceptionInfo (coercion/coerce! m))))))
|
(is (thrown? ExceptionInfo (coercion/coerce! m))))))
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
[reitit.ring :as ring]
|
[reitit.ring :as ring]
|
||||||
[reitit.ring.coercion :as rrc]
|
[reitit.ring.coercion :as rrc]
|
||||||
[reitit.coercion.spec :as spec]
|
[reitit.coercion.spec :as spec]
|
||||||
|
[reitit.coercion.malli :as malli]
|
||||||
[reitit.coercion.schema :as schema]
|
[reitit.coercion.schema :as schema]
|
||||||
#?@(:clj [[muuntaja.middleware]
|
#?@(:clj [[muuntaja.middleware]
|
||||||
[jsonista.core :as j]]))
|
[jsonista.core :as j]]))
|
||||||
|
|
@ -16,22 +17,44 @@
|
||||||
{:keys [b]} :body
|
{:keys [b]} :body
|
||||||
{:keys [c]} :form
|
{:keys [c]} :form
|
||||||
{:keys [d]} :header
|
{:keys [d]} :header
|
||||||
{:keys [e]} :path} :parameters}]
|
{:keys [e]} :path :as parameters} :parameters}]
|
||||||
|
;; extra keys are stripped off
|
||||||
|
(assert (every? #{0 1} (map (comp count val) parameters)))
|
||||||
|
|
||||||
(if (= 666 a)
|
(if (= 666 a)
|
||||||
{:status 500
|
{:status 500
|
||||||
:body {:evil true}}
|
:body {:evil true}}
|
||||||
{:status 200
|
{:status 200
|
||||||
:body {:total (+ a b c d e)}}))
|
:body {:total (+ (or a 101) b c d e)}}))
|
||||||
|
|
||||||
(def valid-request
|
(def valid-request1
|
||||||
{:uri "/api/plus/5"
|
{:uri "/api/plus/5"
|
||||||
:request-method :get
|
:request-method :get
|
||||||
|
:muuntaja/request {:format "application/json"}
|
||||||
:query-params {"a" "1"}
|
:query-params {"a" "1"}
|
||||||
:body-params {:b 2}
|
:body-params {:b 2}
|
||||||
:form-params {:c 3}
|
:form-params {:c 3}
|
||||||
:headers {"d" "4"}})
|
:headers {"d" "4"}})
|
||||||
|
|
||||||
(def invalid-request
|
(def valid-request2
|
||||||
|
{:uri "/api/plus/5"
|
||||||
|
:request-method :get
|
||||||
|
:muuntaja/request {:format "application/json"}
|
||||||
|
:query-params {}
|
||||||
|
:body-params {:b 2}
|
||||||
|
:form-params {:c 3}
|
||||||
|
:headers {"d" "4"}})
|
||||||
|
|
||||||
|
(def valid-request3
|
||||||
|
{:uri "/api/plus/5"
|
||||||
|
:request-method :get
|
||||||
|
:muuntaja/request {:format "application/edn"}
|
||||||
|
:query-params {"a" "1", "EXTRA" "VALUE"}
|
||||||
|
:body-params {:b 2, :EXTRA "VALUE"}
|
||||||
|
:form-params {:c 3, :EXTRA "VALUE"}
|
||||||
|
:headers {"d" "4", "EXTRA" "VALUE"}})
|
||||||
|
|
||||||
|
(def invalid-request1
|
||||||
{:uri "/api/plus/5"
|
{:uri "/api/plus/5"
|
||||||
:request-method :get})
|
:request-method :get})
|
||||||
|
|
||||||
|
|
@ -67,16 +90,22 @@
|
||||||
(testing "all good"
|
(testing "all good"
|
||||||
(is (= {:status 200
|
(is (= {:status 200
|
||||||
:body {:total 15}}
|
:body {:total 15}}
|
||||||
(app valid-request)))
|
(app valid-request1)))
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 115}}
|
||||||
|
(app valid-request2)))
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 15}}
|
||||||
|
(app valid-request3)))
|
||||||
(is (= {:status 500
|
(is (= {:status 500
|
||||||
:body {:evil true}}
|
:body {:evil true}}
|
||||||
(app (assoc-in valid-request [:query-params "a"] "666")))))
|
(app (assoc-in valid-request1 [:query-params "a"] "666")))))
|
||||||
|
|
||||||
(testing "invalid request"
|
(testing "invalid request"
|
||||||
(is (thrown-with-msg?
|
(is (thrown-with-msg?
|
||||||
ExceptionInfo
|
ExceptionInfo
|
||||||
#"Request coercion failed"
|
#"Request coercion failed"
|
||||||
(app invalid-request))))
|
(app invalid-request1))))
|
||||||
|
|
||||||
(testing "invalid response"
|
(testing "invalid response"
|
||||||
(is (thrown-with-msg?
|
(is (thrown-with-msg?
|
||||||
|
|
@ -92,10 +121,10 @@
|
||||||
(testing "all good"
|
(testing "all good"
|
||||||
(is (= {:status 200
|
(is (= {:status 200
|
||||||
:body {:total 15}}
|
:body {:total 15}}
|
||||||
(app valid-request))))
|
(app valid-request1))))
|
||||||
|
|
||||||
(testing "invalid request"
|
(testing "invalid request"
|
||||||
(let [{:keys [status body]} (app invalid-request)
|
(let [{:keys [status body]} (app invalid-request1)
|
||||||
problems (:problems body)]
|
problems (:problems body)]
|
||||||
(is (= 1 (count problems)))
|
(is (= 1 (count problems)))
|
||||||
(is (= 400 status))))
|
(is (= 400 status))))
|
||||||
|
|
@ -110,7 +139,7 @@
|
||||||
(ring/router
|
(ring/router
|
||||||
["/api"
|
["/api"
|
||||||
["/plus/:e"
|
["/plus/:e"
|
||||||
{:get {:parameters {:query {:a s/Int}
|
{:get {:parameters {:query {(s/optional-key :a) s/Int}
|
||||||
:body {:b s/Int}
|
:body {:b s/Int}
|
||||||
:form {:c s/Int}
|
:form {:c s/Int}
|
||||||
:header {:d s/Int}
|
:header {:d s/Int}
|
||||||
|
|
@ -128,40 +157,180 @@
|
||||||
(testing "all good"
|
(testing "all good"
|
||||||
(is (= {:status 200
|
(is (= {:status 200
|
||||||
:body {:total 15}}
|
:body {:total 15}}
|
||||||
(app valid-request)))
|
(app valid-request1)))
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 115}}
|
||||||
|
(app valid-request2)))
|
||||||
(is (= {:status 500
|
(is (= {:status 500
|
||||||
:body {:evil true}}
|
:body {:evil true}}
|
||||||
(app (assoc-in valid-request [:query-params "a"] "666")))))
|
(app (assoc-in valid-request1 [:query-params "a"] "666")))))
|
||||||
|
|
||||||
(testing "invalid request"
|
(testing "invalid request"
|
||||||
(is (thrown-with-msg?
|
(is (thrown-with-msg?
|
||||||
ExceptionInfo
|
ExceptionInfo
|
||||||
#"Request coercion failed"
|
#"Request coercion failed"
|
||||||
(app invalid-request))))
|
(app invalid-request1)))
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Request coercion failed"
|
||||||
|
(app valid-request3))))
|
||||||
|
|
||||||
(testing "invalid response"
|
(testing "invalid response"
|
||||||
(is (thrown-with-msg?
|
(is (thrown-with-msg?
|
||||||
ExceptionInfo
|
ExceptionInfo
|
||||||
#"Response coercion failed"
|
#"Response coercion failed"
|
||||||
(app invalid-request2))))
|
(app invalid-request2))))))
|
||||||
|
|
||||||
(testing "with exception handling"
|
(testing "with exception handling"
|
||||||
(let [app (create [rrc/coerce-exceptions-middleware
|
(let [app (create [rrc/coerce-exceptions-middleware
|
||||||
rrc/coerce-request-middleware
|
rrc/coerce-request-middleware
|
||||||
rrc/coerce-response-middleware])]
|
rrc/coerce-response-middleware])]
|
||||||
|
|
||||||
(testing "all good"
|
(testing "all good"
|
||||||
(is (= {:status 200
|
(is (= {:status 200
|
||||||
:body {:total 15}}
|
:body {:total 15}}
|
||||||
(app valid-request))))
|
(app valid-request1))))
|
||||||
|
|
||||||
(testing "invalid request"
|
(testing "invalid request"
|
||||||
(let [{:keys [status]} (app invalid-request)]
|
(let [{:keys [status]} (app invalid-request1)]
|
||||||
(is (= 400 status))))
|
(is (= 400 status))))
|
||||||
|
|
||||||
(testing "invalid response"
|
(testing "invalid response"
|
||||||
(let [{:keys [status]} (app invalid-request2)]
|
(let [{:keys [status]} (app invalid-request2)]
|
||||||
(is (= 500 status))))))))))
|
(is (= 500 status))))))))
|
||||||
|
|
||||||
|
(deftest malli-coercion-test
|
||||||
|
(let [create (fn [middleware]
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
["/api"
|
||||||
|
["/plus/:e" {:get {:parameters {:query [:map [:a {:optional true} int?]]
|
||||||
|
:body [:map [:b int?]]
|
||||||
|
:form [:map [:c [int? {:default 3}]]]
|
||||||
|
:header [:map [:d int?]]
|
||||||
|
:path [:map [:e int?]]}
|
||||||
|
:responses {200 {:body [:map [:total pos-int?]]}
|
||||||
|
500 {:description "fail"}}
|
||||||
|
:handler handler}}]]
|
||||||
|
{:data {:middleware middleware
|
||||||
|
:coercion malli/coercion}})))]
|
||||||
|
|
||||||
|
(testing "withut exception handling"
|
||||||
|
(let [app (create [rrc/coerce-request-middleware
|
||||||
|
rrc/coerce-response-middleware])]
|
||||||
|
|
||||||
|
(testing "all good"
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 15}}
|
||||||
|
(app valid-request1)))
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 115}}
|
||||||
|
(app valid-request2)))
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 15}}
|
||||||
|
(app valid-request3)))
|
||||||
|
(testing "default values work"
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 15}}
|
||||||
|
(app (update valid-request3 :form-params dissoc :c)))))
|
||||||
|
(is (= {:status 500
|
||||||
|
:body {:evil true}}
|
||||||
|
(app (assoc-in valid-request1 [:query-params "a"] "666")))))
|
||||||
|
|
||||||
|
(testing "invalid request"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Request coercion failed"
|
||||||
|
(app invalid-request1))))
|
||||||
|
|
||||||
|
(testing "invalid response"
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
ExceptionInfo
|
||||||
|
#"Response coercion failed"
|
||||||
|
(app invalid-request2))))))
|
||||||
|
|
||||||
|
(testing "with exception handling"
|
||||||
|
(let [app (create [rrc/coerce-exceptions-middleware
|
||||||
|
rrc/coerce-request-middleware
|
||||||
|
rrc/coerce-response-middleware])]
|
||||||
|
|
||||||
|
(testing "all good"
|
||||||
|
(is (= {:status 200
|
||||||
|
:body {:total 15}}
|
||||||
|
(app valid-request1))))
|
||||||
|
|
||||||
|
(testing "invalid request"
|
||||||
|
(let [{:keys [status]} (app invalid-request1)]
|
||||||
|
(is (= 400 status))))
|
||||||
|
|
||||||
|
(testing "invalid response"
|
||||||
|
(let [{:keys [status]} (app invalid-request2)]
|
||||||
|
(is (= 500 status))))))
|
||||||
|
|
||||||
|
(testing "open & closed schemas"
|
||||||
|
(let [endpoint (fn [schema]
|
||||||
|
{:get {:parameters {:body schema}
|
||||||
|
:responses {200 {:body schema}}
|
||||||
|
:handler (fn [{{:keys [body]} :parameters}]
|
||||||
|
{:status 200, :body (assoc body :response true)})}})
|
||||||
|
->app (fn [options]
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
["/api"
|
||||||
|
["/default" (endpoint [:map [:x int?]])]
|
||||||
|
["/closed" (endpoint [:map {:closed true} [:x int?]])]
|
||||||
|
["/open" (endpoint [:map {:closed false} [:x int?]])]]
|
||||||
|
{:data {:middleware [rrc/coerce-exceptions-middleware
|
||||||
|
rrc/coerce-request-middleware
|
||||||
|
rrc/coerce-response-middleware]
|
||||||
|
:coercion (malli/create options)}})))
|
||||||
|
->request (fn [uri] {:uri (str "/api/" uri)
|
||||||
|
:request-method :get
|
||||||
|
:muuntaja/request {:format "application/json"}
|
||||||
|
:body-params {:x 1, :request true}})]
|
||||||
|
|
||||||
|
(testing "with defaults"
|
||||||
|
(let [app (->app nil)]
|
||||||
|
|
||||||
|
(testing "default: keys are stripped"
|
||||||
|
(is (= {:status 200, :body {:x 1}}
|
||||||
|
(app (->request "default")))))
|
||||||
|
|
||||||
|
(testing "closed: keys are stripped"
|
||||||
|
(is (= {:status 200, :body {:x 1}}
|
||||||
|
(app (->request "closed")))))
|
||||||
|
|
||||||
|
(testing "open: keys are NOT stripped"
|
||||||
|
(is (= {:status 200, :body {:x 1, :request true, :response true}}
|
||||||
|
(app (->request "open")))))))
|
||||||
|
|
||||||
|
(testing "when schemas are not closed"
|
||||||
|
(let [app (->app {:compile identity})]
|
||||||
|
|
||||||
|
(testing "default: keys are stripped"
|
||||||
|
(is (= {:status 200, :body {:x 1}}
|
||||||
|
(app (->request "default")))))
|
||||||
|
|
||||||
|
(testing "closed: keys are stripped"
|
||||||
|
(is (= {:status 200, :body {:x 1}}
|
||||||
|
(app (->request "closed")))))
|
||||||
|
|
||||||
|
(testing "open: keys are NOT stripped"
|
||||||
|
(is (= {:status 200, :body {:x 1, :request true, :response true}}
|
||||||
|
(app (->request "open")))))))
|
||||||
|
|
||||||
|
(testing "when schemas are not closed and extra keys are not stripped"
|
||||||
|
(let [app (->app {:compile identity, :strip-extra-keys false})]
|
||||||
|
(testing "default: keys are NOT stripped"
|
||||||
|
(is (= {:status 200, :body {:x 1, :request true, :response true}}
|
||||||
|
(app (->request "default")))))
|
||||||
|
|
||||||
|
(testing "closed: FAILS for extra keys"
|
||||||
|
(is (= 400 (:status (app (->request "closed"))))))
|
||||||
|
|
||||||
|
(testing "open: keys are NOT stripped"
|
||||||
|
(is (= {:status 200, :body {:x 1, :request true, :response true}}
|
||||||
|
(app (->request "open")))))))))))
|
||||||
|
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(deftest muuntaja-test
|
(deftest muuntaja-test
|
||||||
|
|
@ -189,11 +358,11 @@
|
||||||
(testing "json coercion"
|
(testing "json coercion"
|
||||||
(let [e2e #(-> (request "application/json" (ByteArrayInputStream. (j/write-value-as-bytes %)))
|
(let [e2e #(-> (request "application/json" (ByteArrayInputStream. (j/write-value-as-bytes %)))
|
||||||
(app) :body (slurp) (j/read-value (j/object-mapper {:decode-key-fn true})))]
|
(app) :body (slurp) (j/read-value (j/object-mapper {:decode-key-fn true})))]
|
||||||
(is (= data-json (e2e data-edn)))
|
(is (= data-json (e2e (assoc data-edn :EXTRA "VALUE"))))
|
||||||
(is (= data-json (e2e data-json)))))
|
(is (= data-json (e2e (assoc data-json :EXTRA "VALUE"))))))
|
||||||
|
|
||||||
(testing "edn coercion"
|
(testing "edn coercion"
|
||||||
(let [e2e #(-> (request "application/edn" (pr-str %))
|
(let [e2e #(-> (request "application/edn" (pr-str %))
|
||||||
(app) :body slurp (read-string))]
|
(app) :body slurp (read-string))]
|
||||||
(is (= data-edn (e2e data-edn)))
|
(is (= data-edn (e2e (assoc data-edn :EXTRA "VALUE"))))
|
||||||
(is (thrown? ExceptionInfo (e2e data-json))))))))
|
(is (thrown? ExceptionInfo (e2e data-json))))))))
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@
|
||||||
[reitit.swagger-ui :as swagger-ui]
|
[reitit.swagger-ui :as swagger-ui]
|
||||||
[reitit.ring.coercion :as rrc]
|
[reitit.ring.coercion :as rrc]
|
||||||
[reitit.coercion.spec :as spec]
|
[reitit.coercion.spec :as spec]
|
||||||
|
[reitit.coercion.malli :as malli]
|
||||||
[reitit.coercion.schema :as schema]
|
[reitit.coercion.schema :as schema]
|
||||||
[schema.core :refer [Int]]
|
[schema.core :refer [Int]]
|
||||||
[muuntaja.core :as m]))
|
[muuntaja.core :as m]
|
||||||
|
[spec-tools.data-spec :as ds]))
|
||||||
|
|
||||||
(def app
|
(def app
|
||||||
(ring/ring-handler
|
(ring/ring-handler
|
||||||
|
|
@ -33,7 +35,7 @@
|
||||||
{:keys [z]} :path} :parameters}]
|
{:keys [z]} :path} :parameters}]
|
||||||
{:status 200, :body {:total (+ x y z)}})}
|
{:status 200, :body {:total (+ x y z)}})}
|
||||||
:post {:summary "plus with body"
|
:post {:summary "plus with body"
|
||||||
:parameters {:body [int?]
|
:parameters {:body (ds/maybe [int?])
|
||||||
:path {:z int?}}
|
:path {:z int?}}
|
||||||
:swagger {:responses {400 {:schema {:type "string"}
|
:swagger {:responses {400 {:schema {:type "string"}
|
||||||
:description "kosh"}}}
|
:description "kosh"}}}
|
||||||
|
|
@ -43,6 +45,29 @@
|
||||||
xs :body} :parameters}]
|
xs :body} :parameters}]
|
||||||
{:status 200, :body {:total (+ (reduce + xs) z)}})}}]]
|
{:status 200, :body {:total (+ (reduce + xs) z)}})}}]]
|
||||||
|
|
||||||
|
["/malli" {:coercion malli/coercion}
|
||||||
|
["/plus/*z"
|
||||||
|
{:get {:summary "plus"
|
||||||
|
:parameters {:query [:map [:x int?] [:y int?]]
|
||||||
|
:path [:map [:z int?]]}
|
||||||
|
:swagger {:responses {400 {:schema {:type "string"}
|
||||||
|
:description "kosh"}}}
|
||||||
|
:responses {200 {:body [:map [:total int?]]}
|
||||||
|
500 {:description "fail"}}
|
||||||
|
:handler (fn [{{{:keys [x y]} :query
|
||||||
|
{:keys [z]} :path} :parameters}]
|
||||||
|
{:status 200, :body {:total (+ x y z)}})}
|
||||||
|
:post {:summary "plus with body"
|
||||||
|
:parameters {:body [:maybe [:vector int?]]
|
||||||
|
:path [:map [:z int?]]}
|
||||||
|
:swagger {:responses {400 {:schema {:type "string"}
|
||||||
|
:description "kosh"}}}
|
||||||
|
:responses {200 {:body [:map [:total int?]]}
|
||||||
|
500 {:description "fail"}}
|
||||||
|
:handler (fn [{{{:keys [z]} :path
|
||||||
|
xs :body} :parameters}]
|
||||||
|
{:status 200, :body {:total (+ (reduce + xs) z)}})}}]]
|
||||||
|
|
||||||
["/schema" {:coercion schema/coercion}
|
["/schema" {:coercion schema/coercion}
|
||||||
["/plus/*z"
|
["/plus/*z"
|
||||||
{:get {:summary "plus"
|
{:get {:summary "plus"
|
||||||
|
|
@ -115,6 +140,56 @@
|
||||||
:description "kosh"}
|
:description "kosh"}
|
||||||
500 {:description "fail"}}
|
500 {:description "fail"}}
|
||||||
:summary "plus"}}
|
:summary "plus"}}
|
||||||
|
"/api/malli/plus/{z}" {:get {:parameters [{:description ""
|
||||||
|
:format "int64"
|
||||||
|
:in "query"
|
||||||
|
:name :x
|
||||||
|
:required true
|
||||||
|
:type "integer"}
|
||||||
|
{:description ""
|
||||||
|
:format "int64"
|
||||||
|
:in "query"
|
||||||
|
:name :y
|
||||||
|
:required true
|
||||||
|
:type "integer"}
|
||||||
|
{:in "path"
|
||||||
|
:name :z
|
||||||
|
:description ""
|
||||||
|
:type "integer"
|
||||||
|
:required true
|
||||||
|
:format "int64"}]
|
||||||
|
:responses {200 {:description ""
|
||||||
|
:schema {:properties {:total {:format "int64"
|
||||||
|
:type "integer"}}
|
||||||
|
:required [:total]
|
||||||
|
:type "object"}}
|
||||||
|
400 {:schema {:type "string"}
|
||||||
|
:description "kosh"}
|
||||||
|
500 {:description "fail"}}
|
||||||
|
:summary "plus"}
|
||||||
|
:post {:parameters [{:in "body",
|
||||||
|
:name "",
|
||||||
|
:description "",
|
||||||
|
:required false,
|
||||||
|
:schema {:type "array",
|
||||||
|
:items {:type "integer",
|
||||||
|
:format "int64"}
|
||||||
|
:x-nullable true}}
|
||||||
|
{:in "path"
|
||||||
|
:name :z
|
||||||
|
:description ""
|
||||||
|
:type "integer"
|
||||||
|
:required true
|
||||||
|
:format "int64"}]
|
||||||
|
:responses {200 {:description ""
|
||||||
|
:schema {:properties {:total {:format "int64"
|
||||||
|
:type "integer"}}
|
||||||
|
:required [:total]
|
||||||
|
:type "object"}}
|
||||||
|
400 {:schema {:type "string"}
|
||||||
|
:description "kosh"}
|
||||||
|
500 {:description "fail"}}
|
||||||
|
:summary "plus with body"}}
|
||||||
"/api/spec/plus/{z}" {:get {:parameters [{:description ""
|
"/api/spec/plus/{z}" {:get {:parameters [{:description ""
|
||||||
:format "int64"
|
:format "int64"
|
||||||
:in "query"
|
:in "query"
|
||||||
|
|
@ -145,10 +220,11 @@
|
||||||
:post {:parameters [{:in "body",
|
:post {:parameters [{:in "body",
|
||||||
:name "",
|
:name "",
|
||||||
:description "",
|
:description "",
|
||||||
:required true,
|
:required false,
|
||||||
:schema {:type "array",
|
:schema {:type "array",
|
||||||
:items {:type "integer",
|
:items {:type "integer",
|
||||||
:format "int64"}}}
|
:format "int64"}
|
||||||
|
:x-nullable true}}
|
||||||
{:in "path"
|
{:in "path"
|
||||||
:name "z"
|
:name "z"
|
||||||
:description ""
|
:description ""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue