welcome 2-phase schema compilation

1) use `:update-paths` to handle data in certain (loose) paths differently
  - accumulate schemas in all relevant routers into vector
  - we do not know the coercion here (ring/http have special handling of data, e.g. http-methods)

2) run coercion compiler for the model to merge the effective model
  - schema + malli = should work ok, spec = best effort

3) publish final schemas into compiled route data
This commit is contained in:
Tommi Reiman 2023-05-21 20:32:40 +03:00
parent 3f265888a4
commit ce06214014
13 changed files with 299 additions and 245 deletions

View file

@ -12,7 +12,10 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
## UNREALEASED
## UNRELEASED
**BREAKING**: `compile-request-coercers` returns a map with `:data` and `:coerce` instead of plain `:coerce` function
**BREAKING**: Parameter and Response schemas are acculated into vector in route data - to be merged properly into compiled result, fixes [#422](https://github.com/metosin/reitit/issues/422) - works will all of `Malli`, `Schema` and `Spec`.
```clojure
[metosin/schema-tools "0.13.1"] is available but we use "0.13.0"
@ -20,11 +23,11 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
[com.fasterxml.jackson.core/jackson-databind "2.15.1"] is available but we use "2.14.2"
```
## 0.7.3-alpha4 (2023-05-17)
## 0.7.0-alpha4 (2023-05-17)
* OpenAPI 3 parameter descriptions get populated from malli/spec/schema descriptions. [#612](https://github.com/metosin/reitit/issues/612)
## 0.7.3-alpha3 (2023-05-05)
## 0.7.0-alpha3 (2023-05-05)
* Compile `reitit.Trie` with Java 1.8 target for compatibility

View file

@ -2,18 +2,21 @@
Routers can be configured via options. The following options are available for the `reitit.core/router`:
| key | description
|---------------|-------------
| `:path` | Base-path for routes
| `:routes` | Initial resolved routes (default `[]`)
| `:data` | Initial route data (default `{}`)
| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this
| `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon})
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
| `:compile` | Function of `route opts => result` to compile a route handler
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes
| `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`)
| `:meta-merge` | Function of `left right => merged` to merge route-data (default `meta-merge.core/meta-merge`)
| `:router` | Function of `routes opts => router` to override the actual router implementation
| key | description
|-----------------|-------------
| `:path` | Base-path for routes
| `:routes` | Initial resolved routes (default `[]`)
| `:data` | Initial route data (default `{}`)
| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this
| `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon})
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
| `:compile` | Function of `route opts => result` to compile a route handler
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes
| `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`)
| `:meta-merge` | Function of `left right => merged` to merge route-data (default `meta-merge.core/meta-merge`)
| `:update-paths` | Sequence of Vectors with elements `update-path` and `function`, used to preprocess route data
| `:router` | Function of `routes opts => router` to override the actual router implementation

View file

