mirror of
https://github.com/metosin/reitit.git
synced 2026-01-26 16:20:35 +00:00
Merge pull request #592 from metosin/openapi-fixes
misc. fixes for openapi3 support
This commit is contained in:
commit
8bf4b5c6a6
3 changed files with 203 additions and 197 deletions
|
|
@ -39,148 +39,148 @@
|
||||||
|
|
||||||
(def app
|
(def app
|
||||||
(http/ring-handler
|
(http/ring-handler
|
||||||
(http/router
|
(http/router
|
||||||
[["/swagger.json"
|
[["/swagger.json"
|
||||||
{:get {:no-doc true
|
{:get {:no-doc true
|
||||||
:swagger {:info {:title "my-api"
|
:swagger {:info {:title "my-api"
|
||||||
:description "swagger-docs with reitit-http"
|
:description "swagger-docs with reitit-http"
|
||||||
:version "0.0.1"}
|
:version "0.0.1"}
|
||||||
;; used in /secure APIs below
|
;; used in /secure APIs below
|
||||||
:securityDefinitions {"auth" {:type :apiKey
|
:securityDefinitions {"auth" {:type :apiKey
|
||||||
:in :header
|
:in :header
|
||||||
:name "Example-Api-Key"}}}
|
:name "Example-Api-Key"}}}
|
||||||
:handler (swagger/create-swagger-handler)}}]
|
:handler (swagger/create-swagger-handler)}}]
|
||||||
["/openapi.json"
|
["/openapi.json"
|
||||||
{:get {:no-doc true
|
{:get {:no-doc true
|
||||||
:openapi {:info {:title "my-api"
|
:openapi {:info {:title "my-api"
|
||||||
:description "openap-docs with reitit-http"
|
:description "openapi3-docs with reitit-http"
|
||||||
:version "0.0.1"}
|
:version "0.0.1"}
|
||||||
;; used in /secure APIs below
|
;; used in /secure APIs below
|
||||||
:components {:securitySchemes {"auth" {:type :apiKey
|
:components {:securitySchemes {"auth" {:type :apiKey
|
||||||
:in :header
|
:in :header
|
||||||
:name "Example-Api-Key"}}}}
|
:name "Example-Api-Key"}}}}
|
||||||
:handler (openapi/create-openapi-handler)}}]
|
:handler (openapi/create-openapi-handler)}}]
|
||||||
|
|
||||||
["/files"
|
["/files"
|
||||||
{:tags ["files"]}
|
{:tags ["files"]}
|
||||||
|
|
||||||
["/upload"
|
["/upload"
|
||||||
{:post {:summary "upload a file"
|
{:post {:summary "upload a file"
|
||||||
:parameters {:multipart {:file multipart/temp-file-part}}
|
:parameters {:multipart {:file multipart/temp-file-part}}
|
||||||
:responses {200 {:body {:name string?, :size int?}}}
|
:responses {200 {:body {:name string?, :size int?}}}
|
||||||
:handler (fn [{{{:keys [file]} :multipart} :parameters}]
|
:handler (fn [{{{:keys [file]} :multipart} :parameters}]
|
||||||
{:status 200
|
{:status 200
|
||||||
:body {:name (:filename file)
|
:body {:name (:filename file)
|
||||||
:size (:size file)}})}}]
|
:size (:size file)}})}}]
|
||||||
|
|
||||||
["/download"
|
["/download"
|
||||||
{:get {:summary "downloads a file"
|
{:get {:summary "downloads a file"
|
||||||
:swagger {:produces ["image/png"]}
|
:swagger {:produces ["image/png"]}
|
||||||
:handler (fn [_]
|
:handler (fn [_]
|
||||||
|
{:status 200
|
||||||
|
:headers {"Content-Type" "image/png"}
|
||||||
|
:body (io/input-stream
|
||||||
|
(io/resource "reitit.png"))})}}]]
|
||||||
|
|
||||||
|
["/async"
|
||||||
|
{:get {:tags ["async"]
|
||||||
|
:summary "fetches random users asynchronously over the internet"
|
||||||
|
:parameters {:query (s/keys :req-un [::results] :opt-un [::seed])}
|
||||||
|
:responses {200 {:body any?}}
|
||||||
|
:handler (fn [{{{:keys [seed results]} :query} :parameters}]
|
||||||
|
(d/chain
|
||||||
|
(aleph/get
|
||||||
|
"https://randomuser.me/api/"
|
||||||
|
{:query-params {:seed seed, :results results}})
|
||||||
|
:body
|
||||||
|
(partial m/decode "application/json")
|
||||||
|
:results
|
||||||
|
(fn [results]
|
||||||
{:status 200
|
{:status 200
|
||||||
:headers {"Content-Type" "image/png"}
|
:body results})))}}]
|
||||||
:body (io/input-stream
|
|
||||||
(io/resource "reitit.png"))})}}]]
|
|
||||||
|
|
||||||
["/async"
|
["/math"
|
||||||
{:get {:tags ["async"]
|
{:tags ["math"]}
|
||||||
:summary "fetches random users asynchronously over the internet"
|
|
||||||
:parameters {:query (s/keys :req-un [::results] :opt-un [::seed])}
|
|
||||||
:responses {200 {:body any?}}
|
|
||||||
:handler (fn [{{{:keys [seed results]} :query} :parameters}]
|
|
||||||
(d/chain
|
|
||||||
(aleph/get
|
|
||||||
"https://randomuser.me/api/"
|
|
||||||
{:query-params {:seed seed, :results results}})
|
|
||||||
:body
|
|
||||||
(partial m/decode "application/json")
|
|
||||||
:results
|
|
||||||
(fn [results]
|
|
||||||
{:status 200
|
|
||||||
:body results})))}}]
|
|
||||||
|
|
||||||
["/math"
|
["/plus"
|
||||||
{:tags ["math"]}
|
{:get {:summary "plus with data-spec query parameters"
|
||||||
|
:parameters {:query {:x int?, :y int?}}
|
||||||
|
:responses {200 {:body {:total pos-int?}}}
|
||||||
|
:handler (fn [{{{:keys [x y]} :query} :parameters}]
|
||||||
|
{:status 200
|
||||||
|
:body {:total (+ x y)}})}
|
||||||
|
:post {:summary "plus with data-spec body parameters"
|
||||||
|
:parameters {:body {:x int?, :y int?}}
|
||||||
|
:responses {200 {:body {:total int?}}}
|
||||||
|
:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||||
|
{:status 200
|
||||||
|
:body {:total (+ x y)}})}}]
|
||||||
|
|
||||||
["/plus"
|
["/minus"
|
||||||
{:get {:summary "plus with data-spec query parameters"
|
{:get {:summary "minus with clojure.spec query parameters"
|
||||||
:parameters {:query {:x int?, :y int?}}
|
:parameters {:query (s/keys :req-un [::x ::y])}
|
||||||
:responses {200 {:body {:total pos-int?}}}
|
:responses {200 {:body (s/keys :req-un [::total])}}
|
||||||
:handler (fn [{{{:keys [x y]} :query} :parameters}]
|
:handler (fn [{{{:keys [x y]} :query} :parameters}]
|
||||||
|
{:status 200
|
||||||
|
:body {:total (- x y)}})}
|
||||||
|
:post {:summary "minus with clojure.spec body parameters"
|
||||||
|
:parameters {:body (s/keys :req-un [::x ::y])}
|
||||||
|
:responses {200 {:body (s/keys :req-un [::total])}}
|
||||||
|
:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||||
|
{:status 200
|
||||||
|
:body {:total (- x y)}})}}]]
|
||||||
|
["/secure"
|
||||||
|
{:tags ["secure"]
|
||||||
|
:openapi {:security [{"auth" []}]}
|
||||||
|
:swagger {:security [{"auth" []}]}}
|
||||||
|
["/get"
|
||||||
|
{:get {:summary "endpoint authenticated with a header"
|
||||||
|
:responses {200 {:body {:secret string?}}
|
||||||
|
401 {:body {:error string?}}}
|
||||||
|
:handler (fn [request]
|
||||||
|
;; In a real app authentication would be handled by middleware
|
||||||
|
(if (= "secret" (get-in request [:headers "example-api-key"]))
|
||||||
{:status 200
|
{:status 200
|
||||||
:body {:total (+ x y)}})}
|
:body {:secret "I am a marmot"}}
|
||||||
:post {:summary "plus with data-spec body parameters"
|
{:status 401
|
||||||
:parameters {:body {:x int?, :y int?}}
|
:body {:error "unauthorized"}}))}}]]]
|
||||||
:responses {200 {:body {:total int?}}}
|
|
||||||
:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
|
||||||
{:status 200
|
|
||||||
:body {:total (+ x y)}})}}]
|
|
||||||
|
|
||||||
["/minus"
|
{;:reitit.interceptor/transform dev/print-context-diffs ;; pretty context diffs
|
||||||
{:get {:summary "minus with clojure.spec query parameters"
|
;;:validate spec/validate ;; enable spec validation for route data
|
||||||
:parameters {:query (s/keys :req-un [::x ::y])}
|
;;:reitit.spec/wrap spell/closed ;; strict top-level validation
|
||||||
:responses {200 {:body (s/keys :req-un [::total])}}
|
:exception pretty/exception
|
||||||
:handler (fn [{{{:keys [x y]} :query} :parameters}]
|
:data {:coercion reitit.coercion.spec/coercion
|
||||||
{:status 200
|
:muuntaja m/instance
|
||||||
:body {:total (- x y)}})}
|
:interceptors [;; swagger feature
|
||||||
:post {:summary "minus with clojure.spec body parameters"
|
swagger/swagger-feature
|
||||||
:parameters {:body (s/keys :req-un [::x ::y])}
|
;; openapi feature
|
||||||
:responses {200 {:body (s/keys :req-un [::total])}}
|
openapi/openapi-feature
|
||||||
:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
;; query-params & form-params
|
||||||
{:status 200
|
(parameters/parameters-interceptor)
|
||||||
:body {:total (- x y)}})}}]]
|
;; content-negotiation
|
||||||
["/secure"
|
(muuntaja/format-negotiate-interceptor)
|
||||||
{:tags ["secure"]
|
;; encoding response body
|
||||||
:openapi {:security [{"auth" []}]}
|
(muuntaja/format-response-interceptor)
|
||||||
:swagger {:security [{"auth" []}]}}
|
;; exception handling
|
||||||
["/get"
|
(exception/exception-interceptor)
|
||||||
{:get {:summary "endpoint authenticated with a header"
|
;; decoding request body
|
||||||
:responses {200 {:body {:secret string?}}
|
(muuntaja/format-request-interceptor)
|
||||||
401 {:body {:error string?}}}
|
;; coercing response bodys
|
||||||
:handler (fn [request]
|
(coercion/coerce-response-interceptor)
|
||||||
;; In a real app authentication would be handled by middleware
|
;; coercing request parameters
|
||||||
(if (= "secret" (get-in request [:headers "example-api-key"]))
|
(coercion/coerce-request-interceptor)
|
||||||
{:status 200
|
;; multipart
|
||||||
:body {:secret "I am a marmot"}}
|
(multipart/multipart-interceptor)]}})
|
||||||
{:status 401
|
(ring/routes
|
||||||
:body {:error "unauthorized"}}))}}]]]
|
(swagger-ui/create-swagger-ui-handler
|
||||||
|
{:path "/"
|
||||||
{;:reitit.interceptor/transform dev/print-context-diffs ;; pretty context diffs
|
:config {:validatorUrl nil
|
||||||
;;:validate spec/validate ;; enable spec validation for route data
|
:urls [{:name "swagger", :url "swagger.json"}
|
||||||
;;:reitit.spec/wrap spell/closed ;; strict top-level validation
|
{:name "openapi", :url "openapi.json"}]
|
||||||
:exception pretty/exception
|
:urls.primaryName "openapi"
|
||||||
:data {:coercion reitit.coercion.spec/coercion
|
:operationsSorter "alpha"}})
|
||||||
:muuntaja m/instance
|
(ring/create-default-handler))
|
||||||
:interceptors [;; swagger feature
|
{:executor sieppari/executor}))
|
||||||
swagger/swagger-feature
|
|
||||||
;; openapi feature
|
|
||||||
openapi/openapi-feature
|
|
||||||
;; query-params & form-params
|
|
||||||
(parameters/parameters-interceptor)
|
|
||||||
;; content-negotiation
|
|
||||||
(muuntaja/format-negotiate-interceptor)
|
|
||||||
;; encoding response body
|
|
||||||
(muuntaja/format-response-interceptor)
|
|
||||||
;; exception handling
|
|
||||||
(exception/exception-interceptor)
|
|
||||||
;; decoding request body
|
|
||||||
(muuntaja/format-request-interceptor)
|
|
||||||
;; coercing response bodys
|
|
||||||
(coercion/coerce-response-interceptor)
|
|
||||||
;; coercing request parameters
|
|
||||||
(coercion/coerce-request-interceptor)
|
|
||||||
;; multipart
|
|
||||||
(multipart/multipart-interceptor)]}})
|
|
||||||
(ring/routes
|
|
||||||
(swagger-ui/create-swagger-ui-handler
|
|
||||||
{:path "/"
|
|
||||||
:config {:validatorUrl nil
|
|
||||||
:urls [{:name "swagger", :url "swagger.json"}
|
|
||||||
{:name "openapi", :url "openapi.json"}]
|
|
||||||
:urls.primaryName "openapi"
|
|
||||||
:operationsSorter "alpha"}})
|
|
||||||
(ring/create-default-handler))
|
|
||||||
{:executor sieppari/executor}))
|
|
||||||
|
|
||||||
(defn start []
|
(defn start []
|
||||||
(jetty/run-jetty #'app {:port 3000, :join? false, :async true})
|
(jetty/run-jetty #'app {:port 3000, :join? false, :async true})
|
||||||
|
|
|
||||||
|
|
@ -107,40 +107,39 @@
|
||||||
(if (:schema $)
|
(if (:schema $)
|
||||||
(update $ :schema #(coercion/-compile-model this % nil))
|
(update $ :schema #(coercion/-compile-model this % nil))
|
||||||
$))]))})))
|
$))]))})))
|
||||||
:openapi (openapi/openapi-spec
|
:openapi (merge
|
||||||
(merge
|
(when (seq (dissoc parameters :body :request))
|
||||||
(when (seq (dissoc parameters :body :request))
|
(openapi/openapi-spec {::openapi/parameters
|
||||||
{::openapi/parameters
|
(into (empty parameters)
|
||||||
(into (empty parameters)
|
(for [[k v] (dissoc parameters :body :request)]
|
||||||
(for [[k v] (dissoc parameters :body :request)]
|
[k (coercion/-compile-model this v nil)]))}))
|
||||||
[k (coercion/-compile-model this v nil)]))})
|
(when (:body parameters)
|
||||||
(when (:body parameters)
|
{:requestBody (openapi/openapi-spec
|
||||||
{:requestBody (openapi/openapi-spec
|
{::openapi/content (zipmap content-types (repeat (coercion/-compile-model this (:body parameters) nil)))})})
|
||||||
{::openapi/content (zipmap content-types (repeat (coercion/-compile-model this (:body parameters) nil)))})})
|
(when (:request parameters)
|
||||||
(when (:request parameters)
|
{:requestBody (openapi/openapi-spec
|
||||||
{:requestBody (openapi/openapi-spec
|
{::openapi/content (merge
|
||||||
{::openapi/content (merge
|
(when-let [default (get-in parameters [:request :body])]
|
||||||
(when-let [default (get-in parameters [:request :body])]
|
(zipmap content-types (repeat (coercion/-compile-model this default nil))))
|
||||||
(zipmap content-types (repeat (coercion/-compile-model this default nil))))
|
(into {}
|
||||||
(into {}
|
(for [[format model] (:content (:request parameters))]
|
||||||
(for [[format model] (:content (:request parameters))]
|
[format (coercion/-compile-model this model nil)])))})})
|
||||||
[format (coercion/-compile-model this model nil)])))})})
|
(when responses
|
||||||
(when responses
|
{:responses
|
||||||
{:responses
|
(into
|
||||||
(into
|
(empty responses)
|
||||||
(empty responses)
|
(for [[k {:keys [body content] :as response}] responses]
|
||||||
(for [[k {:keys [body content] :as response}] responses]
|
[k (merge
|
||||||
[k (merge
|
(select-keys response [:description])
|
||||||
(select-keys response [:description])
|
(when (or body content)
|
||||||
(when (or body content)
|
(openapi/openapi-spec
|
||||||
(openapi/openapi-spec
|
{::openapi/content (merge
|
||||||
{::openapi/content (merge
|
(when body
|
||||||
(when body
|
(zipmap content-types (repeat (coercion/-compile-model this (:body response) nil))))
|
||||||
(zipmap content-types (repeat (coercion/-compile-model this (:body response) nil))))
|
(when response
|
||||||
(when response
|
(into {}
|
||||||
(into {}
|
(for [[format model] (:content response)]
|
||||||
(for [[format model] (:content response)]
|
[format (coercion/-compile-model this model nil)]))))})))]))}))
|
||||||
[format (coercion/-compile-model this model nil)]))))})))]))})))
|
|
||||||
(throw
|
(throw
|
||||||
(ex-info
|
(ex-info
|
||||||
(str "Can't produce Spec apidocs for " specification)
|
(str "Can't produce Spec apidocs for " specification)
|
||||||
|
|
|
||||||
|
|
@ -408,20 +408,23 @@
|
||||||
(get-in [:paths "/parameters" :post :parameters])
|
(get-in [:paths "/parameters" :post :parameters])
|
||||||
normalize))))
|
normalize))))
|
||||||
(testing "body parameter"
|
(testing "body parameter"
|
||||||
(is (match? {:schema {:type "object"
|
(is (match? (merge {:type "object"
|
||||||
:properties {:b {:type "string"}}
|
:properties {:b {:type "string"}}
|
||||||
#_#_:additionalProperties false ;; not present for spec
|
:required ["b"]}
|
||||||
:required ["b"]}}
|
;; spec outputs open schemas
|
||||||
|
(when-not (#{#'spec/coercion} coercion)
|
||||||
|
{:additionalProperties false}))
|
||||||
(-> spec
|
(-> spec
|
||||||
(get-in [:paths "/parameters" :post :requestBody :content "application/json"])
|
(get-in [:paths "/parameters" :post :requestBody :content "application/json" :schema])
|
||||||
normalize))))
|
normalize))))
|
||||||
(testing "body response"
|
(testing "body response"
|
||||||
(is (match? {:schema {:type "object"
|
(is (match? (merge {:type "object"
|
||||||
:properties {:ok {:type "string"}}
|
:properties {:ok {:type "string"}}
|
||||||
#_#_:additionalProperties false ;; not present for spec
|
:required ["ok"]}
|
||||||
:required ["ok"]}}
|
(when-not (#{#'spec/coercion} coercion)
|
||||||
|
{:additionalProperties false}))
|
||||||
(-> spec
|
(-> spec
|
||||||
(get-in [:paths "/parameters" :post :responses 200 :content "application/json"])
|
(get-in [:paths "/parameters" :post :responses 200 :content "application/json" :schema])
|
||||||
normalize))))
|
normalize))))
|
||||||
(testing "spec is valid"
|
(testing "spec is valid"
|
||||||
(is (nil? (validate spec))))))))
|
(is (nil? (validate spec))))))))
|
||||||
|
|
@ -458,34 +461,38 @@
|
||||||
app
|
app
|
||||||
:body)]
|
:body)]
|
||||||
(testing "body parameter"
|
(testing "body parameter"
|
||||||
(is (match? {:schema {:type "object"
|
(is (match? (merge {:type "object"
|
||||||
:properties {:b {:type "string"}}
|
:properties {:b {:type "string"}}
|
||||||
#_#_:additionalProperties false ;; not present for spec
|
:required ["b"]}
|
||||||
:required ["b"]}}
|
(when-not (#{#'spec/coercion} coercion)
|
||||||
|
{:additionalProperties false}))
|
||||||
(-> spec
|
(-> spec
|
||||||
(get-in [:paths "/parameters" :post :requestBody :content "application/json"])
|
(get-in [:paths "/parameters" :post :requestBody :content "application/json" :schema])
|
||||||
normalize)))
|
normalize)))
|
||||||
(is (match? {:schema {:type "object"
|
(is (match? (merge {:type "object"
|
||||||
:properties {:c {:type "string"}}
|
:properties {:c {:type "string"}}
|
||||||
#_#_:additionalProperties false ;; not present for spec
|
:required ["c"]}
|
||||||
:required ["c"]}}
|
(when-not (#{#'spec/coercion} coercion)
|
||||||
|
{:additionalProperties false}))
|
||||||
(-> spec
|
(-> spec
|
||||||
(get-in [:paths "/parameters" :post :requestBody :content "application/edn"])
|
(get-in [:paths "/parameters" :post :requestBody :content "application/edn" :schema])
|
||||||
normalize))))
|
normalize))))
|
||||||
(testing "body response"
|
(testing "body response"
|
||||||
(is (match? {:schema {:type "object"
|
(is (match? (merge {:type "object"
|
||||||
:properties {:ok {:type "string"}}
|
:properties {:ok {:type "string"}}
|
||||||
#_#_:additionalProperties false ;; not present for spec
|
:required ["ok"]}
|
||||||
:required ["ok"]}}
|
(when-not (#{#'spec/coercion} coercion)
|
||||||
|
{:additionalProperties false}))
|
||||||
(-> spec
|
(-> spec
|
||||||
(get-in [:paths "/parameters" :post :responses 200 :content "application/json"])
|
(get-in [:paths "/parameters" :post :responses 200 :content "application/json" :schema])
|
||||||
normalize)))
|
normalize)))
|
||||||
(is (match? {:schema {:type "object"
|
(is (match? (merge {:type "object"
|
||||||
:properties {:edn {:type "string"}}
|
:properties {:edn {:type "string"}}
|
||||||
#_#_:additionalProperties false ;; not present for spec
|
:required ["edn"]}
|
||||||
:required ["edn"]}}
|
(when-not (#{#'spec/coercion} coercion)
|
||||||
|
{:additionalProperties false}))
|
||||||
(-> spec
|
(-> spec
|
||||||
(get-in [:paths "/parameters" :post :responses 200 :content "application/edn"])
|
(get-in [:paths "/parameters" :post :responses 200 :content "application/edn" :schema])
|
||||||
normalize))))
|
normalize))))
|
||||||
(testing "validation"
|
(testing "validation"
|
||||||
(let [query {:request-method :post
|
(let [query {:request-method :post
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue