Skip to content

Commit

Permalink
Merge pull request #638 from metosin/openapi-refactor
Browse files Browse the repository at this point in the history
share openapi generation code between malli, spec & schema
  • Loading branch information
opqdonut authored Aug 28, 2023
2 parents b0c810a + 8af89c0 commit 7b88125
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 214 deletions.
33 changes: 2 additions & 31 deletions modules/reitit-core/src/reitit/coercion.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
;;
Expand Down
109 changes: 10 additions & 99 deletions modules/reitit-malli/src/reitit/coercion/malli.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand All @@ -263,11 +174,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)
Expand Down
92 changes: 91 additions & 1 deletion modules/reitit-openapi/src/reitit/openapi.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]]
Expand Down
44 changes: 8 additions & 36 deletions modules/reitit-schema/src/reitit/coercion/schema.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -61,42 +68,7 @@
(empty responses)
(for [[k response] responses]
[k (set/rename-keys response {: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)
Expand Down
Loading

0 comments on commit 7b88125

Please sign in to comment.