Skip to content

Commit

Permalink
Merge pull request #673 from metosin/malli-vars
Browse files Browse the repository at this point in the history
Generate correct OpenAPI $ref schemas for malli var and ref schemas
  • Loading branch information
opqdonut authored Apr 22, 2024
2 parents 066c54b + a06b2c9 commit c8c8c0e
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 23 deletions.
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))))

0 comments on commit c8c8c0e

Please sign in to comment.