@ -94,8 +94,9 @@
(conj (:content model) [:default (:body model)])
[[:default model]])
format->coercer (some->> (for [[format schema] format-schema-pairs
:when schema]
[format (-request-coercer coercion (case style :request :body style) (->open schema))])
:when schema
:let [type (case style :request :body style)]]
[format (-request-coercer coercion type (->open schema))])
(filter second)
(seq)
(into {}))]
@ -117,7 +118,8 @@
(defn response-coercer [coercion {:keys [content body]} {:keys [extract-response-format serialize-failed-result]
:or {extract-response-format extract-response-format-default}}]
(if coercion
(let [per-format-coercers (some->> (for [[format schema] content]
(let [per-format-coercers (some->> (for [[format schema] content
:when schema]
[format (-response-coercer coercion schema)])
(filter second)
(seq)
@ -152,25 +154,23 @@
response)))
(defn request-coercers [coercion parameters opts]
(some->> (for [[k v] parameters
:when v]
(some->> (for [[k v] parameters, :when v]
[k (request-coercer coercion k v opts)])
(filter second)
(seq)
(into {})))
(filter second) (seq) (into {})))
(defn response-coercers [coercion responses opts]
(some->> (for [[status model] responses]
[status (response-coercer coercion model opts)])
(filter second)
(seq)
(into {})))
(filter second) (seq) (into {})))
(defn -compile-parameters [data coercion]
(impl/path-update data [[[:parameters any?] #(-compile-model coercion % nil)]]))
;;
;; api-docs
;;
(defn -warn-unsupported-coercions [{:keys [parameters responses] :as data}]
(defn -warn-unsupported-coercions [{:keys [parameters responses] :as _data}]
(when (:request parameters)
(println "WARNING [reitit.coercion]: swagger apidocs don't support :request coercion"))
(when (some :content (vals responses))
@ -204,17 +204,29 @@
(defn compile-request-coercers
"A router :compile implementation which reads the `:parameters`
and `:coercion` data to create compiled coercers into Match under
`:result. A pre-requisite to use [[coerce!]]."
[[_ {:keys [parameters coercion]}] opts]
and `:coercion` data to both compile the schemas and create compiled coercers
into Match under `:result with the following keys:
| key | description
| ----------|-------------
| `:data` | data with compiled schemas
| `:coerce` | function of `Match -> coerced parameters` to coerce parameters
A pre-requisite to use [[coerce!]].
NOTE: this is not needed with ring/http, where the coercion compilation is
managed in the request coercion middleware/interceptors."
[[_ {:keys [parameters coercion] :as data}] opts]
(if (and parameters coercion)
(request-coercers coercion parameters opts)))
(let [{:keys [parameters] :as data} (-compile-parameters data coercion)]
{:data data
:coerce (request-coercers coercion parameters opts)})))
(defn coerce!
"Returns a map of coerced input parameters using pre-compiled
coercers under `:result` (provided by [[compile-request-coercers]].
Throws `ex-info` if parameters can't be coerced
If coercion or parameters are not defined, return `nil`"
"Returns a map of coerced input parameters using pre-compiled coercers in `Match`
under path `[:result :coerce]` (provided by [[compile-request-coercers]].
Throws `ex-info` if parameters can't be coerced. If coercion or parameters
are not defined, returns `nil`"
[match]
(if-let [coercers (:result match)]
(if-let [coercers (-> match :result :coerce)]
(coerce-request coercers match)))

View file

@ -307,6 +307,7 @@
:coerce (fn coerce [route _] route)
:compile (fn compile [[_ {:keys [handler]}] _] handler)
:exception exception/exception
:update-paths [[[:parameters any?] impl/accumulate]]
:conflicts (fn throw! [conflicts] (exception/fail! :path-conflicts conflicts))})
(defn router
@ -314,21 +315,22 @@
Selects implementation based on route details. The following options
are available:
| key | description
| --------------|-------------
| `:path` | Base-path for routes
| `:routes` | Initial resolved routes (default `[]`)
| `:data` | Initial route data (default `{}`)
| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this
| `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon})
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
| `:compile` | Function of `route opts => result` to compile a route handler
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes
| `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`)
| `:meta-merge` | Function of `left right => merged` to merge route-data (default `meta-merge.core/meta-merge`)
| `:router` | Function of `routes opts => router` to override the actual router implementation"
| key | description
| ----------------|-------------
| `:path` | Base-path for routes
| `:routes` | Initial resolved routes (default `[]`)
| `:data` | Initial route data (default `{}`)
| `:spec` | clojure.spec definition for a route data, see `reitit.spec` on how to use this
| `:syntax` | Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon})
| `:expand` | Function of `arg opts => data` to expand route arg to route data (default `reitit.core/expand`)
| `:coerce` | Function of `route opts => route` to coerce resolved route, can throw or return `nil`
| `:compile` | Function of `route opts => result` to compile a route handler
| `:validate` | Function of `routes opts => ()` to validate route (data) via side-effects
| `:conflicts` | Function of `{route #{route}} => ()` to handle conflicting routes
| `:exception` | Function of `Exception => Exception ` to handle creation time exceptions (default `reitit.exception/exception`)
| `:meta-merge` | Function of `left right => merged` to merge route-data (default `meta-merge.core/meta-merge`)
| `:update-paths` | Sequence of Vectors with elements `update-path` and `function`, used to preprocess route data
| `:router` | Function of `routes opts => router` to override the actual router implementation"
([raw-routes]
(router raw-routes {}))
([raw-routes opts]

View file

@ -105,8 +105,10 @@
(defn map-data [f routes]
(mapv (fn [[p ds]] [p (f p ds)]) routes))
(defn meta-merge [left right opts]
((or (:meta-merge opts) mm/meta-merge) left right))
(defn meta-merge [left right {:keys [meta-merge update-paths]}]
(let [update (if update-paths #(path-update % update-paths) identity)
merge (or meta-merge mm/meta-merge)]
(merge (update left) (update right))))
(defn merge-data [opts p x]
(reduce

View file

@ -22,11 +22,12 @@
compile (fn [[path data] opts scope]
(interceptor/compile-result [path data] opts scope))
->endpoint (fn [p d m s]
(let [compiled (compile [p d] opts s)]
(-> compiled
(map->Endpoint)
(assoc :path p)
(assoc :method m))))
(let [d (ring/-compile-coercion d)]
(let [compiled (compile [p d] opts s)]
(-> compiled
(map->Endpoint)
(assoc :path p)
(assoc :method m)))))
->methods (fn [any? data]
(reduce
(fn [acc method]
@ -67,6 +68,7 @@
([data opts]
(let [opts (merge {:coerce coerce-handler
:compile compile-result
:update-paths (ring/-update-paths impl/accumulate)
::default-options-endpoint ring/default-options-endpoint} opts)]
(when (contains? opts ::default-options-handler)
(ex/fail! (str "Option :reitit.http/default-options-handler is deprecated."

View file

@ -139,85 +139,84 @@
parameters (dissoc parameters :request :body :multipart)
->schema-object (fn [schema opts]
(let [current-opts (merge options opts)]
(json-schema/transform (coercion/-compile-model coercion schema current-opts)
current-opts)))]
(json-schema/transform schema current-opts)))]
(merge
(when (seq parameters)
{:parameters
(->> (for [[in schema] parameters
:let [{:keys [properties required]} (->schema-object schema {:in in :type :parameter})
required? (partial contains? (set required))]
[k schema] properties]
(merge {:in (name in)
:name k
:required (required? k)
:schema schema}
(select-keys schema [:description])))
(into []))})
(when body
;; body uses a single schema to describe every :requestBody
;; the schema-object transformer should be able to transform into distinct content-types
{:requestBody {:content (into {}
(map (fn [content-type]
(let [schema (->schema-object body {:in :requestBody
(when (seq parameters)
{:parameters
(->> (for [[in schema] parameters
:let [{:keys [properties required]} (->schema-object schema {:in in :type :parameter})
required? (partial contains? (set required))]
[k schema] properties]
(merge {:in (name in)
:name k
:required (required? k)
:schema schema}
(select-keys schema [:description])))
(into []))})
(when body
;; body uses a single schema to describe every :requestBody
;; the schema-object transformer should be able to transform into distinct content-types
{:requestBody {:content (into {}
(map (fn [content-type]
(let [schema (->schema-object body {:in :requestBody
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content-types)}})
(when request
;; request allow to different :requestBody per content-type
{:requestBody
{:content (merge
(when (:body request)
(into {}
(map (fn [content-type]
(let [schema (->schema-object (:body request) {:in :requestBody
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content-types)}})
(when request
;; request allow to different :requestBody per content-type
{:requestBody
{:content (merge
(when (:body request)
(into {}
(map (fn [content-type]
(let [schema (->schema-object (:body request) {:in :requestBody
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content-types))
(into {}
(map (fn [[content-type requestBody]]
(let [schema (->schema-object requestBody {:in :requestBody
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
(:content request)))}})
(when multipart
{:requestBody
{:content
{"multipart/form-data"
{:schema
(->schema-object multipart {:in :requestBody
:type :schema
:content-type "multipart/form-data"})}}}})
(when responses
{:responses
(into {}
(map (fn [[status {:keys [body content]
:as response}]]
(let [content (merge
(when body
(into {}
(map (fn [content-type]
(let [schema (->schema-object body {:in :responses
[content-type {:schema schema}])))
content-types))
(into {}
(map (fn [[content-type requestBody]]
(let [schema (->schema-object requestBody {:in :requestBody
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
(:content request)))}})
(when multipart
{:requestBody
{:content
{"multipart/form-data"
{:schema
(->schema-object multipart {:in :requestBody
:type :schema
:content-type "multipart/form-data"})}}}})
(when responses
{:responses
(into {}
(map (fn [[status {:keys [body content]
:as response}]]
(let [content (merge
(when body
(into {}
(map (fn [content-type]
(let [schema (->schema-object body {:in :responses
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content-types))
(when content
(into {}
(map (fn [[content-type schema]]
(let [schema (->schema-object schema {:in :responses
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content-types))
[content-type {:schema schema}])))
content)))]
[status (merge (select-keys response [:description])
(when content
(into {}
(map (fn [[content-type schema]]
(let [schema (->schema-object schema {:in :responses
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content)))]
[status (merge (select-keys response [:description])
(when content
{:content content}))])))
responses)}))))
{:content content}))])))
responses)}))))
(defn create
([]
@ -226,7 +225,8 @@
(let [{:keys [transformers lite compile options error-keys encode-error] :as opts} (merge default-options opts)
show? (fn [key] (contains? error-keys key))
transformers (walk/prewalk #(if (satisfies? TransformationProvider %) (-transformer % opts) %) transformers)
compile (if lite (fn [schema options] (compile (binding [l/*options* options] (l/schema schema)) options))
compile (if lite (fn [schema options]
(compile (binding [l/*options* options] (l/schema schema)) options))
compile)]
^{:type ::coercion/coercion}
(reify coercion/Coercion
@ -238,7 +238,7 @@
(if parameters
{:parameters
(->> (for [[in schema] parameters
parameter (extract-parameter in (compile schema options) options)]
parameter (extract-parameter in schema options)]
parameter)
(into []))})
(if responses
@ -250,16 +250,17 @@
(set/rename-keys $ {:body :schema})
(update $ :description (fnil identity ""))
(if (:schema $)
(-> $
(update :schema compile options)
(update :schema swagger/transform {:type :schema}))
(update $ :schema swagger/transform {:type :schema})
$))]))}))
:openapi (-get-apidocs-openapi this data options)
(throw
(ex-info
(str "Can't produce Schema apidocs for " specification)
{:type specification, :coercion :schema}))))
(-compile-model [_ model _] (compile model options))
(-compile-model [_ model _]
(if (= 1 (count model))
(compile (first model) options)
(reduce (fn [x y] (mu/merge x y options)) (map #(compile % options) model))))
(-open-model [_ schema] schema)
(-encode-error [_ error]
(cond-> error
@ -270,8 +271,8 @@
(seq error-keys) (select-keys error-keys)
encode-error (encode-error)))
(-request-coercer [_ type schema]
(-coercer (compile schema options) type transformers :decode opts))
(-coercer schema type transformers :decode opts))
(-response-coercer [_ schema]
(-coercer (compile schema options) :response transformers :encode opts))))))
(-coercer schema :response transformers :encode opts))))))
(def coercion (create default-options))

View file

@ -3,6 +3,7 @@
#?@(:clj [[ring.util.mime-type :as mime-type]
[ring.util.response :as response]])
[reitit.core :as r]
[reitit.coercion :as coercion]
[reitit.exception :as ex]
[reitit.impl :as impl]
[reitit.middleware :as middleware]))
@ -28,16 +29,37 @@
(update acc method expand opts)
acc)) data http-methods)])
(defn -update-paths [f]
(let [not-request? #(not= :request %)
http-method? #(contains? http-methods %)]
[;; default parameters and responses
[[:parameters not-request?] f]
[[http-method? :parameters not-request?] f]
[[:responses any? :body] f]
[[http-method? :responses any? :body] f]
;; openapi3 parameters and responses
[[:parameters :request :content any?] f]
[[http-method? :parameters :request :content any?] f]
[[:parameters :request :body] f]
[[http-method? :parameters :request :body] f]
[[:responses any? :content any?] f]
[[http-method? :responses any? :content any?] f]]))
(defn -compile-coercion [{:keys [coercion] :as data}]
(cond-> data coercion (impl/path-update (-update-paths #(coercion/-compile-model coercion % nil)))))
(defn compile-result [[path data] {:keys [::default-options-endpoint expand] :as opts}]
(let [[top childs] (group-keys data)
childs (cond-> childs
(and (not (:options childs)) (not (:handler top)) default-options-endpoint)
(assoc :options (expand default-options-endpoint opts)))
->endpoint (fn [p d m s]
(-> (middleware/compile-result [p d] opts s)
(map->Endpoint)
(assoc :path p)
(assoc :method m)))
(let [d (-compile-coercion d)]
(-> (middleware/compile-result [p d] opts s)
(map->Endpoint)
(assoc :path p)
(assoc :method m))))
->methods (fn [any? data]
(reduce
(fn [acc method]
@ -97,6 +119,7 @@
([data opts]
(let [opts (merge {:coerce coerce-handler
:compile compile-result
:update-paths (-update-paths impl/accumulate)
::default-options-endpoint default-options-endpoint}
opts)]
(when (contains? opts ::default-options-handler)

View file

@ -54,28 +54,16 @@
:swagger (swagger/swagger-spec
(merge
(if parameters
{::swagger/parameters
(into
(empty parameters)
(for [[k v] parameters]
[k (coercion/-compile-model this v nil)]))})
{::swagger/parameters parameters})
(if responses
{::swagger/responses
(into
(empty responses)
(for [[k response] responses]
[k (as-> response $
(set/rename-keys $ {:body :schema})
(if (:schema $)
(update $ :schema #(coercion/-compile-model this % nil))
$))]))})))
[k (set/rename-keys response {:body :schema})]))})))
:openapi (merge
(when (seq (dissoc parameters :body :request :multipart))
(openapi/openapi-spec {::openapi/parameters
(into
(empty parameters)
(for [[k v] (dissoc parameters :body :request)]
[k (coercion/-compile-model this v nil)]))}))
(openapi/openapi-spec {::openapi/parameters (dissoc parameters :body :request)}))
(when (:body parameters)
{:requestBody (openapi/openapi-spec
{::openapi/content (zipmap content-types (repeat (:body parameters)))})})
@ -92,23 +80,26 @@
(when responses
{:responses
(into
(empty responses)
(for [[k {:keys [body content] :as response}] responses]
[k (merge
(select-keys response [:description])
(when (or body content)
(openapi/openapi-spec
{::openapi/content (merge
(when body
(zipmap content-types (repeat (coercion/-compile-model this body nil))))
(when response
(:content response)))})))]))}))
(empty responses)
(for [[k {:keys [body content] :as response}] responses]
[k (merge
(select-keys response [:description])
(when (or body content)
(openapi/openapi-spec
{::openapi/content (merge
(when body
(zipmap content-types (repeat body)))
(when response
(:content response)))})))]))}))
(throw
(ex-info
(str "Can't produce Schema apidocs for " specification)
{:type specification, :coercion :schema}))))
(-compile-model [_ model _] model)
(-compile-model [_ model _]
(if (= 1 (count model))
(first model)
(apply st/merge model)))
(-open-model [_ schema] (st/open-schema schema))
(-encode-error [_ error]
(-> error

View file

@ -1,7 +1,9 @@
(ns reitit.coercion.spec
(:require [clojure.set :as set]
[clojure.spec.alpha :as s]
[meta-merge.core :as mm]
[reitit.coercion :as coercion]
[reitit.exception :as ex]
[spec-tools.core :as st #?@(:cljs [:refer [Spec]])]
[spec-tools.data-spec :as ds #?@(:cljs [:refer [Maybe]])]
[spec-tools.openapi.core :as openapi]
@ -66,7 +68,7 @@
(st/create-spec {:spec this}))
nil
(into-spec [this _]))
(into-spec [_ _]))
(defn stringify-pred [pred]
(str (if (seq? pred) (seq pred) pred)))
@ -92,44 +94,30 @@
:swagger (swagger/swagger-spec
(merge
(if parameters
{::swagger/parameters
(into
(empty parameters)
(for [[k v] parameters]
[k (coercion/-compile-model this v nil)]))})
{::swagger/parameters parameters})
(if responses
{::swagger/responses
(into
(empty responses)
(for [[k response] responses]
[k (as-> response $
(set/rename-keys $ {:body :schema})
(if (:schema $)
(update $ :schema #(coercion/-compile-model this % nil))
$))]))})))
(set/rename-keys $ {:body :schema}))]))})))
:openapi (merge
(when (seq (dissoc parameters :body :request :multipart))
(openapi/openapi-spec {::openapi/parameters
(into (empty parameters)
(for [[k v] (dissoc parameters :body :request)]
[k (coercion/-compile-model this v nil)]))}))
(openapi/openapi-spec {::openapi/parameters (dissoc parameters :body :request)}))
(when (:body parameters)
{:requestBody (openapi/openapi-spec
{::openapi/content (zipmap content-types (repeat (coercion/-compile-model this (:body parameters) nil)))})})
{::openapi/content (zipmap content-types (repeat (:body parameters)))})})
(when (:request parameters)
{:requestBody (openapi/openapi-spec
{::openapi/content (merge
(when-let [default (get-in parameters [:request :body])]
(zipmap content-types (repeat (coercion/-compile-model this default nil))))
(into {}
(for [[format model] (:content (:request parameters))]
[format (coercion/-compile-model this model nil)])))})})
(zipmap content-types (repeat default)))
(:content (:request parameters)))})})
(when (:multipart parameters)
{:requestBody
(openapi/openapi-spec
{::openapi/content
{"multipart/form-data"
(coercion/-compile-model this (:multipart parameters) nil)}})})
{:requestBody
(openapi/openapi-spec
{::openapi/content {"multipart/form-data" (:multipart parameters)}})})
(when responses
{:responses
(into
@ -141,26 +129,33 @@
(openapi/openapi-spec
{::openapi/content (merge
(when body
(zipmap content-types (repeat (coercion/-compile-model this (:body response) nil))))
(zipmap content-types (repeat (:body response))))
(when response
(into {}
(for [[format model] (:content response)]
[format (coercion/-compile-model this model nil)]))))})))]))}))
(:content response)))})))]))}))
(throw
(ex-info
(str "Can't produce Spec apidocs for " specification)
{:specification specification, :coercion :spec}))))
(-compile-model [_ model name]
(into-spec model name))
(into-spec
(cond
;; we are safe!
(= (count model) 1) (first model)
;; here be dragons, best effort
(every? map? model) (apply mm/meta-merge model)
;; not sure if this is what we want
(every? s/spec? model) (reduce (fn [acc s] (st/merge acc s)) model)
;; fail fast
:else (ex/fail! ::model-error {:message "Can't merge nested data-specs & specs together", :spec model}))
name))
(-open-model [_ spec] spec)
(-encode-error [_ error]
(let [problems (-> error :problems ::s/problems)]
(-> error
(update :spec (comp str s/form))
(assoc :problems (mapv #(update % :pred stringify-pred) problems)))))
(-request-coercer [this type spec]
(let [spec (coercion/-compile-model this spec nil)
{:keys [formats default]} (transformers type)]
(-request-coercer [_ type spec]
(let [{:keys [formats default]} (transformers type)]
(fn [value format]
(if-let [transformer (or (get formats format) default)]
(let [coerced (st/coerce spec value transformer)]

View file

@ -14,27 +14,28 @@
(deftest coercion-test
(let [r (r/router
[["/schema" {:coercion reitit.coercion.schema/coercion}
["/:number/:keyword" {:parameters {:path {:number s/Int
:keyword s/Keyword}
:query (s/maybe {:int s/Int, :ints [s/Int], :map {s/Int s/Int}})}}]]
["/:number" {:parameters {:path {:number s/Int}}}
["/:keyword" {:parameters {:path {:keyword s/Keyword}
: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?]]]]}}]]
["/:number" {:parameters {:path [:map [:number int?]]}}
["/:keyword" {:parameters {:path [:map [:keyword keyword?]]
:query [:maybe [:map [:int int?]
[:ints [:vector int?]]
[:map [:map-of int? int?]]]]}}]]]
["/malli-lite" {:coercion reitit.coercion.malli/coercion}
["/:number/:keyword" {:parameters {:path {:number int?
:keyword keyword?}
:query (l/maybe {:int int?
:ints (l/vector int?)
:map (l/map-of int? int?)})}}]]
["/:number" {:parameters {:path {:number int?}}}
["/:keyword" {:parameters {:path {:keyword keyword?}
:query (l/maybe {:int int?
:ints (l/vector int?)
:map (l/map-of int? int?)})}}]]]
["/spec" {:coercion reitit.coercion.spec/coercion}
["/:number/:keyword" {:parameters {:path {:number int?
:keyword keyword?}
:query (ds/maybe {:int int?, :ints [int?], :map {int? int?}})}}]]
["/:number" {:parameters {:path {:number int?}}}
["/:keyword" {:parameters {:path {:keyword keyword?}
:query (ds/maybe {:int int?, :ints [int?], :map {int? int?}})}}]]]
["/none"
["/:number/:keyword" {:parameters {:path {:number int?
:keyword keyword?}}}]]]
["/:number" {:parameters {:path {:number int?}}}
["/:keyword" {:parameters {:path {:keyword keyword?}}}]]]]
{:compile coercion/compile-request-coercers})]
(testing "schema-coercion"

View file

@ -186,3 +186,24 @@
[[any? :parameters any?] vector]
[[:responses any? :body] vector]
[[any? :responses any? :body] vector]]))))
(deftest meta-merge-test
(is (= {:get {:responses {200 {:body [[:map [:total :int]]
[:map [:total :int]]]}},
:parameters {:query [[:map [:x :int]]
[:map [:y :int]]]}},
:parameters {:query [[:map [:x :int]]
[:map [:y :int]]]},
:post {:parameters {:query [[:map [:y :int]]]}}}
(impl/meta-merge
{:parameters {:query [:map [:x :int]]}
:get {:parameters {:query [:map [:x :int]]}
:responses {200 {:body [:map [:total :int]]}}}}
{:parameters {:query [:map [:y :int]]}
:get {:parameters {:query [:map [:y :int]]}
:responses {200 {:body [:map [:total :int]]}}}
:post {:parameters {:query [:map [:y :int]]}}}
{:update-paths [[[:parameters any?] vector]
[[any? :parameters any?] vector]
[[:responses any? :body] vector]
[[any? :responses any? :body] vector]]}))))

View file

@ -234,14 +234,12 @@
([] {})
([left] left)
([left right]
(if (and (map? left) (map? right)
(contains? left :parameters)
(contains? right :parameters))
(-> (merge-with custom-meta-merge-checking-parameters left right)
(assoc :parameters (merge-with mu/merge
(:parameters left)
(:parameters right))))
(meta-merge left right)))
(let [pleft (-> left :parameters :path)
pright (-> right :parameters :path)]
(if (and (map? left) (map? right) pleft pright)
(-> (merge-with custom-meta-merge-checking-parameters left right)
(assoc-in [:parameters :path] (reduce mu/merge (concat pleft pright))))
(meta-merge left right))))
([left right & more]
(reduce custom-meta-merge-checking-parameters left (cons right more))))
@ -586,43 +584,43 @@
(deftest per-content-type-test
(doseq [[coercion json-request edn-request default-request json-response edn-response default-response]
[[#'malli/coercion
[[malli/coercion
[:map [:request [:enum :json]] [:response any?]]
[:map [:request [:enum :edn]] [:response any?]]
[:map [:request [:enum :default]] [:response any?]]
[:map [:request any?] [:response [:enum :json]]]
[:map [:request any?] [:response [:enum :edn]]]
[:map [:request any?] [:response [:enum :default]]]]
[#'schema/coercion
[schema/coercion
{:request (s/eq :json) :response s/Any}
{:request (s/eq :edn) :response s/Any}
{:request (s/eq :default) :response s/Any}
{:request s/Any :response (s/eq :json)}
{:request s/Any :response (s/eq :edn)}
{:request s/Any :response (s/eq :default)}]
[#'spec/coercion
[spec/coercion
{:request (clojure.spec.alpha/spec #{:json}) :response any?}
{:request (clojure.spec.alpha/spec #{:edn}) :response any?}
{:request (clojure.spec.alpha/spec #{:default}) :response any?}
{:request any? :response (clojure.spec.alpha/spec #{:json})}
{:request any? :response (clojure.spec.alpha/spec #{:end})}
{:request any? :response (clojure.spec.alpha/spec #{:default})}]]]
(testing coercion
(testing (str coercion)
(let [app (ring/ring-handler
(ring/router
[["/foo" {:post {:parameters {:request {:content {"application/json" json-request
"application/edn" edn-request}
:body default-request}}
:responses {200 {:content {"application/json" json-response
"application/edn" edn-response}
:body default-response}}
:handler (fn [req]
{:status 200
:body (-> req :parameters :request)})}}]]
{:validate reitit.ring.spec/validate
["/foo" {:post {:parameters {:request {:content {"application/json" json-request
"application/edn" edn-request}
:body default-request}}
:responses {200 {:content {"application/json" json-response
"application/edn" edn-response}
:body default-response}}
:handler (fn [req]
{:status 200
:body (-> req :parameters :request)})}}]
{#_#_:validate reitit.ring.spec/validate
:data {:middleware [rrc/coerce-request-middleware
rrc/coerce-response-middleware]
:coercion @coercion}}))
:coercion coercion}}))
call (fn [request]
(try
(app request)