diff --git a/doc/ring/openapi.md b/doc/ring/openapi.md index d3e127cf..c5187095 100644 --- a/doc/ring/openapi.md +++ b/doc/ring/openapi.md @@ -31,6 +31,29 @@ Coercion keys also contribute to the docs: Use `:request` parameter coercion (instead of `:body`) to unlock per-content-type coercions. See [Coercion](coercion.md). +## Custom OpenAPI data + +The `:openapi` route data key can be used to add top-level or +route-level information to the generated OpenAPI spec. This is useful +for providing `"securitySchemes"`, `"examples"` or other OpenAPI keys +that are not generated automatically by reitit. + +```clj +["/foo" + {:post {:parameters {:body {:name string? :age int?}} + :openapi {:requestBody + {:content + {"application/json" + {:examples {"Pyry" {:summary "Pyry, 45y" + :value {:name "Pyry" :age 45}} + "Cat" {:summary "Cat, 8y" + :value {:name "Cat" :age 8}}}}}}} + ...}}] +``` + +See [the http-swagger example](../../examples/http-swagger) for a +working examples of `"securitySchemes"` and `"examples"`. + ## OpenAPI spec Serving the OpenAPI specification is handled by `reitit.openapi/create-openapi-handler`. It takes no arguments and returns a ring handler which collects at request-time data from all routes and returns an OpenAPI specification as Clojure data, to be encoded by a response formatter. diff --git a/examples/http-swagger/src/example/server.clj b/examples/http-swagger/src/example/server.clj index d0a9164e..6b5903d2 100644 --- a/examples/http-swagger/src/example/server.clj +++ b/examples/http-swagger/src/example/server.clj @@ -112,6 +112,22 @@ {:status 200 :body {:total (+ x y)}})} :post {:summary "plus with data-spec body parameters" + ;; OpenAPI3 named examples for request & response + :openapi {:requestBody + {:content + {"application/json" + {:examples {"add-one-one" {:summary "1+1" + :value {:x 1 :y 1}} + "add-one-two" {:summary "1+2" + :value {:x 1 :y 2}}}}}} + :responses + {200 + {:content + {"application/json" + {:examples {"two" {:summary "2" + :value {:total 2}} + "three" {:summary "3" + :value {:total 3}}}}}}}} :parameters {:body {:x int?, :y int?}} :responses {200 {:body {:total int?}}} :handler (fn [{{{:keys [x y]} :body} :parameters}] diff --git a/test/cljc/reitit/openapi_test.clj b/test/cljc/reitit/openapi_test.clj index 0788432e..bef17f7c 100644 --- a/test/cljc/reitit/openapi_test.clj +++ b/test/cljc/reitit/openapi_test.clj @@ -17,6 +17,7 @@ [reitit.swagger-ui :as swagger-ui] [schema.core :as s] [schema-tools.core] + [spec-tools.core :as st] [spec-tools.data-spec :as ds])) (defn validate @@ -432,6 +433,83 @@ (testing "spec is valid" (is (nil? (validate spec)))))))) +(deftest examples-test + (doseq [[coercion ->schema] + [[#'malli/coercion (fn [nom] [:map + {:json-schema/example {nom "EXAMPLE2"}} + [nom [:string {:json-schema/example "EXAMPLE"}]]])] + [#'schema/coercion (fn [nom] (schema-tools.core/schema + {nom (schema-tools.core/schema s/Str {:openapi/example "EXAMPLE"})} + {:openapi/example {nom "EXAMPLE2"}}))] + [#'spec/coercion (fn [nom] + (assoc + (ds/spec ::foo {nom (st/spec string? {:openapi/example "EXAMPLE"})}) + :openapi/example {nom "EXAMPLE2"}))]]] + (testing coercion + (let [app (ring/ring-handler + (ring/router + [["/examples" + {:post {:decription "examples" + :coercion @coercion + :parameters {:query (->schema :q) + :request {:body (->schema :b)}} + :responses {200 {:description "success" + :body (->schema :ok)}} + :openapi {:requestBody + {:content + {"application/json" + {:examples + {"named-example" {:description "a named example" + :value {:b "named"}}}}}} + :responses + {200 + {:content + {"application/json" + {:examples + {"response-example" {:value {:ok "response"}}}}}}}} + :handler identity}}] + ["/openapi.json" + {:get {:handler (openapi/create-openapi-handler) + :openapi {:info {:title "" :version "0.0.1"}} + :no-doc true}}]] + {:data {:middleware [openapi/openapi-feature]}})) + spec (-> {:request-method :get + :uri "/openapi.json"} + app + :body)] + (testing "query parameter" + (is (match? [{:in "query" + :name "q" + :required true + :schema {:type "string" + :example "EXAMPLE"}}] + (-> spec + (get-in [:paths "/examples" :post :parameters]) + normalize)))) + (testing "body parameter" + (is (match? {:schema {:type "object" + :properties {:b {:type "string" + :example "EXAMPLE"}} + :required ["b"] + :example {:b "EXAMPLE2"}} + :examples {:named-example {:description "a named example" + :value {:b "named"}}}} + (-> spec + (get-in [:paths "/examples" :post :requestBody :content "application/json"]) + normalize)))) + (testing "body response" + (is (match? {:schema {:type "object" + :properties {:ok {:type "string" + :example "EXAMPLE"}} + :required ["ok"] + :example {:ok "EXAMPLE2"}} + :examples {:response-example {:value {:ok "response"}}}} + (-> spec + (get-in [:paths "/examples" :post :responses 200 :content "application/json"]) + normalize)))) + (testing "spec is valid" + (is (nil? (validate spec)))))))) + (deftest multipart-test (doseq [[coercion file-schema string-schema] [[#'malli/coercion