mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 08:21:11 +00:00
Merge pull request #673 from metosin/malli-vars
Generate correct OpenAPI $ref schemas for malli var and ref schemas
This commit is contained in:
commit
c8c8c0eb03
6 changed files with 218 additions and 23 deletions
|
|
@ -19,6 +19,7 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
|
||||||
* Fetch OpenAPI content types from Muuntaja [#636](https://github.com/metosin/reitit/issues/636)
|
* Fetch OpenAPI content types from Muuntaja [#636](https://github.com/metosin/reitit/issues/636)
|
||||||
* **BREAKING** OpenAPI support is now clj only
|
* **BREAKING** OpenAPI support is now clj only
|
||||||
* Fix swagger generation when unsupported coercions are present [#671](https://github.com/metosin/reitit/pull/671)
|
* Fix swagger generation when unsupported coercions are present [#671](https://github.com/metosin/reitit/pull/671)
|
||||||
|
* Generate correct OpenAPI $ref schemas for malli var and ref schemas [#673](https://github.com/metosin/reitit/pull/673)
|
||||||
* Updated dependencies:
|
* Updated dependencies:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,28 @@
|
||||||
[reitit.ring.middleware.multipart :as multipart]
|
[reitit.ring.middleware.multipart :as multipart]
|
||||||
[reitit.ring.middleware.parameters :as parameters]
|
[reitit.ring.middleware.parameters :as parameters]
|
||||||
[ring.adapter.jetty :as jetty]
|
[ring.adapter.jetty :as jetty]
|
||||||
|
[malli.core :as malli]
|
||||||
[muuntaja.core :as m]))
|
[muuntaja.core :as m]))
|
||||||
|
|
||||||
|
(def Transaction
|
||||||
|
[:map
|
||||||
|
[:amount :double]
|
||||||
|
[:from :string]])
|
||||||
|
|
||||||
|
(def AccountId
|
||||||
|
[:map
|
||||||
|
[:bank :string]
|
||||||
|
[:id :string]])
|
||||||
|
|
||||||
|
(def Account
|
||||||
|
[:map
|
||||||
|
[:bank :string]
|
||||||
|
[:id :string]
|
||||||
|
[:balance :double]
|
||||||
|
[:transactions [:vector #'Transaction]]])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(def app
|
(def app
|
||||||
(ring/ring-handler
|
(ring/ring-handler
|
||||||
(ring/router
|
(ring/router
|
||||||
|
|
@ -89,8 +109,23 @@
|
||||||
[:email {:json-schema/example "heidi@alps.ch"}
|
[:email {:json-schema/example "heidi@alps.ch"}
|
||||||
string?]]]}}}}
|
string?]]]}}}}
|
||||||
:handler (fn [_request]
|
:handler (fn [_request]
|
||||||
[{:name "Heidi"
|
{:status 200
|
||||||
:email "heidi@alps.ch"}])}}]
|
:body [{:name "Heidi"
|
||||||
|
:email "heidi@alps.ch"}]})}}]
|
||||||
|
|
||||||
|
["/account"
|
||||||
|
{:get {:summary "Fetch an account | Recursive schemas using malli registry"
|
||||||
|
:parameters {:query #'AccountId}
|
||||||
|
:responses {200 {:content {:default {:schema #'Account}}}}
|
||||||
|
:handler (fn [_request]
|
||||||
|
{:status 200
|
||||||
|
:body {:bank "MiniBank"
|
||||||
|
:id "0001"
|
||||||
|
:balance 13.5
|
||||||
|
:transactions [{:from "0002"
|
||||||
|
:amount 20.0}
|
||||||
|
{:from "0003"
|
||||||
|
:amount -6.5}]}})}}]
|
||||||
|
|
||||||
["/secure"
|
["/secure"
|
||||||
{:tags #{"secure"}
|
{:tags #{"secure"}
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,12 @@
|
||||||
(-get-options [_] opts)
|
(-get-options [_] opts)
|
||||||
(-get-model-apidocs [this specification model options]
|
(-get-model-apidocs [this specification model options]
|
||||||
(case specification
|
(case specification
|
||||||
:openapi (json-schema/transform model (merge opts options))
|
:openapi (if (= :parameter (:type options))
|
||||||
|
;; For :parameters we need to output an object schema with actual :properties.
|
||||||
|
;; The caller will iterate through the properties and add them individually to the openapi doc.
|
||||||
|
;; Thus, we deref to get the actual [:map ..] instead of some ref-schema.
|
||||||
|
(json-schema/transform (m/deref model) (merge opts options))
|
||||||
|
(json-schema/transform model (merge opts options)))
|
||||||
(throw
|
(throw
|
||||||
(ex-info
|
(ex-info
|
||||||
(str "Can't produce Malli apidocs for " specification)
|
(str "Can't produce Malli apidocs for " specification)
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
(-> path (trie/normalize opts) (str/replace #"\{\*" "{")))
|
(-> path (trie/normalize opts) (str/replace #"\{\*" "{")))
|
||||||
|
|
||||||
(defn -get-apidocs-openapi
|
(defn -get-apidocs-openapi
|
||||||
[coercion {:keys [request muuntaja parameters responses openapi/request-content-types openapi/response-content-types]}]
|
[coercion {:keys [request muuntaja parameters responses openapi/request-content-types openapi/response-content-types]} definitions]
|
||||||
(let [{:keys [body multipart]} parameters
|
(let [{:keys [body multipart]} parameters
|
||||||
parameters (dissoc parameters :request :body :multipart)
|
parameters (dissoc parameters :request :body :multipart)
|
||||||
->content (fn [data schema]
|
->content (fn [data schema]
|
||||||
|
|
@ -85,7 +85,13 @@
|
||||||
{:schema schema}
|
{:schema schema}
|
||||||
(select-keys data [:description :examples])
|
(select-keys data [:description :examples])
|
||||||
(:openapi data)))
|
(:openapi data)))
|
||||||
->schema-object #(coercion/-get-model-apidocs coercion :openapi %1 %2)
|
->schema-object (fn [model opts]
|
||||||
|
(let [result (coercion/-get-model-apidocs
|
||||||
|
coercion :openapi model
|
||||||
|
(assoc opts :malli.json-schema/definitions-path "#/components/schemas/"))]
|
||||||
|
(when-let [d (:definitions result)]
|
||||||
|
(vswap! definitions merge d))
|
||||||
|
(dissoc result :definitions)))
|
||||||
request-content-types (or request-content-types
|
request-content-types (or request-content-types
|
||||||
(when muuntaja (m/decodes muuntaja))
|
(when muuntaja (m/decodes muuntaja))
|
||||||
["application/json"])
|
["application/json"])
|
||||||
|
|
@ -189,6 +195,7 @@
|
||||||
:x-id ids}))
|
:x-id ids}))
|
||||||
accept-route (fn [route]
|
accept-route (fn [route]
|
||||||
(-> route second :openapi :id (or ::default) (trie/into-set) (set/intersection ids) seq))
|
(-> route second :openapi :id (or ::default) (trie/into-set) (set/intersection ids) seq))
|
||||||
|
definitions (volatile! {})
|
||||||
transform-endpoint (fn [[method {{:keys [coercion no-doc openapi] :as data} :data
|
transform-endpoint (fn [[method {{:keys [coercion no-doc openapi] :as data} :data
|
||||||
middleware :middleware
|
middleware :middleware
|
||||||
interceptors :interceptors}]]
|
interceptors :interceptors}]]
|
||||||
|
|
@ -198,7 +205,7 @@
|
||||||
(apply meta-merge (keep (comp :openapi :data) middleware))
|
(apply meta-merge (keep (comp :openapi :data) middleware))
|
||||||
(apply meta-merge (keep (comp :openapi :data) interceptors))
|
(apply meta-merge (keep (comp :openapi :data) interceptors))
|
||||||
(if coercion
|
(if coercion
|
||||||
(-get-apidocs-openapi coercion data))
|
(-get-apidocs-openapi coercion data definitions))
|
||||||
(select-keys data [:tags :summary :description])
|
(select-keys data [:tags :summary :description])
|
||||||
(strip-top-level-keys openapi))]))
|
(strip-top-level-keys openapi))]))
|
||||||
transform-path (fn [[p _ c]]
|
transform-path (fn [[p _ c]]
|
||||||
|
|
@ -207,7 +214,8 @@
|
||||||
map-in-order #(->> % (apply concat) (apply array-map))
|
map-in-order #(->> % (apply concat) (apply array-map))
|
||||||
paths (->> router (r/compiled-routes) (filter accept-route) (map transform-path) map-in-order)]
|
paths (->> router (r/compiled-routes) (filter accept-route) (map transform-path) map-in-order)]
|
||||||
{:status 200
|
{:status 200
|
||||||
:body (meta-merge openapi {:paths paths})}))
|
:body (cond-> (meta-merge openapi {:paths paths})
|
||||||
|
(seq @definitions) (assoc-in [:components :schemas] @definitions))}))
|
||||||
([req res raise]
|
([req res raise]
|
||||||
(try
|
(try
|
||||||
(res (create-openapi req))
|
(res (create-openapi req))
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
(:require [clojure.java.shell :as shell]
|
(:require [clojure.java.shell :as shell]
|
||||||
[clojure.test :refer [deftest is testing]]
|
[clojure.test :refer [deftest is testing]]
|
||||||
[jsonista.core :as j]
|
[jsonista.core :as j]
|
||||||
|
[malli.core :as mc]
|
||||||
[matcher-combinators.test :refer [match?]]
|
[matcher-combinators.test :refer [match?]]
|
||||||
[matcher-combinators.matchers :as matchers]
|
[matcher-combinators.matchers :as matchers]
|
||||||
[muuntaja.core :as m]
|
[muuntaja.core :as m]
|
||||||
|
|
@ -844,20 +845,25 @@
|
||||||
:requestBody
|
:requestBody
|
||||||
{:content
|
{:content
|
||||||
{"application/json"
|
{"application/json"
|
||||||
{:schema {:$ref "#/definitions/friend"
|
{:schema {:$ref "#/components/schemas/friend"}}}}}}}
|
||||||
:definitions {"friend" {:properties {:age {:type "integer"}
|
:components {:schemas {"friend" {:properties {:age {:type "integer"}
|
||||||
:pet {:$ref "#/definitions/pet"}}
|
:pet {:$ref "#/components/schemas/pet"}}
|
||||||
:required [:age :pet]
|
:required [:age :pet]
|
||||||
:type "object"}
|
:type "object"}
|
||||||
"pet" {:properties {:friends {:items {:$ref "#/definitions/friend"}
|
"pet" {:properties {:friends {:items {:$ref "#/components/schemas/friend"}
|
||||||
:type "array"}
|
:type "array"}
|
||||||
:name {:type "string"}}
|
:name {:type "string"}}
|
||||||
:required [:name :friends]
|
:required [:name :friends]
|
||||||
:type "object"}}}}}}}}}}
|
:type "object"}}}}
|
||||||
spec))
|
spec))
|
||||||
(testing "spec is valid"
|
(testing "spec is valid"
|
||||||
(is (nil? (validate spec))))))
|
(is (nil? (validate spec))))))
|
||||||
|
|
||||||
|
(def Y :int)
|
||||||
|
(def Plus [:map
|
||||||
|
[:x :int]
|
||||||
|
[:y #'Y]])
|
||||||
|
|
||||||
(deftest openapi-malli-tests
|
(deftest openapi-malli-tests
|
||||||
(let [app (ring/ring-handler
|
(let [app (ring/ring-handler
|
||||||
(ring/router
|
(ring/router
|
||||||
|
|
@ -901,9 +907,96 @@
|
||||||
:additionalProperties false},
|
:additionalProperties false},
|
||||||
:examples {"2" {:total 2}, "3" {:total 3}},
|
:examples {"2" {:total 2}, "3" {:total 3}},
|
||||||
:example {:total 4}}}}},
|
:example {:total 4}}}}},
|
||||||
:summary "plus with body"}}})
|
:summary "plus with body"}}}
|
||||||
(-> {:request-method :get
|
(-> {:request-method :get
|
||||||
:uri "/openapi.json"}
|
:uri "/openapi.json"}
|
||||||
(app)
|
(app)
|
||||||
:body
|
:body
|
||||||
:paths))))
|
:paths))))
|
||||||
|
(testing "ref schemas"
|
||||||
|
(let [registry (merge (mc/base-schemas)
|
||||||
|
(mc/type-schemas)
|
||||||
|
{::plus [:map [:x :int] [:y ::y]]
|
||||||
|
::y :int})
|
||||||
|
app (ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
[["/openapi.json"
|
||||||
|
{:get {:no-doc true
|
||||||
|
:handler (openapi/create-openapi-handler)}}]
|
||||||
|
["/post"
|
||||||
|
{:post {:coercion malli/coercion
|
||||||
|
:parameters {:body (mc/schema ::plus {:registry registry})}
|
||||||
|
:handler identity}}]
|
||||||
|
["/get"
|
||||||
|
{:get {:coercion malli/coercion
|
||||||
|
:parameters {:query (mc/schema ::plus {:registry registry})}
|
||||||
|
:handler identity}}]]))
|
||||||
|
spec (:body (app {:request-method :get :uri "/openapi.json"}))]
|
||||||
|
(is (= {:openapi "3.1.0"
|
||||||
|
:x-id #{:reitit.openapi/default}
|
||||||
|
:paths {"/get" {:get {:parameters [{:in "query"
|
||||||
|
:name :x
|
||||||
|
:required true
|
||||||
|
:schema {:type "integer"}}
|
||||||
|
{:in "query"
|
||||||
|
:name :y
|
||||||
|
:required true
|
||||||
|
:schema {:$ref "#/components/schemas/reitit.openapi-test~1y"}}]}}
|
||||||
|
"/post" {:post
|
||||||
|
{:requestBody
|
||||||
|
{:content
|
||||||
|
{"application/json"
|
||||||
|
{:schema
|
||||||
|
{:$ref "#/components/schemas/reitit.openapi-test~1plus"}}}}}}}
|
||||||
|
:components {:schemas
|
||||||
|
{"reitit.openapi-test/y" {:type "integer"}
|
||||||
|
"reitit.openapi-test/plus" {:type "object"
|
||||||
|
:properties {:x {:type "integer"}
|
||||||
|
:y {:$ref "#/components/schemas/reitit.openapi-test~1y"}}
|
||||||
|
:required [:x :y]}}}}
|
||||||
|
spec))))
|
||||||
|
(testing "var schemas"
|
||||||
|
(let [app (ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
[["/openapi.json"
|
||||||
|
{:get {:no-doc true
|
||||||
|
:handler (openapi/create-openapi-handler)}}]
|
||||||
|
["/post"
|
||||||
|
{:post {:coercion malli/coercion
|
||||||
|
:parameters {:body #'Plus}
|
||||||
|
:handler identity}}]
|
||||||
|
["/get"
|
||||||
|
{:get {:coercion malli/coercion
|
||||||
|
:parameters {:query #'Plus}
|
||||||
|
:handler identity}}]]))
|
||||||
|
spec (:body (app {:request-method :get :uri "/openapi.json"}))]
|
||||||
|
(is (= {:openapi "3.1.0"
|
||||||
|
:x-id #{:reitit.openapi/default}
|
||||||
|
:paths
|
||||||
|
{"/post"
|
||||||
|
{:post
|
||||||
|
{:requestBody
|
||||||
|
{:content
|
||||||
|
{"application/json"
|
||||||
|
{:schema
|
||||||
|
{:$ref "#/components/schemas/reitit.openapi-test~1Plus"}}}}}}
|
||||||
|
"/get"
|
||||||
|
{:get
|
||||||
|
{:parameters
|
||||||
|
[{:in "query" :name :x
|
||||||
|
:required true
|
||||||
|
:schema {:type "integer"}}
|
||||||
|
{:in "query"
|
||||||
|
:name :y
|
||||||
|
:required true
|
||||||
|
:schema {:$ref "#/components/schemas/reitit.openapi-test~1Y"}}]}}}
|
||||||
|
:components
|
||||||
|
{:schemas
|
||||||
|
{"reitit.openapi-test/Plus"
|
||||||
|
{:type "object"
|
||||||
|
:properties
|
||||||
|
{:x {:type "integer"}
|
||||||
|
:y {:$ref "#/components/schemas/reitit.openapi-test~1Y"}}
|
||||||
|
:required [:x :y]}
|
||||||
|
"reitit.openapi-test/Y" {:type "integer"}}}}
|
||||||
|
spec)))))
|
||||||
|
|
|
||||||
|
|
@ -507,3 +507,56 @@
|
||||||
:type "string"}]
|
:type "string"}]
|
||||||
(normalize
|
(normalize
|
||||||
(get-in spec [:paths "/upload" :post :parameters]))))))))
|
(get-in spec [:paths "/upload" :post :parameters]))))))))
|
||||||
|
|
||||||
|
(def X :int)
|
||||||
|
(def Y :int)
|
||||||
|
(def Plus [:map
|
||||||
|
[:x #'X]
|
||||||
|
[:y #'Y]])
|
||||||
|
|
||||||
|
(deftest malli-var-test
|
||||||
|
(let [app (ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
[["/post"
|
||||||
|
{:post {:coercion malli/coercion
|
||||||
|
:parameters {:body #'Plus}
|
||||||
|
:handler identity}}]
|
||||||
|
["/get"
|
||||||
|
{:get {:coercion malli/coercion
|
||||||
|
:parameters {:query
|
||||||
|
#'Plus}
|
||||||
|
:handler identity}}]
|
||||||
|
["/swagger.json"
|
||||||
|
{:get {:no-doc true
|
||||||
|
:handler (swagger/create-swagger-handler)}}]]))
|
||||||
|
spec (:body (app {:request-method :get, :uri "/swagger.json"}))]
|
||||||
|
(is (= {:definitions {"reitit.swagger-test/Plus" {:properties {:x {:$ref "#/definitions/reitit.swagger-test~1X"},
|
||||||
|
:y {:$ref "#/definitions/reitit.swagger-test~1Y"}},
|
||||||
|
:required [:x :y],
|
||||||
|
:type "object"},
|
||||||
|
"reitit.swagger-test/X" {:format "int64",
|
||||||
|
:type "integer"},
|
||||||
|
"reitit.swagger-test/Y" {:format "int64",
|
||||||
|
:type "integer"}},
|
||||||
|
:paths {"/post" {:post {:parameters [{:description "",
|
||||||
|
:in "body",
|
||||||
|
:name "body",
|
||||||
|
:required true,
|
||||||
|
:schema {:$ref "#/definitions/reitit.swagger-test~1Plus"}}],
|
||||||
|
:responses {:default {:description ""}}}}
|
||||||
|
"/get" {:get {:responses {:default {:description ""}}
|
||||||
|
:parameters [{:in "query"
|
||||||
|
:name :x
|
||||||
|
:description ""
|
||||||
|
:type "integer"
|
||||||
|
:required true
|
||||||
|
:format "int64"}
|
||||||
|
{:in "query"
|
||||||
|
:name :y
|
||||||
|
:description ""
|
||||||
|
:type "integer"
|
||||||
|
:required true
|
||||||
|
:format "int64"}]}}}
|
||||||
|
:swagger "2.0",
|
||||||
|
:x-id #{:reitit.swagger/default}}
|
||||||
|
spec))))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue