diff --git a/examples/openapi/README.md b/examples/openapi/README.md new file mode 100644 index 00000000..ac5c0915 --- /dev/null +++ b/examples/openapi/README.md @@ -0,0 +1,18 @@ +# OpenAPI 3 feature showcase + +## Usage + +```clj +> lein repl +(start) +``` + +- Swagger UI served at +- Openapi spec served at +- See [src/example/server.clj](src/example/server.clj) for details + + + +## License + +Copyright © 2023 Metosin Oy diff --git a/examples/openapi/openapi.png b/examples/openapi/openapi.png new file mode 100644 index 00000000..6f775221 Binary files /dev/null and b/examples/openapi/openapi.png differ diff --git a/examples/openapi/project.clj b/examples/openapi/project.clj new file mode 100644 index 00000000..b9750867 --- /dev/null +++ b/examples/openapi/project.clj @@ -0,0 +1,9 @@ +(defproject openapi "0.1.0-SNAPSHOT" + :description "Reitit OpenAPI example" + :dependencies [[org.clojure/clojure "1.10.0"] + [metosin/jsonista "0.2.6"] + [ring/ring-jetty-adapter "1.7.1"] + [metosin/reitit "0.7.0-alpha5"] + [metosin/ring-swagger-ui "5.0.0-alpha.0"]] + :repl-options {:init-ns example.server} + :profiles {:dev {:dependencies [[ring/ring-mock "0.3.2"]]}}) diff --git a/examples/openapi/src/example/server.clj b/examples/openapi/src/example/server.clj new file mode 100644 index 00000000..9a1227a2 --- /dev/null +++ b/examples/openapi/src/example/server.clj @@ -0,0 +1,146 @@ +(ns example.server + (:require [reitit.ring :as ring] + [reitit.coercion.malli] + [reitit.openapi :as openapi] + [reitit.ring.malli] + [reitit.swagger-ui :as swagger-ui] + [reitit.ring.coercion :as coercion] + [reitit.dev.pretty :as pretty] + [reitit.ring.middleware.muuntaja :as muuntaja] + [reitit.ring.middleware.exception :as exception] + [reitit.ring.middleware.multipart :as multipart] + [reitit.ring.middleware.parameters :as parameters] + [ring.adapter.jetty :as jetty] + [muuntaja.core :as m])) + +(def app + (ring/ring-handler + (ring/router + [["/openapi.json" + {:get {:no-doc true + :openapi {:info {:title "my-api" + :description "openapi3 docs with [malli](https://github.com/metosin/malli) and reitit-ring" + :version "0.0.1"} + ;; used in /secure APIs below + :components {:securitySchemes {"auth" {:type :apiKey + :in :header + :name "Example-Api-Key"}}}} + :handler (openapi/create-openapi-handler)}}] + + ["/pizza" + {:get {:summary "Fetch a pizza | Multiple content-types, multiple examples" + :content-types ["application/json" "application/edn"] + :responses {200 {:content {"application/json" {:description "Fetch a pizza as json" + :schema [:map + [:color :keyword] + [:pineapple :boolean]] + :examples {:white {:description "White pizza with pineapple" + :value {:color :white + :pineapple true}} + :red {:description "Red pizza" + :value {:color :red + :pineapple false}}}} + "application/edn" {:description "Fetch a pizza as edn" + :schema [:map + [:color :keyword] + [:pineapple :boolean]] + :examples {:red {:description "Red pizza with pineapple" + :value (pr-str {:color :red :pineapple true})}}}}}} + :handler (fn [_request] + {:status 200 + :body {:color :red + :pineapple true}})} + :post {:summary "Create a pizza | Multiple content-types, multiple examples" + :content-types ["application/json" "application/edn"] + :request {:content {"application/json" {:description "Create a pizza using json" + :schema [:map + [:color :keyword] + [:pineapple :boolean]] + :examples {:purple {:value {:color :purple + :pineapple false}}}} + "application/edn" {:description "Create a pizza using EDN" + :schema [:map + [:color :keyword] + [:pineapple :boolean]] + :examples {:purple {:value (pr-str {:color :purple + :pineapple false})}}}}} + :responses {200 {:content {:default {:description "Success" + :schema [:map [:success :boolean]] + :example {:success true}}}}} + :handler (fn [_request] + {:status 200 + :body {:success true}})}}] + + + ["/contact" + {:get {:summary "Search for a contact | Customizing via malli properties" + :parameters {:query [:map + [:limit {:title "How many results to return? Optional." + :optional true + :json-schema/default 30 + :json-schema/example 10} + int?] + [:email {:title "Email address to search for" + :json-schema/format "email"} + string?]]} + :responses {200 {:content {:default {:schema [:vector + [:map + [:name {:json-schema/example "Heidi"} + string?] + [:email {:json-schema/example "heidi@alps.ch"} + string?]]]}}}} + :handler (fn [_request] + [{:name "Heidi" + :email "heidi@alps.ch"}])}}] + + ["/secure" + {:tags ["secure"] + :openapi {:security [{"auth" []}]}} + ["/get" + {:get {:summary "endpoint authenticated with a header" + :responses {200 {:body [:map [:secret :string]]} + 401 {:body [:map [: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 + :body {:secret "I am a marmot"}} + {:status 401 + :body {:error "unauthorized"}}))}}]]] + + {;;:reitit.middleware/transform dev/print-request-diffs ;; pretty diffs + :exception pretty/exception + :data {:coercion reitit.coercion.malli/coercion + :muuntaja m/instance + :middleware [openapi/openapi-feature + ;; query-params & form-params + parameters/parameters-middleware + ;; content-negotiation + muuntaja/format-negotiate-middleware + ;; encoding response body + muuntaja/format-response-middleware + ;; exception handling + exception/exception-middleware + ;; decoding request body + muuntaja/format-request-middleware + ;; coercing response bodys + coercion/coerce-response-middleware + ;; coercing request parameters + coercion/coerce-request-middleware + ;; multipart + multipart/multipart-middleware]}}) + (ring/routes + (swagger-ui/create-swagger-ui-handler + {:path "/" + :config {:validatorUrl nil + :urls [{:name "openapi", :url "openapi.json"}] + :urls.primaryName "openapi" + :operationsSorter "alpha"}}) + (ring/create-default-handler)))) + +(defn start [] + (jetty/run-jetty #'app {:port 3000, :join? false}) + (println "server running in port 3000")) + +(comment + (start))