From ce06214014499caf86d3abd65884a85e7004b53d Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 21 May 2023 20:32:40 +0300 Subject: [PATCH] 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 --- CHANGELOG.md | 9 +- doc/advanced/configuring_routers.md | 33 ++-- modules/reitit-core/src/reitit/coercion.cljc | 54 +++--- modules/reitit-core/src/reitit/core.cljc | 32 ++-- modules/reitit-core/src/reitit/impl.cljc | 6 +- modules/reitit-http/src/reitit/http.cljc | 12 +- .../src/reitit/coercion/malli.cljc | 163 +++++++++--------- modules/reitit-ring/src/reitit/ring.cljc | 31 +++- .../src/reitit/coercion/schema.cljc | 45 ++--- .../reitit-spec/src/reitit/coercion/spec.cljc | 59 +++---- test/cljc/reitit/coercion_test.cljc | 35 ++-- test/cljc/reitit/impl_test.cljc | 21 +++ test/cljc/reitit/ring_coercion_test.cljc | 44 +++-- 13 files changed, 299 insertions(+), 245 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79db8dc8..cacbce06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,10 @@ We use [Break Versioning][breakver]. The version numbers follow a `.. 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 + + diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc index 2c1af0c7..0952bfab 100644 --- a/modules/reitit-core/src/reitit/coercion.cljc +++ b/modules/reitit-core/src/reitit/coercion.cljc @@ -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))) diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index cd3b092b..96264581 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -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] diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc index 173c07c9..9eb87894 100644 --- a/modules/reitit-core/src/reitit/impl.cljc +++ b/modules/reitit-core/src/reitit/impl.cljc @@ -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 diff --git a/modules/reitit-http/src/reitit/http.cljc b/modules/reitit-http/src/reitit/http.cljc index bf0f3ae5..b6038555 100644 --- a/modules/reitit-http/src/reitit/http.cljc +++ b/modules/reitit-http/src/reitit/http.cljc @@ -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." diff --git a/modules/reitit-malli/src/reitit/coercion/malli.cljc b/modules/reitit-malli/src/reitit/coercion/malli.cljc index cb9ca777..6f4dff5b 100644 --- a/modules/reitit-malli/src/reitit/coercion/malli.cljc +++ b/modules/reitit-malli/src/reitit/coercion/malli.cljc @@ -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)) diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc index 5c292734..7a4cde7f 100644 --- a/modules/reitit-ring/src/reitit/ring.cljc +++ b/modules/reitit-ring/src/reitit/ring.cljc @@ -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) diff --git a/modules/reitit-schema/src/reitit/coercion/schema.cljc b/modules/reitit-schema/src/reitit/coercion/schema.cljc index ce66e153..3dc50bb1 100644 --- a/modules/reitit-schema/src/reitit/coercion/schema.cljc +++ b/modules/reitit-schema/src/reitit/coercion/schema.cljc @@ -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 diff --git a/modules/reitit-spec/src/reitit/coercion/spec.cljc b/modules/reitit-spec/src/reitit/coercion/spec.cljc index d5edd78b..77a630a9 100644 --- a/modules/reitit-spec/src/reitit/coercion/spec.cljc +++ b/modules/reitit-spec/src/reitit/coercion/spec.cljc @@ -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)] diff --git a/test/cljc/reitit/coercion_test.cljc b/test/cljc/reitit/coercion_test.cljc index 97ab0aae..7d689507 100644 --- a/test/cljc/reitit/coercion_test.cljc +++ b/test/cljc/reitit/coercion_test.cljc @@ -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" diff --git a/test/cljc/reitit/impl_test.cljc b/test/cljc/reitit/impl_test.cljc index dee92921..c4ae1a39 100644 --- a/test/cljc/reitit/impl_test.cljc +++ b/test/cljc/reitit/impl_test.cljc @@ -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]]})))) diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc index 66d151ed..10aa8409 100644 --- a/test/cljc/reitit/ring_coercion_test.cljc +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -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)