mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 00:11: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)
|
||||
* **BREAKING** OpenAPI support is now clj only
|
||||
* 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:
|
||||
|
||||
```clojure
|
||||
|
|
|
|||
|
|
@ -12,8 +12,28 @@
|
|||
[reitit.ring.middleware.multipart :as multipart]
|
||||
[reitit.ring.middleware.parameters :as parameters]
|
||||
[ring.adapter.jetty :as jetty]
|
||||
[malli.core :as malli]
|
||||
[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
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
|
|
@ -89,8 +109,23 @@
|
|||
[:email {:json-schema/example "heidi@alps.ch"}
|
||||
string?]]]}}}}
|
||||
:handler (fn [_request]
|
||||
[{:name "Heidi"
|
||||
:email "heidi@alps.ch"}])}}]
|
||||
{:status 200
|
||||
: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"
|
||||
{:tags #{"secure"}
|
||||
|
|
|
|||
|
|
@ -122,7 +122,12 @@
|
|||
(-get-options [_] opts)
|
||||
(-get-model-apidocs [this specification model options]
|
||||
(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
|
||||
(ex-info
|
||||
(str "Can't produce Malli apidocs for " specification)
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@
|
|||
(-> path (trie/normalize opts) (str/replace #"\{\*" "{")))
|
||||
|
||||
(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
|
||||
parameters (dissoc parameters :request :body :multipart)
|
||||
->content (fn [data schema]
|
||||
|
|
@ -85,7 +85,13 @@
|
|||
{:schema schema}
|
||||
(select-keys data [:description :examples])
|
||||
(: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
|
||||
(when muuntaja (m/decodes muuntaja))
|
||||
["application/json"])
|
||||
|
|
@ -189,6 +195,7 @@
|
|||
:x-id ids}))
|
||||
accept-route (fn [route]
|
||||
(-> 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
|
||||
middleware :middleware
|
||||
interceptors :interceptors}]]
|
||||
|
|
@ -198,7 +205,7 @@
|
|||
(apply meta-merge (keep (comp :openapi :data) middleware))
|
||||
(apply meta-merge (keep (comp :openapi :data) interceptors))
|
||||
(if coercion
|
||||
(-get-apidocs-openapi coercion data))
|
||||
(-get-apidocs-openapi coercion data definitions))
|
||||
(select-keys data [:tags :summary :description])
|
||||
(strip-top-level-keys openapi))]))
|
||||
transform-path (fn [[p _ c]]
|
||||
|
|
@ -207,7 +214,8 @@
|
|||
map-in-order #(->> % (apply concat) (apply array-map))
|
||||
paths (->> router (r/compiled-routes) (filter accept-route) (map transform-path) map-in-order)]
|
||||
{: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]
|
||||
(try
|
||||
(res (create-openapi req))
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
(:require [clojure.java.shell :as shell]
|
||||
[clojure.test :refer [deftest is testing]]
|
||||
[jsonista.core :as j]
|
||||
[malli.core :as mc]
|
||||
[matcher-combinators.test :refer [match?]]
|
||||
[matcher-combinators.matchers :as matchers]
|
||||
[muuntaja.core :as m]
|
||||
|
|
@ -844,20 +845,25 @@
|
|||
:requestBody
|
||||
{:content
|
||||
{"application/json"
|
||||
{:schema {:$ref "#/definitions/friend"
|
||||
:definitions {"friend" {:properties {:age {:type "integer"}
|
||||
:pet {:$ref "#/definitions/pet"}}
|
||||
:required [:age :pet]
|
||||
:type "object"}
|
||||
"pet" {:properties {:friends {:items {:$ref "#/definitions/friend"}
|
||||
:type "array"}
|
||||
:name {:type "string"}}
|
||||
:required [:name :friends]
|
||||
:type "object"}}}}}}}}}}
|
||||
{:schema {:$ref "#/components/schemas/friend"}}}}}}}
|
||||
:components {:schemas {"friend" {:properties {:age {:type "integer"}
|
||||
:pet {:$ref "#/components/schemas/pet"}}
|
||||
:required [:age :pet]
|
||||
:type "object"}
|
||||
"pet" {:properties {:friends {:items {:$ref "#/components/schemas/friend"}
|
||||
:type "array"}
|
||||
:name {:type "string"}}
|
||||
:required [:name :friends]
|
||||
:type "object"}}}}
|
||||
spec))
|
||||
(testing "spec is valid"
|
||||
(is (nil? (validate spec))))))
|
||||
|
||||
(def Y :int)
|
||||
(def Plus [:map
|
||||
[:x :int]
|
||||
[:y #'Y]])
|
||||
|
||||
(deftest openapi-malli-tests
|
||||
(let [app (ring/ring-handler
|
||||
(ring/router
|
||||
|
|
@ -901,9 +907,96 @@
|
|||
:additionalProperties false},
|
||||
:examples {"2" {:total 2}, "3" {:total 3}},
|
||||
:example {:total 4}}}}},
|
||||
:summary "plus with body"}}})
|
||||
(-> {:request-method :get
|
||||
:uri "/openapi.json"}
|
||||
(app)
|
||||
:body
|
||||
:paths))))
|
||||
:summary "plus with body"}}}
|
||||
(-> {:request-method :get
|
||||
:uri "/openapi.json"}
|
||||
(app)
|
||||
:body
|
||||
: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"}]
|
||||
(normalize
|
||||
(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