Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate correct OpenAPI $ref schemas for malli var and ref schemas #673

Merged
merged 7 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 37 additions & 2 deletions examples/openapi/src/example/server.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -89,8 +109,23 @@
[:email {:json-schema/example "[email protected]"}
string?]]]}}}}
:handler (fn [_request]
[{:name "Heidi"
:email "[email protected]"}])}}]
{:status 200
:body [{:name "Heidi"
:email "[email protected]"}]})}}]

["/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"}
Expand Down
7 changes: 6 additions & 1 deletion modules/reitit-malli/src/reitit/coercion/malli.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 12 additions & 4 deletions modules/reitit-openapi/src/reitit/openapi.clj
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,21 @@
(-> 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]
(merge
{: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"])
Expand Down Expand Up @@ -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}]]
Expand All @@ -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]]
Expand All @@ -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))
Expand Down
125 changes: 109 additions & 16 deletions test/cljc/reitit/openapi_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)))))
53 changes: 53 additions & 0 deletions test/cljc/reitit/swagger_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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