diff --git a/README.md b/README.md index 840fb41..8f8a55a 100644 --- a/README.md +++ b/README.md @@ -32,3 +32,5 @@ The following links proved more than useful when working on this assignment: * * * +* +* diff --git a/project.clj b/project.clj index a2d60c4..17eef23 100644 --- a/project.clj +++ b/project.clj @@ -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"]] diff --git a/src/yohoho/core.clj b/src/yohoho/core.clj index 0022a3a..b360c32 100644 --- a/src/yohoho/core.clj +++ b/src/yohoho/core.clj @@ -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]}}) - ;; Default route: anything not explicitely handled should give a 404 - (ring/create-default-handler - {:not-found (constantly {:status 404 - :body {:error "Not Found"}})}))) + + (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"}})})))) ;; 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})))