Add API documentation using OpenAPI and SwaggerUI

This commit is contained in:
2026-02-01 01:45:18 +01:00
parent 3ca0514b7a
commit 1f62fe6f66
3 changed files with 83 additions and 12 deletions

View File

@@ -32,3 +32,5 @@ The following links proved more than useful when working on this assignment:
* <https://clojurecivitas.github.io/malli/elements_of_malli.html>
* <https://github.com/metosin/reitit/blob/master/doc/coercion/malli_coercion.md>
* <https://cljdoc.org/d/com.github.seancorfield/next.jdbc/1.3.1086/doc/getting-started>
* <https://cljdoc.org/d/metosin/reitit/0.10.0/doc/ring/swagger-support>
* <https://www.baeldung.com/clojure-ring>

View File

@@ -1,5 +1,5 @@
(defproject yohoho "0.1.0-SNAPSHOT"
:description "Yo-Ho-Ho, a take home assignment for the brave"
(defproject yohoho "0.42.0"
:description "Yo-ho-ho, a take home assignment for the brave"
:url "https://git.dromaludaire.info/yohoho"
:license {:name "WTFPL Do What the Fuck You Want to Public License"
:url "https://www.wtfpl.net/about/"}
@@ -8,6 +8,8 @@
[ring/ring-jetty-adapter "1.15.3"]
[metosin/reitit "0.10.0"]
[metosin/reitit-malli "0.10.0"]
[fi.metosin/reitit-openapi "0.10.0"]
[metosin/reitit-swagger-ui "0.10.0"]
[metosin/muuntaja "0.6.11"]
[com.github.seancorfield/next.jdbc "1.3.1086"]
[org.xerial/sqlite-jdbc "3.51.1.0"]]

View File

@@ -5,6 +5,8 @@
[muuntaja.core :as m]
[reitit.coercion.malli :as malli]
[reitit.ring.coercion :as coercion]
[reitit.openapi :as openapi]
[reitit.swagger-ui :as swagger-ui]
[next.jdbc :as jdbc]
[next.jdbc.sql :as sql]
[next.jdbc.result-set :as rs])
@@ -25,11 +27,38 @@
(def Email [:re email-regexp])
(def Item
(def ItemInput
[:map
[:name :string]
[:email Email]])
(def Item
[:map
[:id :int]
[:name :string]
[:email Email]])
(def ItemResponse Item)
(def ItemsResponse
[:map
[:items [:vector Item]]])
(def ErrorResponse
[:map
[:error :string]
[:message {:optional true} :string]])
;; Those are generated by Malli when validation fails
;; TODO: find a way to produce lighter messages on validation error
;; (maybe a custom middleware?)
(def Error400Response
[:map
[:value :map]
[:type :string]
[:coercion :string]
[:in [:vector :string]]
[:humanized :map]])
;; Database stuff -------------------------------------------------
(def db (:db config))
@@ -124,12 +153,45 @@
(ring/ring-handler
(ring/router
[;; Health check
["/ahoy" {:get ahoy-handler}]
["/ahoy" {:get {:summary "Health check"
:openapi {:tags ["Health"]}
:handler ahoy-handler
:responses {200 {:body :string
:description "Server is alive"}}}}]
;; The real assignment: create and retrieve items
["/item/:id" {:get get-item-handler}]
["/items" {:get get-items-handler
:post {:handler create-item-handler
:parameters {:body Item}}}]]
["/item/:id" {:get {:summary "Get an item by id"
:openapi {:tags ["Items"]}
:handler get-item-handler
:parameters {:path [:map [:id :int]]}
:responses {200 {:body ItemResponse
:description "Item found"}
400 {:body Error400Response
:description "Invalid input"}
404 {:body ErrorResponse
:description "Item not found"}}}}]
["/items" {:get {:summary "Get all items"
:openapi {:tags ["Items"]}
:handler get-items-handler
:responses {200 {:body ItemsResponse ;; TODO!
:description "List of all items"}}}
:post {:summary "Create an item"
:openapi {:tags ["Items"]}
:handler create-item-handler
:parameters {:body ItemInput}
:responses {201 {:body Item
:description "Item created successfully"}
400 {:body Error400Response
:description "Invalid input"}
415 {:body ErrorResponse
:description "Unsupported media type"}}}}]
;; OpenAPI routes
["" {:no-doc true
:openapi {:info {:title "Yo-ho-ho API"
:description "A take-home assignment for HolidayPirates"
:version "0.42.0"}}}
["/openapi.json" {:get (openapi/create-openapi-handler)}]
["/doc/*" {:get (swagger-ui/create-swagger-ui-handler {:url "/openapi.json"})}]]]
;; Middlewares:
;; - wrap-content-type-json: ensure POST routes are sent JSON payload
;; - muuntaja middleware to automatically decode JSON
@@ -141,10 +203,14 @@
coercion/coerce-exceptions-middleware
coercion/coerce-request-middleware
coercion/coerce-response-middleware]}})
(ring/routes
;; Add trailing slash when missing, eg. for /doc
(ring/redirect-trailing-slash-handler {:method :add})
;; Default route: anything not explicitely handled should give a 404
(ring/create-default-handler
{:not-found (constantly {:status 404
:body {:error "Not Found"}})})))
:body {:error "Not Found"}})}))))
;; Entry point ----------------------------------------------------
@@ -156,4 +222,5 @@
(println "Initializing SQLite database...")
(init-db!)
(println "Ahoy! Yo-ho-ho API starting on port" port)
(println "API documentation on /openapi.json and on /doc (interactive UI)")
(http-server/run-jetty app {:port port})))