From c2372473d01215d20804e4acd5982aea511543df Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Fri, 19 Apr 2024 10:35:28 +0300 Subject: [PATCH 1/7] test: test for malli vars + swagger --- test/cljc/reitit/swagger_test.clj | 53 +++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/test/cljc/reitit/swagger_test.clj b/test/cljc/reitit/swagger_test.clj index 0ad096c7..fc8130e2 100644 --- a/test/cljc/reitit/swagger_test.clj +++ b/test/cljc/reitit/swagger_test.clj @@ -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)))) From 288b701d4eca87c513e085aada514c314da7e471 Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Fri, 19 Apr 2024 10:57:51 +0300 Subject: [PATCH 2/7] feat: openapi #/components/schemas collect definitions when traversing the models, and put them in the right place for openapi depends on :malli.json-schema/definitions-path support --- modules/reitit-openapi/src/reitit/openapi.clj | 16 +++++++++++---- test/cljc/reitit/openapi_test.clj | 20 +++++++++---------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/modules/reitit-openapi/src/reitit/openapi.clj b/modules/reitit-openapi/src/reitit/openapi.clj index 8e49de77..497b811e 100644 --- a/modules/reitit-openapi/src/reitit/openapi.clj +++ b/modules/reitit-openapi/src/reitit/openapi.clj @@ -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)) diff --git a/test/cljc/reitit/openapi_test.clj b/test/cljc/reitit/openapi_test.clj index a3e8347d..805a4183 100644 --- a/test/cljc/reitit/openapi_test.clj +++ b/test/cljc/reitit/openapi_test.clj @@ -844,16 +844,16 @@ :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)))))) From 337d94823aec8b6ab112d7f7a2440bd928d1beee Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Fri, 19 Apr 2024 11:08:04 +0300 Subject: [PATCH 3/7] feat: support ref schemas in openapi parameters e.g. {:parameters {:query #'MyVar}} --- modules/reitit-malli/src/reitit/coercion/malli.cljc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/reitit-malli/src/reitit/coercion/malli.cljc b/modules/reitit-malli/src/reitit/coercion/malli.cljc index 583a8db3..5057eb9e 100644 --- a/modules/reitit-malli/src/reitit/coercion/malli.cljc +++ b/modules/reitit-malli/src/reitit/coercion/malli.cljc @@ -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) From ce52b26329abdf2c1487c1c58cef425f3662f8c4 Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Fri, 19 Apr 2024 11:46:02 +0300 Subject: [PATCH 4/7] test: actually assert something in openapi-malli-tests (is (= x) y) strikes again --- test/cljc/reitit/openapi_test.clj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/cljc/reitit/openapi_test.clj b/test/cljc/reitit/openapi_test.clj index 805a4183..1c542842 100644 --- a/test/cljc/reitit/openapi_test.clj +++ b/test/cljc/reitit/openapi_test.clj @@ -901,9 +901,9 @@ :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))))) From 57fc00a45e31fdc4dc14bd19210ae5aa886a4824 Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Fri, 19 Apr 2024 11:56:49 +0300 Subject: [PATCH 5/7] test: tests for openapi + malli refs/vars --- test/cljc/reitit/openapi_test.clj | 95 ++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/test/cljc/reitit/openapi_test.clj b/test/cljc/reitit/openapi_test.clj index 1c542842..8d77dd4b 100644 --- a/test/cljc/reitit/openapi_test.clj +++ b/test/cljc/reitit/openapi_test.clj @@ -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] @@ -858,6 +859,11 @@ (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 @@ -906,4 +912,91 @@ :uri "/openapi.json"} (app) :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))))) From 3cb387747b499c1a62f1b3729c17be8a93854ce4 Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Fri, 19 Apr 2024 11:58:13 +0300 Subject: [PATCH 6/7] doc: use malli vars in examples/openapi --- examples/openapi/src/example/server.clj | 39 +++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/examples/openapi/src/example/server.clj b/examples/openapi/src/example/server.clj index 9070a529..77639e41 100644 --- a/examples/openapi/src/example/server.clj +++ b/examples/openapi/src/example/server.clj @@ -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"} From a06b2c98a7263432fd6cff565bdb7c43163f77cd Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Mon, 22 Apr 2024 08:17:09 +0300 Subject: [PATCH 7/7] doc: update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7299479a..62bd4956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ We use [Break Versioning][breakver]. The version numbers follow a `.