Merge pull request #341 from metosin/malli

Implement malli-based coercion
This commit is contained in:
Tommi Reiman 2020-01-10 16:07:56 +02:00 committed by GitHub
commit 8b374678d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 815 additions and 60 deletions

View file

@ -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"]}`).

View file

@ -1,13 +1,12 @@
# reitit [![Build Status](https://img.shields.io/circleci/project/github/metosin/reitit.svg)](https://circleci.com/gh/metosin/reitit) [![cljdoc badge](https://cljdoc.xyz/badge/metosin/reitit)](https://cljdoc.xyz/jump/release/metosin/reitit) [![Slack](https://img.shields.io/badge/clojurians-reitit-blue.svg?logo=slack)](https://clojurians.slack.com/messages/reitit/) # reitit [![Build Status](https://img.shields.io/circleci/project/github/metosin/reitit.svg)](https://circleci.com/gh/metosin/reitit) [![cljdoc badge](https://cljdoc.xyz/badge/metosin/reitit)](https://cljdoc.xyz/jump/release/metosin/reitit) [![Slack](https://img.shields.io/badge/clojurians-reitit-blue.svg?logo=slack)](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)

View file

@ -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)

View 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...
```

View file

@ -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
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,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

View 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"]]}})

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

View 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))

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

View 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}")))))

View file

@ -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

View 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]])

View 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))

View 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?]]))

View file

@ -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}}})

View file

@ -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)
(do
(schema-perf-test) (schema-perf-test)
(data-spec-perf-test) (data-spec-perf-test)
(spec-perf-test)) (spec-perf-test)
(malli-perf-test)))

View file

@ -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"]

View file

@ -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 \

View file

@ -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))))))

View file

@ -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,22 +157,29 @@
(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
@ -153,15 +189,148 @@
(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))))))))

View file

@ -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 ""