diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc index 75b78d79..2bf11482 100644 --- a/modules/reitit-core/src/reitit/coercion.cljc +++ b/modules/reitit-core/src/reitit/coercion.cljc @@ -13,6 +13,8 @@ (-get-name [this] "Keyword name for the coercion") (-get-options [this] "Coercion options") (-get-apidocs [this specification data] "Returns api documentation") + ;; TODO doc options: + (-get-model-apidocs [this specification model options] "Convert model into a format that can be used in api docs") (-compile-model [this model name] "Compiles a model") (-open-model [this model] "Returns a new model which allows extra keys in maps") (-encode-error [this error] "Converts error in to a serializable format") @@ -189,37 +191,6 @@ (defn -compile-parameters [data coercion] (impl/path-update data [[[:parameters any?] #(-compile-model coercion % nil)]])) -;; -;; api-docs -;; - -(defn -warn-unsupported-coercions [{:keys [request responses] :as _data}] - (when request - (println "WARNING [reitit.coercion]: swagger apidocs don't support :request coercion")) - (when (some :content (vals responses)) - (println "WARNING [reitit.coercion]: swagger apidocs don't support :responses :content coercion"))) - -(defn get-apidocs [coercion specification data] - (let [swagger-parameter {:query :query - :body :body - :form :formData - :header :header - :path :path - :multipart :formData}] - (case specification - :openapi (-get-apidocs coercion specification data) - :swagger (do - (-warn-unsupported-coercions data) - (->> (update - data - :parameters - (fn [parameters] - (->> parameters - (map (fn [[k v]] [(swagger-parameter k) v])) - (filter first) - (into {})))) - (-get-apidocs coercion specification)))))) - ;; ;; integration ;; diff --git a/modules/reitit-malli/src/reitit/coercion/malli.cljc b/modules/reitit-malli/src/reitit/coercion/malli.cljc index 03cf456a..311660f7 100644 --- a/modules/reitit-malli/src/reitit/coercion/malli.cljc +++ b/modules/reitit-malli/src/reitit/coercion/malli.cljc @@ -133,102 +133,6 @@ ;; malli options :options nil}) -;; TODO: this is now seems like a generic transforming function that could be used in all of malli, spec, schema -;; ... just tranform the schemas in place -;; also, this has internally massive amount of duplicate code, could be simplified -;; ... tests too -(defn -get-apidocs-openapi - [_ {:keys [request parameters responses content-types] :or {content-types ["application/json"]}} options] - (let [{:keys [body multipart]} parameters - parameters (dissoc parameters :request :body :multipart) - ->schema-object (fn [schema opts] - (let [current-opts (merge options opts)] - (json-schema/transform schema current-opts))) - ->content (fn [data schema] - (merge - {:schema schema} - (select-keys data [:description :examples]) - (:openapi data)))] - (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 - :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 - (select-keys request [:description]) - (when-let [{:keys [schema] :as data} (coercion/get-default request)] - (into {} - (map (fn [content-type] - (let [schema (->schema-object schema {:in :requestBody - :type :schema - :content-type content-type})] - [content-type (->content data schema)]))) - content-types)) - (into {} - (map (fn [[content-type {:keys [schema] :as data}]] - (let [schema (->schema-object schema {:in :requestBody - :type :schema - :content-type content-type})] - [content-type (->content data 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 [content], :as response}]] - (let [default (coercion/get-default-schema response) - content (-> (merge - (when default - (into {} - (map (fn [content-type] - (let [schema (->schema-object default {:in :responses - :type :schema - :content-type content-type})] - [content-type (->content nil schema)]))) - content-types)) - (when content - (into {} - (map (fn [[content-type {:keys [schema] :as data}]] - (let [schema (->schema-object schema {:in :responses - :type :schema - :content-type content-type})] - [content-type (->content data schema)]))) - content))) - (dissoc :default))] - [status (merge (select-keys response [:description]) - (when content - {:content content}))])) - responses))})))) - (defn create ([] (create nil)) @@ -243,6 +147,13 @@ (reify coercion/Coercion (-get-name [_] :malli) (-get-options [_] opts) + (-get-model-apidocs [this specification model options] + (case specification + :openapi (json-schema/transform model (merge opts options)) + (throw + (ex-info + (str "Can't produce Malli apidocs for " specification) + {:type specification, :coercion :malli})))) (-get-apidocs [this specification {:keys [parameters responses] :as data}] (case specification :swagger (merge @@ -264,11 +175,11 @@ (if (:schema $) (update $ :schema swagger/transform {:type :schema}) $))]))})) - :openapi (-get-apidocs-openapi this data options) + ;; :openapi handled in reitit.openapi/-get-apidocs-openapi (throw (ex-info - (str "Can't produce Schema apidocs for " specification) - {:type specification, :coercion :schema})))) + (str "Can't produce Malli apidocs for " specification) + {:type specification, :coercion :malli})))) (-compile-model [_ model _] (if (= 1 (count model)) (compile (first model) options) diff --git a/modules/reitit-openapi/src/reitit/openapi.cljc b/modules/reitit-openapi/src/reitit/openapi.cljc index 89bfd3df..68dc2caa 100644 --- a/modules/reitit-openapi/src/reitit/openapi.cljc +++ b/modules/reitit-openapi/src/reitit/openapi.cljc @@ -73,6 +73,96 @@ (defn- openapi-path [path opts] (-> path (trie/normalize opts) (str/replace #"\{\*" "{"))) +(defn -get-apidocs-openapi + [coercion {:keys [request parameters responses content-types] :or {content-types ["application/json"]}}] + (let [{:keys [body multipart]} parameters + parameters (dissoc parameters :request :body :multipart) + ->content (fn [data schema] + (merge + {:schema schema} + (select-keys data [:description :examples]) + (:openapi data))) + ->schema-object #(coercion/-get-model-apidocs coercion :openapi %1 %2)] + (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 + :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 + (select-keys request [:description]) + (when-let [{:keys [schema] :as data} (coercion/get-default request)] + (into {} + (map (fn [content-type] + (let [schema (->schema-object schema {:in :requestBody + :type :schema + :content-type content-type})] + [content-type (->content data schema)]))) + content-types)) + (into {} + (map (fn [[content-type {:keys [schema] :as data}]] + (let [schema (->schema-object schema {:in :requestBody + :type :schema + :content-type content-type})] + [content-type (->content data 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 [content], :as response}]] + (let [default (coercion/get-default-schema response) + content (-> (merge + (when default + (into {} + (map (fn [content-type] + (let [schema (->schema-object default {:in :responses + :type :schema + :content-type content-type})] + [content-type (->content nil schema)]))) + content-types)) + (when content + (into {} + (map (fn [[content-type {:keys [schema] :as data}]] + (let [schema (->schema-object schema {:in :responses + :type :schema + :content-type content-type})] + [content-type (->content data schema)]))) + content))) + (dissoc :default))] + [status (merge (select-keys response [:description]) + (when content + {:content content}))])) + responses))})))) + (defn create-openapi-handler "Stability: alpha @@ -99,7 +189,7 @@ (apply meta-merge (keep (comp :openapi :data) middleware)) (apply meta-merge (keep (comp :openapi :data) interceptors)) (if coercion - (coercion/get-apidocs coercion :openapi data)) + (-get-apidocs-openapi coercion data)) (select-keys data [:tags :summary :description]) (strip-top-level-keys openapi))])) transform-path (fn [[p _ c]] diff --git a/modules/reitit-schema/src/reitit/coercion/schema.cljc b/modules/reitit-schema/src/reitit/coercion/schema.cljc index 321cd92e..b2fd14d1 100644 --- a/modules/reitit-schema/src/reitit/coercion/schema.cljc +++ b/modules/reitit-schema/src/reitit/coercion/schema.cljc @@ -47,6 +47,13 @@ (reify coercion/Coercion (-get-name [_] :schema) (-get-options [_] opts) + (-get-model-apidocs [_ specification model options] + (case specification + :openapi (openapi/transform model (merge opts options)) + (throw + (ex-info + (str "Can't produce Schema apidocs for " specification) + {:type specification, :coercion :schema})))) (-get-apidocs [_ specification {:keys [request parameters responses content-types] :or {content-types ["application/json"]}}] ;; TODO: this looks identical to spec, refactor when schema is done. @@ -63,42 +70,7 @@ [k (-> response (dissoc :content) (set/rename-keys {:body :schema}))]))}))) - :openapi (merge - (when (seq (dissoc parameters :body :request :multipart)) - (openapi/openapi-spec {::openapi/parameters (dissoc parameters :body :request)})) - (when (:body parameters) - {:requestBody (openapi/openapi-spec - {::openapi/content (zipmap content-types (repeat (:body parameters)))})}) - (when request - {:requestBody (openapi/openapi-spec - {::openapi/content (merge - (when-let [default (coercion/get-default-schema request)] - (zipmap content-types (repeat default))) - (->> (for [[content-type {:keys [schema]}] (:content request)] - [content-type schema]) - (into {})))})}) - (when (:multipart parameters) - {:requestBody - (openapi/openapi-spec - {::openapi/content {"multipart/form-data" (:multipart parameters)}})}) - (when responses - {:responses - (into - (empty responses) - (for [[k {:keys [content] :as response}] responses - :let [default (coercion/get-default-schema response)]] - [k (merge - (select-keys response [:description]) - (when (or content default) - (openapi/openapi-spec - {::openapi/content (-> (merge - (when default - (zipmap content-types (repeat default))) - (->> (for [[content-type {:keys [schema]}] content] - [content-type schema]) - (into {}))) - (dissoc :default))})))]))})) - + ;; :openapi handled in reitit.openapi/-get-apidocs-openapi (throw (ex-info (str "Can't produce Schema apidocs for " specification) diff --git a/modules/reitit-spec/src/reitit/coercion/spec.cljc b/modules/reitit-spec/src/reitit/coercion/spec.cljc index 85798f42..bf4bed82 100644 --- a/modules/reitit-spec/src/reitit/coercion/spec.cljc +++ b/modules/reitit-spec/src/reitit/coercion/spec.cljc @@ -88,6 +88,13 @@ (reify coercion/Coercion (-get-name [_] :spec) (-get-options [_] opts) + (-get-model-apidocs [_ specification model options] + (case specification + :openapi (openapi/transform model (merge opts options)) + (throw + (ex-info + (str "Can't produce Spec apidocs for " specification) + {:type specification, :coercion :spec})))) (-get-apidocs [_ specification {:keys [request parameters responses content-types] :or {content-types ["application/json"]}}] (case specification @@ -103,42 +110,7 @@ [k (as-> response $ (dissoc $ :content) (set/rename-keys $ {:body :schema}))]))}))) - :openapi (merge - (when (seq (dissoc parameters :body :request :multipart)) - (openapi/openapi-spec {::openapi/parameters (dissoc parameters :body :request)})) - (when (:body parameters) - {:requestBody (openapi/openapi-spec - {::openapi/content (zipmap content-types (repeat (:body parameters)))})}) - (when request - {:requestBody (openapi/openapi-spec - {::openapi/content (merge - (when-let [default (coercion/get-default-schema request)] - (zipmap content-types (repeat default))) - (->> (for [[content-type {:keys [schema]}] (:content request)] - [content-type schema]) - (into {})))})}) - (when (:multipart parameters) - {:requestBody - (openapi/openapi-spec - {::openapi/content {"multipart/form-data" (:multipart parameters)}})}) - (when responses - {:responses - (into - (empty responses) - (for [[k {:keys [content] :as response}] responses - :let [default (coercion/get-default-schema response) - content-types (remove #{:default} content-types)]] - [k (merge - (select-keys response [:description]) - (when (or content default) - (openapi/openapi-spec - {::openapi/content (-> (merge - (when default - (zipmap content-types (repeat default))) - (->> (for [[content-type {:keys [schema]}] content] - [content-type schema]) - (into {}))) - (dissoc :default))})))]))})) + ;; :openapi handled in reitit.openapi/-get-apidocs-openapi (throw (ex-info (str "Can't produce Spec apidocs for " specification) diff --git a/modules/reitit-swagger/src/reitit/swagger.cljc b/modules/reitit-swagger/src/reitit/swagger.cljc index 3c3403cb..9525035b 100644 --- a/modules/reitit-swagger/src/reitit/swagger.cljc +++ b/modules/reitit-swagger/src/reitit/swagger.cljc @@ -69,6 +69,30 @@ (defn- swagger-path [path opts] (-> path (trie/normalize opts) (str/replace #"\{\*" "{"))) +(defn -warn-unsupported-coercions [{:keys [request responses] :as _data}] + (when request + (println "WARNING [reitit.coercion]: swagger apidocs don't support :request coercion")) + (when (some :content (vals responses)) + (println "WARNING [reitit.coercion]: swagger apidocs don't support :responses :content coercion"))) + +(defn -get-swagger-apidocs [coercion data] + (let [swagger-parameter {:query :query + :body :body + :form :formData + :header :header + :path :path + :multipart :formData}] + (-warn-unsupported-coercions data) + (->> (update + data + :parameters + (fn [parameters] + (->> parameters + (map (fn [[k v]] [(swagger-parameter k) v])) + (filter first) + (into {})))) + (coercion/-get-apidocs coercion :swagger)))) + (defn create-swagger-handler "Create a ring handler to emit swagger spec. Collects all routes from router which have an intersecting `[:swagger :id]` and which are not marked with `:no-doc` route data." @@ -95,7 +119,7 @@ (apply meta-merge (keep (comp :swagger :data) middleware)) (apply meta-merge (keep (comp :swagger :data) interceptors)) (if coercion - (coercion/get-apidocs coercion :swagger data)) + (-get-swagger-apidocs coercion data)) (select-keys data [:tags :summary :description :operationId]) (strip-top-level-keys swagger))])) transform-path (fn [[p _ c]] diff --git a/test/cljc/reitit/openapi_test.clj b/test/cljc/reitit/openapi_test.clj index f147c407..3724b7ac 100644 --- a/test/cljc/reitit/openapi_test.clj +++ b/test/cljc/reitit/openapi_test.clj @@ -157,19 +157,16 @@ :version "0.0.1"} :paths {"/api/spec/plus/{z}" {:get {:parameters [{:in "query" :name "x" - :description "" :required true :schema {:type "integer" :format "int64"}} {:in "query" :name "y" - :description "" :required true :schema {:type "integer" :format "int64"}} {:in "path" :name "z" - :description "" :required true :schema {:type "integer" :format "int64"}}] @@ -188,7 +185,6 @@ :post {:parameters [{:in "path" :name "z" :required true - :description "" :schema {:type "integer" :format "int64"}}] :requestBody {:content {"application/json" {:schema {:oneOf [{:items {:type "integer" @@ -251,21 +247,18 @@ :required [:error] :type "object"}}}}} :summary "plus with body"}} - "/api/schema/plus/{z}" {:get {:parameters [{:description "" - :in "query" + "/api/schema/plus/{z}" {:get {:parameters [{:in "query" :name "x" :required true :schema {:format "int32" :type "integer"}} - {:description "" - :in "query" + {:in "query" :name "y" :required true :schema {:type "integer" :format "int32"}} {:in "path" :name "z" - :description "" :required true :schema {:type "integer" :format "int32"}}] @@ -282,7 +275,6 @@ :summary "plus"} :post {:parameters [{:in "path" :name "z" - :description "" :required true :schema {:type "integer" :format "int32"}}]