diff --git a/doc/ring/openapi.md b/doc/ring/openapi.md index 01432007d..f01ff4d89 100644 --- a/doc/ring/openapi.md +++ b/doc/ring/openapi.md @@ -207,11 +207,13 @@ useful when you have multiple endpoints that use the same schema. It can also make OpenAPI-based code nicer for consumers of your API. These schemas are also rendered in their own section in Swagger UI. -Reusable schema objects are generated for Malli `:ref`s and vars. The -[openapi example](../../examples/openapi) showcases this. +Reusable schema objects are generated for +- Malli `:ref`s and vars and +- Plumatic Schema named schemas (`defschema` and `named`). +The [openapi example](../../examples/openapi) showcases this. Currently (as of 0.7.2), reusable schema objects are **not** generated -for Plumatic Schema or Spec. +for Spec. ## Other caveats diff --git a/examples/openapi/src/example/server.clj b/examples/openapi/src/example/server.clj index 65f29c663..a388b9046 100644 --- a/examples/openapi/src/example/server.clj +++ b/examples/openapi/src/example/server.clj @@ -2,6 +2,7 @@ (:require [reitit.ring :as ring] [reitit.ring.spec] [reitit.coercion.malli] + [reitit.coercion.schema] [reitit.openapi :as openapi] [reitit.ring.malli] [reitit.swagger-ui :as swagger-ui] @@ -12,7 +13,8 @@ [reitit.ring.middleware.multipart :as multipart] [reitit.ring.middleware.parameters :as parameters] [ring.adapter.jetty :as jetty] - [muuntaja.core :as m])) + [muuntaja.core :as m] + [schema.core :as s])) (def Transaction [:map @@ -31,7 +33,19 @@ [:balance :double] [:transactions [:vector #'Transaction]]]) +(s/defschema TransactionSchema + {:amount s/Num + :from s/Str}) +(s/defschema AccountIdSchema + {:bank s/Str + :id s/Str}) + +(s/defschema AccountSchema + {:bank s/Str + :id s/Str + :balance s/Num + :transactions [TransactionSchema]}) (def app (ring/ring-handler @@ -132,7 +146,7 @@ :email "heidi@alps.ch"}]})}}] ["/account" - {:get {:summary "Fetch an account | Recursive schemas using malli registry, link to external docs" + {:get {:summary "Fetch an account | Named schemas using malli registry, link to external docs" :parameters {:query #'AccountId} :responses {200 {:content {:default {:schema #'Account}}}} :openapi {:externalDocs {:description "The reitit repository" @@ -186,7 +200,22 @@ {:status 200 :body {:secret "I am a marmot"}} {:status 401 - :body {:error "unauthorized"}}))}}]]] + :body {:error "unauthorized"}}))}}]] + + ["/plumatic-schema/account" + {:get {:summary "Fetch an account | Named schemas using Plumatic Schema" + :coercion reitit.coercion.schema/coercion + :parameters {:query AccountIdSchema} + :responses {200 {:content {:default {:schema AccountSchema}}}} + :handler (fn [_request] + {:status 200 + :body {:bank "MiniBank" + :id "0001" + :balance 13.5 + :transactions [{:from "0002" + :amount 20.0} + {:from "0003" + :amount -6.5}]}})}}]] {;;:reitit.middleware/transform dev/print-request-diffs ;; pretty diffs :validate reitit.ring.spec/validate diff --git a/modules/reitit-schema/src/reitit/coercion/schema.cljc b/modules/reitit-schema/src/reitit/coercion/schema.cljc index d3b37d9a8..64776a473 100644 --- a/modules/reitit-schema/src/reitit/coercion/schema.cljc +++ b/modules/reitit-schema/src/reitit/coercion/schema.cljc @@ -49,7 +49,11 @@ (-get-options [_] opts) (-get-model-apidocs [_ specification model options] (case specification - :openapi (openapi/transform model (merge opts options)) + :openapi (if (= :parameter (:type options)) + ;; For :parameters we need to output an object schema with actual :properties, not a $ref + ;; The caller will iterate through the properties and add them individually to the openapi doc. + (openapi/transform-inline model (merge opts options)) + (openapi/transform model (merge opts options))) (throw (ex-info (str "Can't produce Schema apidocs for " specification) diff --git a/package-lock.json b/package-lock.json index 4a9f66000..e92048439 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "shadow-cljs": "^3.2.1" }, "devDependencies": { - "@seriousme/openapi-schema-validator": "^2.7.0", + "@seriousme/openapi-schema-validator": "^2.9.1", "karma": "^6.4.4", "karma-chrome-launcher": "^3.2.0", "karma-cli": "^2.0.0", @@ -26,15 +26,15 @@ } }, "node_modules/@seriousme/openapi-schema-validator": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@seriousme/openapi-schema-validator/-/openapi-schema-validator-2.7.0.tgz", - "integrity": "sha512-7GrE9T3UWTWpIyaUq+r+UlsaMJhHFhzGtXkjBMZDIo5Pz+iNDT3oBt0N/EdGX7HDRt5TDmdIJGR8co3O/qOvWw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@seriousme/openapi-schema-validator/-/openapi-schema-validator-2.9.1.tgz", + "integrity": "sha512-EgGqVIP8xiKHmNTHWbrxec+RhD/WPUay7D/erEc7vWoZKxTT9f2aCEu1egKVpxcEbT1z0wdAbWFR9o2Is5FJEw==", "dev": true, "dependencies": { - "ajv": "^8.17.1", + "ajv": "^8.20.0", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", - "js-yaml": "^4.1.0" + "yaml": "^2.9.0" }, "bin": { "bundle-api": "bin/bundle-api-cli.js", @@ -79,9 +79,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.3", @@ -162,12 +162,6 @@ "node": ">= 8" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1130,18 +1124,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -2168,6 +2150,21 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/package.json b/package.json index 3c1a5e439..d5493b798 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "reitit", "private": true, "devDependencies": { - "@seriousme/openapi-schema-validator": "^2.7.0", + "@seriousme/openapi-schema-validator": "^2.9.1", "karma": "^6.4.4", "karma-chrome-launcher": "^3.2.0", "karma-cli": "^2.0.0", diff --git a/project.clj b/project.clj index 19f57ab0f..480c3501c 100644 --- a/project.clj +++ b/project.clj @@ -36,7 +36,7 @@ [metosin/reitit-pedestal "0.10.1"] [metosin/ring-swagger-ui "5.31.0"] [metosin/spec-tools "0.10.8"] - [metosin/schema-tools "0.13.1"] + [metosin/schema-tools "0.14.0"] [metosin/muuntaja "0.6.11"] [metosin/jsonista "0.3.14"] [metosin/sieppari "0.0.0-alpha13"] @@ -95,7 +95,7 @@ [org.clojure/clojurescript "1.12.134"] ;; modules dependencies - [metosin/schema-tools "0.13.1"] + [metosin/schema-tools "0.14.0"] [metosin/spec-tools "0.10.8"] [metosin/muuntaja "0.6.11"] [metosin/sieppari "0.0.0-alpha13"] diff --git a/test/cljc/reitit/openapi_test.clj b/test/cljc/reitit/openapi_test.clj index 839b25b8f..5242bce1a 100644 --- a/test/cljc/reitit/openapi_test.clj +++ b/test/cljc/reitit/openapi_test.clj @@ -1143,3 +1143,84 @@ :anyOf [{:required ["address" "zip"]} {:required ["city" "street"]}]} (get-in spec [:paths "/spec" :post :requestBody :content "application/json" :schema])))))) + +(s/defschema Y2 s/Int) +(s/defschema Plus2 {:x s/Int + :y Y2}) + +(deftest openapi-schema-tests + (testing "named schemas" + (let [app (ring/ring-handler + (ring/router + [["/openapi.json" + {:get {:no-doc true + :openapi {:info {:title "" :version "0.0.1"}} + :handler (openapi/create-openapi-handler)}}] + ["/post" + {:post {:parameters {:body Plus2} + :handler identity}}] + ["/get" + {:get {:parameters {:query Plus2} + :handler identity}}]] + {:data {:coercion schema/coercion}})) + spec (:body (app {:request-method :get :uri "/openapi.json"}))] + (is (= {:openapi "3.1.0" + :x-id #{:reitit.openapi/default} + :info {:title "" :version "0.0.1"} + :paths + {"/post" + {:post + {:requestBody + {:content + {"application/json" + {:schema + {:$ref "#/components/schemas/reitit.openapi-test.Plus2"}}}}}} + "/get" + {:get + {:parameters + [{:in "query" :name "x" + :required true + :schema {:type "integer" :format "int32"}} + {:in "query" + :name "y" + :required true + :schema {:$ref "#/components/schemas/reitit.openapi-test.Y2"}}]}}} + :components + {:schemas + {"reitit.openapi-test.Plus2" + {:type "object" + :title "reitit.openapi-test/Plus2" + :additionalProperties false + :properties + {"x" {:type "integer" :format "int32"} + "y" {:$ref "#/components/schemas/reitit.openapi-test.Y2"}} + :required ["x" "y"]} + "reitit.openapi-test.Y2" {:type "integer" :format "int32"}}}} + spec)) + (is (nil? (validate spec)))) + (testing "under additionalParameters" + (let [app (ring/ring-handler + (ring/router + [["/openapi.json" + {:get {:no-doc true + :openapi {:info {:title "" :version "0.0.1"}} + :handler (openapi/create-openapi-handler)}}] + ["/post" + {:post {:parameters {:body {s/Keyword Y2}} + :handler identity}}]] + {:data {:coercion schema/coercion}})) + spec (:body (app {:request-method :get :uri "/openapi.json"}))] + (is (= {:openapi "3.1.0" + :x-id #{:reitit.openapi/default} + :info {:title "" :version "0.0.1"} + :paths + {"/post" + {:post + {:requestBody + {:content + {"application/json" + {:schema {:type "object" + :additionalProperties {:$ref "#/components/schemas/reitit.openapi-test.Y2"}}}}}}}} + :components {:schemas {"reitit.openapi-test.Y2" {:type "integer" :format "int32"}}}} + spec)) + (is (nil? (validate spec)))))))