mirror of
https://github.com/metosin/reitit.git
synced 2025-12-16 16:01:11 +00:00
OpenAPI V3 Support
This commit is contained in:
parent
3dff4c84aa
commit
c576b47634
8 changed files with 805 additions and 2 deletions
|
|
@ -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
|
||||
;;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
12
modules/reitit-openapi/project.clj
Normal file
12
modules/reitit-openapi/project.clj
Normal file
|
|
@ -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]])
|
||||
109
modules/reitit-openapi/src/reitit/openapi.cljc
Normal file
109
modules/reitit-openapi/src/reitit/openapi.cljc
Normal file
|
|
@ -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))))))
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
552
test/cljc/reitit/openapi_test.clj
Normal file
552
test/cljc/reitit/openapi_test.clj
Normal file
|
|
@ -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))))))
|
||||
Loading…
Reference in a new issue