diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc index 0fd5d2347..a8b0d9c63 100644 --- a/modules/reitit-core/src/reitit/coercion.cljc +++ b/modules/reitit-core/src/reitit/coercion.cljc @@ -146,6 +146,7 @@ :path :path :multipart :formData}] (case specification + :openapi (-get-apidocs coercion specification data) :swagger (->> (update data :parameters @@ -156,6 +157,7 @@ (into {})))) (-get-apidocs coercion specification))))) + ;; ;; integration ;; diff --git a/modules/reitit-malli/src/reitit/coercion/malli.cljc b/modules/reitit-malli/src/reitit/coercion/malli.cljc index a57d2641c..949da00f8 100644 --- a/modules/reitit-malli/src/reitit/coercion/malli.cljc +++ b/modules/reitit-malli/src/reitit/coercion/malli.cljc @@ -5,6 +5,7 @@ [malli.edn :as edn] [malli.error :as me] [malli.experimental.lite :as l] + [malli.json-schema :as json-schema] [malli.swagger :as swagger] [malli.transform :as mt] [malli.util :as mu] @@ -132,6 +133,76 @@ ;; malli options :options nil}) +(defn -get-apidocs-openapi + [coercion {:keys [parameters responses content-types] :or {content-types ["application/json"]}} options] + (let [{:keys [body request]} parameters + parameters (dissoc parameters :request :body) + ->schema-object (fn [schema opts] + (let [current-opts (merge options opts)] + (json-schema/transform (coercion/-compile-model coercion schema current-opts) + current-opts)))] + + (merge + (when (seq parameters) + {:parameters + (->> (for [[in schema] parameters + :let [{:keys [properties required] :as root} (->schema-object schema {:in in :type :parameter}) + required? (partial contains? (set required))] + [k schema] properties] + (merge {:in (name in) + :name k + :required (required? k) + :schema schema} + (select-keys root [:description]))) + (into []))}) + (when body + ;; body uses a single schema to describe every :requestBody + ;; the schema-object transformer should be able to transform into distinct content-types + {:requestBody {:content (into {} + (map (fn [content-type] + (let [schema (->schema-object body {:in :requestBody + :type :schema + :content-type content-type})] + [content-type {:schema schema}]))) + content-types)}}) + (when request + ;; request allow to different :requestBody per content-type + {:requestBody + {:content + (into {} + (map (fn [[content-type requestBody]] + (let [schema (->schema-object requestBody {:in :requestBody + :type :schema + :content-type content-type})] + [content-type {:schema schema}]))) + (:content request))}}) + (when responses + {:responses + (into {} + (map (fn [[status {:keys [body content] + :as response}]] + (let [content (merge + (when body + (into {} + (map (fn [content-type] + (let [schema (->schema-object body {:in :responses + :type :schema + :content-type content-type})] + [content-type {:schema schema}]))) + content-types)) + (when content + (into {} + (map (fn [[content-type schema]] + (let [schema (->schema-object schema {:in :responses + :type :schema + :content-type content-type})] + [content-type {:schema schema}]))) + content)))] + [status (merge (select-keys response [:description]) + (when content + {:content content}))]))) + responses)})))) + (defn create ([] (create nil)) @@ -145,7 +216,7 @@ (reify coercion/Coercion (-get-name [_] :malli) (-get-options [_] opts) - (-get-apidocs [_ specification {:keys [parameters responses]}] + (-get-apidocs [this specification {:keys [parameters responses] :as data}] (case specification :swagger (merge (if parameters @@ -167,6 +238,7 @@ (update :schema compile options) (update :schema swagger/transform {:type :schema})) $))]))})) + :openapi (-get-apidocs-openapi this data options) (throw (ex-info (str "Can't produce Schema apidocs for " specification) diff --git a/modules/reitit-openapi/project.clj b/modules/reitit-openapi/project.clj new file mode 100644 index 000000000..535b51ece --- /dev/null +++ b/modules/reitit-openapi/project.clj @@ -0,0 +1,12 @@ +(defproject metosin/reitit-openapi "0.5.18" + :description "Reitit: OpenAPI-support" + :url "https://github.com/metosin/reitit" + :license {:name "Eclipse Public License" + :url "http://www.eclipse.org/legal/epl-v10.html"} + :scm {:name "git" + :url "https://github.com/metosin/reitit" + :dir "../.."} + :plugins [[lein-parent "0.3.8"]] + :parent-project {:path "../../project.clj" + :inherit [:deploy-repositories :managed-dependencies]} + :dependencies [[metosin/reitit-core]]) diff --git a/modules/reitit-openapi/src/reitit/openapi.cljc b/modules/reitit-openapi/src/reitit/openapi.cljc new file mode 100644 index 000000000..6b1a465e1 --- /dev/null +++ b/modules/reitit-openapi/src/reitit/openapi.cljc @@ -0,0 +1,109 @@ +(ns reitit.openapi + (:require [clojure.set :as set] + [clojure.spec.alpha :as s] + [clojure.string :as str] + [meta-merge.core :refer [meta-merge]] + [reitit.coercion :as coercion] + [reitit.core :as r] + [reitit.trie :as trie])) + +(s/def ::id (s/or :keyword keyword? :set (s/coll-of keyword? :into #{}))) +(s/def ::no-doc boolean?) +(s/def ::tags (s/coll-of (s/or :keyword keyword? :string string?) :kind #{})) +(s/def ::summary string?) +(s/def ::description string?) + +(s/def ::openapi (s/keys :opt-un [::id])) +(s/def ::spec (s/keys :opt-un [::openapi ::no-doc ::tags ::summary ::description])) + +(def openapi-feature + "Feature for handling openapi-documentation for routes. + Works both with Middleware & Interceptors. Does not participate + in actual request processing, just provides specs for the new + documentation keys for the route data. Should be accompanied by a + [[openapi-spec-handler]] to expose the openapi spec. + + New route data keys contributing to openapi docs: + + | key | description | + | --------------|-------------| + | :openapi | map of any openapi-data. Must have `:id` (keyword or sequence of keywords) to identify the api + | :no-doc | optional boolean to exclude endpoint from api docs + | :summary | optional short string summary of an endpoint + | :description | optional long description of an endpoint. Supports http://spec.commonmark.org/ + + Also the coercion keys contribute to openapi spec: + + | key | description | + | --------------|-------------| + | :parameters | optional input parameters for a route, in a format defined by the coercion + | :responses | optional descriptions of responses, in a format defined by coercion + + Example: + + [\"/api\" + {:openapi {:id :my-api} + :middleware [reitit.openapi/openapi-feature]} + + [\"/openapi.json\" + {:get {:no-doc true + :openapi {:info {:title \"my-api\"}} + :handler reitit.openapi/openapi-spec-handler}}] + + [\"/plus\" + {:get {:openapi {:tags \"math\"} + :summary \"adds numbers together\" + :description \"takes `x` and `y` query-params and adds them together\" + :parameters {:query {:x int?, :y int?}} + :responses {200 {:body {:total pos-int?}}} + :handler (fn [{:keys [parameters]}] + {:status 200 + :body (+ (-> parameters :query :x) + (-> parameters :query :y)})}}]]" + {:name ::openapi + :spec ::spec}) + +(defn- openapi-path [path opts] + (-> path (trie/normalize opts) (str/replace #"\{\*" "{"))) + +(defn create-openapi-handler + "Create a ring handler to emit openapi spec. Collects all routes from router which have + an intersecting `[:openapi :id]` and which are not marked with `:no-doc` route data." + [] + (fn create-openapi + ([{::r/keys [router match] :keys [request-method]}] + (let [{:keys [id] :or {id ::default} :as openapi} (-> match :result request-method :data :openapi) + ids (trie/into-set id) + strip-top-level-keys #(dissoc % :id :info :host :basePath :definitions :securityDefinitions) + strip-endpoint-keys #(dissoc % :id :parameters :responses :summary :description) + openapi (->> (strip-endpoint-keys openapi) + (merge {:openapi "3.1.0" + :x-id ids})) + accept-route (fn [route] + (-> route second :openapi :id (or ::default) (trie/into-set) (set/intersection ids) seq)) + ;base-openapi-spec {:responses ^:displace {:default {:description ""}}} + transform-endpoint (fn [[method {{:keys [coercion no-doc openapi] :as data} :data + middleware :middleware + interceptors :interceptors}]] + (if (and data (not no-doc)) + [method + (meta-merge + #_base-openapi-spec + (apply meta-merge (keep (comp :openapi :data) middleware)) + (apply meta-merge (keep (comp :openapi :data) interceptors)) + (if coercion + (coercion/get-apidocs coercion :openapi data)) + (select-keys data [:tags :summary :description]) + (strip-top-level-keys openapi))])) + transform-path (fn [[p _ c]] + (if-let [endpoint (some->> c (keep transform-endpoint) (seq) (into {}))] + [(openapi-path p (r/options router)) endpoint])) + 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})})) + ([req res raise] + (try + (res (create-openapi req)) + (catch #?(:clj Exception :cljs :default) e + (raise e)))))) diff --git a/modules/reitit-schema/src/reitit/coercion/schema.cljc b/modules/reitit-schema/src/reitit/coercion/schema.cljc index 022f38728..b647b8460 100644 --- a/modules/reitit-schema/src/reitit/coercion/schema.cljc +++ b/modules/reitit-schema/src/reitit/coercion/schema.cljc @@ -4,6 +4,7 @@ [reitit.coercion :as coercion] [schema-tools.coerce :as stc] [schema-tools.core :as st] + [schema-tools.openapi.core :as openapi] [schema-tools.swagger.core :as swagger] [schema.coerce :as sc] [schema.core :as s] @@ -67,6 +68,33 @@ (if (:schema $) (update $ :schema #(coercion/-compile-model this % nil)) $))]))}))) + :openapi (merge + (when (seq (dissoc parameters :body :request)) + (openapi/openapi-spec {::openapi/parameters + (into + (empty parameters) + (for [[k v] (dissoc parameters :body :request)] + [k (coercion/-compile-model this v nil)]))})) + (when (:body parameters) + {:requestBody (openapi/openapi-spec + {::openapi/content {"application/json" (:body parameters)}})}) + (when (:request parameters) + {:requestBody (openapi/openapi-spec + {::openapi/content (:content (:request parameters))})}) + (when responses + {:responses + (into + (empty responses) + (for [[k response] responses] + [k (merge + (select-keys response [:description]) + (when (:body response) + (openapi/openapi-spec + {::openapi/content {"application/json" (coercion/-compile-model this (:body response) nil)}})) + (when (:content response) + (openapi/openapi-spec + {::openapi/content (:content response)})))]))})) + (throw (ex-info (str "Can't produce Schema apidocs for " specification) diff --git a/modules/reitit-spec/src/reitit/coercion/spec.cljc b/modules/reitit-spec/src/reitit/coercion/spec.cljc index 5f9809a7d..0184a2e05 100644 --- a/modules/reitit-spec/src/reitit/coercion/spec.cljc +++ b/modules/reitit-spec/src/reitit/coercion/spec.cljc @@ -4,6 +4,7 @@ [reitit.coercion :as coercion] [spec-tools.core :as st #?@(:cljs [:refer [Spec]])] [spec-tools.data-spec :as ds #?@(:cljs [:refer [Maybe]])] + [spec-tools.openapi.core :as openapi] [spec-tools.swagger.core :as swagger]) #?(:clj (:import (spec_tools.core Spec) @@ -105,6 +106,32 @@ (if (:schema $) (update $ :schema #(coercion/-compile-model this % nil)) $))]))}))) + :openapi (openapi/openapi-spec + (merge + (when (seq (dissoc parameters :body :request)) + {::openapi/parameters + (into (empty parameters) + (for [[k v] (dissoc parameters :body :request)] + [k (coercion/-compile-model this v nil)]))}) + (when (:body parameters) + {:requestBody (openapi/openapi-spec + {::openapi/content {"application/json" (coercion/-compile-model this (:body parameters) nil)}})}) + (when (:request parameters) + {:requestBody (openapi/openapi-spec + {::openapi/content (coercion/-compile-model this (:content (:request parameters)) nil)})}) + (when responses + {:responses + (into + (empty responses) + (for [[k response] responses] + [k (merge + (select-keys response [:description]) + (when (:body response) + (openapi/openapi-spec + {::openapi/content {"application/json" (coercion/-compile-model this (:body response) nil)}})) + (when (:content response) + (openapi/openapi-spec + {::openapi/content (coercion/-compile-model this (:content response) nil)})))]))}))) (throw (ex-info (str "Can't produce Spec apidocs for " specification) diff --git a/project.clj b/project.clj index 8afdf2608..893860b46 100644 --- a/project.clj +++ b/project.clj @@ -65,6 +65,7 @@ "modules/reitit-ring/src" "modules/reitit-http/src" "modules/reitit-middleware/src" + "modules/reitit-openapi/src" "modules/reitit-interceptors/src" "modules/reitit-malli/src" "modules/reitit-spec/src" @@ -86,7 +87,7 @@ [metosin/muuntaja "0.6.8"] [metosin/sieppari "0.0.0-alpha13"] [metosin/jsonista "0.3.5"] - [metosin/malli "0.8.2"] + [metosin/malli "0.8.9"] [lambdaisland/deep-diff "0.0-47"] [meta-merge "1.0.0"] [com.bhauman/spell-spec "0.1.2"] diff --git a/test/cljc/reitit/openapi_test.clj b/test/cljc/reitit/openapi_test.clj new file mode 100644 index 000000000..22d43453e --- /dev/null +++ b/test/cljc/reitit/openapi_test.clj @@ -0,0 +1,552 @@ +(ns reitit.openapi-test + (:require [clojure.test :refer [deftest is testing]] + [muuntaja.core :as m] + [reitit.coercion.malli :as malli] + [reitit.coercion.schema :as schema] + [reitit.coercion.spec :as spec] + [reitit.openapi :as openapi] + [reitit.ring :as ring] + [reitit.ring.coercion :as rrc] + [reitit.swagger-ui :as swagger-ui] + [schema.core :as s] + [spec-tools.data-spec :as ds])) + +(def app + (ring/ring-handler + (ring/router + ["/api" + {:openapi {:id ::math}} + + ["/openapi.json" + {:get {:no-doc true + :openapi {:info {:title "my-api"}} + :handler (openapi/create-openapi-handler)}}] + + #_["/spec" {:coercion spec/coercion} + ["/plus/:z" + {:patch {:summary "patch" + :handler (constantly {:status 200})} + :options {:summary "options" + :middleware [{:data {:openapi {:responses {200 {:description "200"}}}}}] + :handler (constantly {:status 200})} + :get {:summary "plus" + :parameters {:query {:x int?, :y int?} + :path {:z int?}} + :openapi {:responses {400 {:description "kosh" + :content {"application/json" {:schema {:type "string"}}}}}} + :responses {200 {:body {:total int?}} + 500 {:description "fail"}} + :handler (fn [{{{:keys [x y]} :query + {:keys [z]} :path} :parameters}] + {:status 200, :body {:total (+ x y z)}})} + :post {:summary "plus with body" + :parameters {:body (ds/maybe [int?]) + :path {:z int?}} + :openapi {:responses {400 {:content {"application/json" {:schema {:type "string"}}} + :description "kosh"}}} + :responses {200 {:body {:total int?}} + 500 {:description "fail"}} + :handler (fn [{{{:keys [z]} :path + xs :body} :parameters}] + {:status 200, :body {:total (+ (reduce + xs) z)}})}}]] + + ["/malli" {:coercion malli/coercion} + ["/plus/*z" + {:get {:summary "plus" + :parameters {:query [:map [:x int?] [:y int?]] + :path [:map [:z int?]]} + :openapi {:responses {400 {:description "kosh" + :content {"application/json" {:schema {:type "string"}}}}}} + :responses {200 {:body [:map [:total int?]]} + 500 {:description "fail"}} + :handler (fn [{{{:keys [x y]} :query + {:keys [z]} :path} :parameters}] + {:status 200, :body {:total (+ x y z)}})} + :post {:summary "plus with body" + :parameters {:body [:maybe [:vector int?]] + :path [:map [:z int?]]} + :openapi {:responses {400 {:description "kosh" + :content {"application/json" {:schema {:type "string"}}}}}} + :responses {200 {:body [:map [:total int?]]} + 500 {:description "fail"}} + :handler (fn [{{{:keys [z]} :path + xs :body} :parameters}] + {:status 200, :body {:total (+ (reduce + xs) z)}})}}]] + + ["/schema" {:coercion schema/coercion} + ["/plus/*z" + {:get {:summary "plus" + :parameters {:query {:x s/Int, :y s/Int} + :path {:z s/Int}} + :openapi {:responses {400 {:content {"application/json" {:schema {:type "string"}}} + :description "kosh"}}} + :responses {200 {:body {:total s/Int}} + 500 {:description "fail"}} + :handler (fn [{{{:keys [x y]} :query + {:keys [z]} :path} :parameters}] + {:status 200, :body {:total (+ x y z)}})} + :post {:summary "plus with body" + :parameters {:body (s/maybe [s/Int]) + :path {:z s/Int}} + :openapi {:responses {400 {:content {"application/json" {:schema {:type "string"}}} + :description "kosh"}}} + :responses {200 {:body {:total s/Int}} + 500 {:description "fail"}} + :handler (fn [{{{:keys [z]} :path + xs :body} :parameters}] + {:status 200, :body {:total (+ (reduce + xs) z)}})}}]]] + + {:data {:middleware [openapi/openapi-feature + rrc/coerce-exceptions-middleware + rrc/coerce-request-middleware + rrc/coerce-response-middleware]}}))) + +(require '[fipp.edn]) +(deftest openapi-test + (testing "endpoints work" + (testing "malli" + (is (= {:body {:total 6}, :status 200} + (app {:request-method :get + :uri "/api/malli/plus/3" + :query-params {:x "2", :y "1"}}))) + (is (= {:body {:total 7}, :status 200} + (app {:request-method :post + :uri "/api/malli/plus/3" + :body-params [1 3]}))))) + (testing "openapi-spec" + (let [spec (:body (app {:request-method :get + :uri "/api/openapi.json"})) + expected {:x-id #{::math} + :openapi "3.1.0" + :info {:title "my-api"} + :paths {#_#_"/api/spec/plus/{z}" {:patch {:summary "patch" + :responses {:default {:description ""}}} + :options {:summary "options" + :responses {200 {:description "200"}}} + :get {:parameters [{:in "query" + :name "x" + :description "" + :required true + :schema {:type "integer"}} + {:in "query" + :name "y" + :description "" + :required true + :schema {:type "integer"}} + {:in "path" + :name "z" + :description "" + :required true + :schema {:type "integer"}}] + :responses {200 {:content {"application/json" {:schema {:type "object" + :properties {"total" {:format "int64" + :type "integer"}} + :required ["total"]}}}} + 400 {:description "kosh" + :content {"application/json" {:schema {:type "string"}}}} + 500 {:description "fail"}} + :summary "plus"} + :post {:parameters [{:in "path" + :name "z" + :required true + :schema {:type "integer"}}] + :requestBody {:content {"application/json" {:schema {:oneOf [{:items {:type "integer"} + :type "array"} + {:type "null"}]}}}} + :responses {200 {:content {"application/json" {:schema {:properties {"total" {:format "int64" + :type "integer"}} + :required ["total"] + :type "object"}}}} + 400 {:content {"application/json" {:schema {:type "string"}}} + :description "kosh"} + 500 {:description "fail"}} + :summary "plus with body"}} + "/api/malli/plus/{z}" {:get {:parameters [{:in "query" + :name :x + :required true + :schema {:type "integer"}} + {:in "query" + :name :y + :required true + :schema {:type "integer"}} + {:in "path" + :name :z + :required true + :schema {:type "integer"}}] + :responses {200 {:content {"application/json" {:schema {:type "object" + :properties {:total {:type "integer"}} + :required [:total]}}}} + 400 {:description "kosh" + :content {"application/json" {:schema {:type "string"}}}} + 500 {:description "fail"}} + :summary "plus"} + :post {:parameters [{:in "path" + :name :z + :schema {:type "integer"} + :required true}] + :requestBody {:content {"application/json" {:schema {:oneOf [{:items {:type "integer"} + :type "array"} + {:type "null"}]}}}} + :responses {200 {:content {"application/json" {:schema {:properties {:total {:type "integer"}} + :required [:total] + :type "object"}}}} + 400 {:description "kosh" + :content {"application/json" {:schema {:type "string"}}}} + 500 {:description "fail"}} + :summary "plus with body"}} + "/api/schema/plus/{z}" {:get {:parameters [{:description "" + :in "query" + :name "x" + :required true + :schema {:format "int32" + :type "integer"}} + {:description "" + :in "query" + :name "y" + :required true + :schema {:type "integer" + :format "int32"}} + {:in "path" + :name "z" + :description "" + :required true + :schema {:type "integer" + :format "int32"}}] + :responses {200 {:content {"application/json" {:schema {:additionalProperties false + :properties {"total" {:format "int32" + :type "integer"}} + :required ["total"] + :type "object"}}}} + 400 {:description "kosh" + :content {"application/json" {:schema {:type "string"}}}} + 500 {:description "fail"}} + :summary "plus"} + :post {:parameters [{:in "path" + :name "z" + :description "" + :required true + :schema {:type "integer" + :format "int32"}}] + :requestBody {:content {"application/json" {:schema {:oneOf [{:type "array" + :items {:type "integer" + :format "int32"}} + {:type "null"}]}}}} + :responses {200 {:content {"application/json" {:schema {:properties {"total" {:format "int32" + :type "integer"}} + :additionalProperties false + :required ["total"] + :type "object"}}}} + 400 {:description "kosh" + :content {"application/json" {:schema {:type "string"}}}} + 500 {:description "fail"}} + :summary "plus with body"}}}}] + (is (= expected spec))))) + +(defn spec-paths [app uri] + (-> {:request-method :get, :uri uri} app :body :paths keys)) + +(deftest multiple-openapi-apis-test + (let [ping-route ["/ping" {:get (constantly "ping")}] + spec-route ["/openapi.json" + {:get {:no-doc true + :handler (openapi/create-openapi-handler)}}] + app (ring/ring-handler + (ring/router + [["/common" {:openapi {:id #{::one ::two}}} + ping-route] + + ["/one" {:openapi {:id ::one}} + ping-route + spec-route] + + ["/two" {:openapi {:id ::two}} + ping-route + spec-route + ["/deep" {:openapi {:id ::one}} + ping-route]] + ["/one-two" {:openapi {:id #{::one ::two}}} + spec-route]]))] + (is (= ["/common/ping" "/one/ping" "/two/deep/ping"] + (spec-paths app "/one/openapi.json"))) + (is (= ["/common/ping" "/two/ping"] + (spec-paths app "/two/openapi.json"))) + (is (= ["/common/ping" "/one/ping" "/two/ping" "/two/deep/ping"] + (spec-paths app "/one-two/openapi.json"))))) + +(deftest openapi-ui-config-test + (let [app (swagger-ui/create-swagger-ui-handler + {:path "/" + :url "/openapi.json" + :config {:jsonEditor true}})] + (is (= 302 (:status (app {:request-method :get, :uri "/"})))) + (is (= 200 (:status (app {:request-method :get, :uri "/index.html"})))) + (is (= {:jsonEditor true, :url "/openapi.json"} + (->> {:request-method :get, :uri "/config.json"} + (app) :body (m/decode m/instance "application/json")))))) + +(deftest without-openapi-id-test + (let [app (ring/ring-handler + (ring/router + [["/ping" + {:get (constantly "ping")}] + ["/openapi.json" + {:get {:no-doc true + :handler (openapi/create-openapi-handler)}}]]))] + (is (= ["/ping"] (spec-paths app "/openapi.json"))) + (is (= #{::openapi/default} + (-> {:request-method :get :uri "/openapi.json"} + (app) :body :x-id))))) + +(deftest with-options-endpoint-test + (let [app (ring/ring-handler + (ring/router + [["/ping" + {:options (constantly "options")}] + ["/pong" + (constantly "options")] + ["/openapi.json" + {:get {:no-doc true + :handler (openapi/create-openapi-handler)}}]]))] + (is (= ["/ping" "/pong"] (spec-paths app "/openapi.json"))) + (is (= #{::openapi/default} + (-> {:request-method :get :uri "/openapi.json"} + (app) :body :x-id))))) + +(deftest malli-all-parameter-types-test + (let [app (ring/ring-handler + (ring/router + [["/parameters" + {:post {:coercion malli/coercion + :parameters {:query [:map + [:q :string]] + :body [:map + [:b :string]] + :header [:map + [:h :string]] + :cookie [:map + [:c :string]] + :path [:map + [:p :string]]} + :responses {200 {:body [:map [:ok :string]]}} + :handler identity}}] + ["/openapi.json" + {:get {:handler (openapi/create-openapi-handler) + :no-doc true}}]])) + spec (-> {:request-method :get + :uri "/openapi.json"} + app + :body)] + (testing + "all non-body parameters" + (is (= [{:in "query" + :name :q + :required true + :schema {:type "string"}} + {:in "header" + :name :h + :required true + :schema {:type "string"}} + {:in "cookie" + :name :c + :required true + :schema {:type "string"}} + {:in "path" + :name :p + :required true + :schema {:type "string"}}] + (-> spec + (get-in [:paths "/parameters" :post :parameters]) + #_(doto clojure.pprint/pprint))))) + (testing + "body parameter" + (is (= {"application/json" {:schema {:type "object" + :properties {:b {:type "string"}} + :required [:b]}}} + (-> spec + (get-in [:paths "/parameters" :post :requestBody :content]) + #_(doto clojure.pprint/pprint))))) + (testing + "body response" + (is (= {"application/json" {:schema {:type "object" + :properties {:ok {:type "string"}} + :required [:ok]}}} + (-> spec + (get-in [:paths "/parameters" :post :responses 200 :content]) + #_(doto clojure.pprint/pprint))))))) + +(deftest malli-all-parameter-types-test-per-content-type + (let [app (ring/ring-handler + (ring/router + [["/parameters" + {:post {:coercion malli/coercion + :parameters {:query [:map + [:q :string]] + :request {:content {"application/json" [:map + [:b :string]]}} + :header [:map + [:h :string]] + :cookie [:map + [:c :string]] + :path [:map + [:p :string]]} + :responses {200 {:content {"application/json" [:map [:ok :string]]}}} + :handler identity}}] + ["/openapi.json" + {:get {:handler (openapi/create-openapi-handler) + :no-doc true}}]])) + spec (-> {:request-method :get + :uri "/openapi.json"} + app + :body)] + (testing + "all non-body parameters" + (is (= [{:in "query" + :name :q + :required true + :schema {:type "string"}} + {:in "header" + :name :h + :required true + :schema {:type "string"}} + {:in "cookie" + :name :c + :required true + :schema {:type "string"}} + {:in "path" + :name :p + :required true + :schema {:type "string"}}] + (-> spec + (get-in [:paths "/parameters" :post :parameters]) + #_(doto clojure.pprint/pprint))))) + (testing + "body parameter" + (is (= {"application/json" {:schema {:type "object" + :properties {:b {:type "string"}} + :required [:b]}}} + (-> spec + (get-in [:paths "/parameters" :post :requestBody :content]) + #_(doto clojure.pprint/pprint))))) + (testing + "body response" + (is (= {"application/json" {:schema {:type "object" + :properties {:ok {:type "string"}} + :required [:ok]}}} + (-> spec + (get-in [:paths "/parameters" :post :responses 200 :content]) + #_(doto clojure.pprint/pprint))))))) + + +(deftest schema-all-parameter-types-test-per-content-type + (let [app (ring/ring-handler + (ring/router + [["/parameters" + {:post {:coercion schema/coercion + :parameters {:query {:q s/Str} + :request {:content {"application/json" {:b s/Str}}} + :header {:h s/Str} + :cookie {:c s/Str} + :path {:p s/Str}} + :responses {200 {:content {"application/json" {:ok s/Str}}}} + :handler identity}}] + ["/openapi.json" + {:get {:handler (openapi/create-openapi-handler) + :no-doc true}}]])) + spec (-> {:request-method :get + :uri "/openapi.json"} + app + :body)] + (testing + "all non-body parameters" + (is (= [{:description "" + :in "query" + :name "q" + :required true + :schema {:type "string"}} + {:description "" + :in "header" + :name "h" + :required true + :schema {:type "string"}} + {:description "" + :in "cookie" + :name "c" + :required true + :schema {:type "string"}} + {:description "" + :in "path" + :name "p" + :required true + :schema {:type "string"}}] + (-> spec + (get-in [:paths "/parameters" :post :parameters]) + #_(doto clojure.pprint/pprint))))) + (testing + "body parameter" + (is (= {"application/json" {:schema {:additionalProperties false + :properties {"b" {:type "string"}} + :required ["b"] + :type "object"}}} + (-> spec + (get-in [:paths "/parameters" :post :requestBody :content]) + #_(doto clojure.pprint/pprint))))) + (testing + "body response" + (is (= {"application/json" {:schema {:additionalProperties false + :properties {"ok" {:type "string"}} + :required ["ok"] + :type "object"}}} + (-> spec + (get-in [:paths "/parameters" :post :responses 200 :content]) + #_(doto clojure.pprint/pprint))))))) +(deftest all-parameter-types-test + (let [app (ring/ring-handler + (ring/router + [["/parameters" + {:post {:coercion spec/coercion + :parameters {:query {:q string?} + :body {:b string?} + :cookies {:c string?} + :header {:h string?} + :path {:p string?}} + :responses {200 {:body {:ok string?}}} + :handler identity}}] + ["/openapi.json" + {:get {:no-doc true + :handler (openapi/create-openapi-handler)}}]])) + spec (:body (app {:request-method :get, :uri "/openapi.json"}))] + (is (= [{:description "" + :in "query" + :name "q" + :required true + :schema {:type "string"}} + {:description "" + :in "cookies" + :name "c" + :required true + :schema {:type "string"}} + {:description "" + :in "header" + :name "h" + :required true + :schema {:type "string"}} + {:description "" + :in "path" + :name "p" + :required true + :schema {:type "string"}}] + (-> spec + (get-in [:paths "/parameters" :post :parameters]) + #_(doto clojure.pprint/pprint)))) + (is (= {"application/json" {:schema {:properties {"b" {:type "string"}} + :required ["b"] + :type "object"}}} + (-> spec + (get-in [:paths "/parameters" :post :requestBody :content]) + #_(doto clojure.pprint/pprint)))) + (is (= {"application/json" {:schema {:properties {"ok" {:type "string"}} + :required ["ok"] + :type "object"}}} + (-> spec + (get-in [:paths "/parameters" :post :responses 200 :content]) + #_(doto clojure.pprint/pprint))))))