Skip to content

Commit f975bfa

Browse files
committed
feat(api): json -> transit
1 parent 422d5e7 commit f975bfa

File tree

3 files changed

+138
-82
lines changed

3 files changed

+138
-82
lines changed

deps.edn

+9-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
{org.clojure/clojure {:mvn/version "1.12.0"}
44
http-kit/http-kit {:mvn/version "2.8.0"}
55
metosin/reitit {:mvn/version "0.7.2"}
6+
7+
;;
8+
;; Transit support, and format negotiation library
9+
;; https://github.com/cognitect/transit-format
10+
;; https://github.com/metosin/muuntaja
11+
com.cognitect/transit-clj {:mvn/version "1.0.333"}
12+
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
13+
metosin/muuntaja {:mvn/version "0.6.11"}
14+
615
;;
716
;; Auth-related things
817
buddy/buddy-auth {:mvn/version "3.0.323"}
@@ -14,7 +23,6 @@
1423
migratus/migratus {:mvn/version "1.6.3"}
1524
com.github.seancorfield/honeysql {:mvn/version "2.6.1270"}
1625

17-
ring/ring-json {:mvn/version "0.5.1"}
1826
ring/ring-mock {:mvn/version "0.4.0"}
1927

2028
;; Logging

src/main/parts/entity/system.clj

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
relationships (edges)."
55
(:require
66
[clojure.spec.alpha :as s]
7-
[clojure.data.json :as json]
87
[parts.utils :refer [validate-spec]]
98
[parts.db :as db]))
109

src/main/parts/server.clj

+129-80
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
(ns parts.server
2+
"Primary namespace for the Parts server setup, handling route definitions,
3+
middleware configuration, and server lifecycle management. This namespace
4+
organizes the application's API routes with appropriate content types
5+
and authentication controls."
26
(:require
37
[clojure.core.async :as async]
48
[com.brunobonacci.mulog :as mulog]
@@ -7,115 +11,148 @@
711
[parts.api.auth :as api.auth]
812
[parts.api.systems :as api.systems]
913
[parts.auth :as auth]
10-
[parts.config :as config]
1114
[parts.db :as db]
1215
[parts.handlers.pages :as pages]
1316
[parts.handlers.waitlist :as waitlist]
1417
[parts.middleware :as middleware]
15-
[reitit.coercion.spec]
18+
[reitit.coercion.spec :as rcs]
1619
[reitit.ring :as ring]
17-
[reitit.swagger :as swagger]
18-
[reitit.swagger-ui :as swagger-ui]
19-
[ring.middleware.json :refer [wrap-json-body wrap-json-response]]
20+
[reitit.ring.coercion :as rrc]
21+
[reitit.ring.middleware.muuntaja :as muuntaja-middleware]
22+
[muuntaja.core :as muuntaja]
2023
[ring.middleware.params :refer [wrap-params]])
2124
(:gen-class))
2225

26+
;; ===== Content Type Configuration =====
27+
28+
(def transit-format
29+
"A configured Muuntaja instance that restricts API format negotiation to
30+
Transit only (application/transit+json). This instance transforms Clojure
31+
data structures to/from Transit format when used with appropriate middleware.
32+
The default format is explicitly set to Transit, so requests without
33+
an Accept header will receive Transit responses."
34+
(-> muuntaja/default-options
35+
(update :formats select-keys ["application/transit+json"])
36+
(assoc :default-format "application/transit+json")
37+
muuntaja/create))
38+
39+
;; ===== Middleware Configuration =====
40+
41+
(def base-middleware
42+
"Common middleware applied to all routes for essential functionality."
43+
[wrap-params
44+
middleware/exception
45+
middleware/logging])
46+
47+
(def html-middleware
48+
"Middleware specific to HTML routes that ensures responses are properly
49+
formatted as text/html."
50+
{:data {:middleware (into base-middleware
51+
[middleware/wrap-html-response])}})
52+
53+
(def api-middleware
54+
"Middleware for API endpoints that need Transit formatting.
55+
Includes JWT authentication setup but doesn't force authentication
56+
(individual routes can require auth with the with-auth helper)."
57+
{:data {:middleware (into base-middleware
58+
[muuntaja-middleware/format-middleware
59+
rrc/coerce-exceptions-middleware
60+
rrc/coerce-request-middleware
61+
rrc/coerce-response-middleware
62+
middleware/wrap-jwt-authentication])
63+
:muuntaja transit-format
64+
:coercion rcs/coercion}})
65+
66+
(defn with-auth
67+
"Helper function to require authentication for a route."
68+
[route-data]
69+
(update route-data :middleware (fnil conj []) middleware/jwt-auth))
70+
71+
;; ===== Route Definitions =====
72+
2373
(def html-routes
74+
"Public routes that return HTML content. These routes:
75+
- Don't require authentication
76+
- Return text/html content type
77+
- Use the html-middleware stack"
2478
[["/" {:get {:handler #(pages/home-page %)}}]
2579
["/system" {:get {:handler #(pages/system-graph %)}}]
2680
["/up" {:get {:handler (fn [_] {:status 200 :body "OK"})}}]
2781
["/waitlist-signup" {:post {:handler #(waitlist/signup %)}}]])
2882

2983
(def api-routes
30-
[["/swagger.json"
31-
{:get {:no-doc true
32-
:swagger {:info {:title "Parts API"
33-
:description "API for Parts"}}
34-
:handler (swagger/create-swagger-handler)}}]
35-
["/api"
84+
"API routes that return Transit data. Contains both public and authenticated routes.
85+
All routes:
86+
- Return application/transit+json content type
87+
- Use the api-middleware stack
88+
89+
Authenticated routes use the with-auth helper to require authentication."
90+
[["/api"
3691
["/ping"
37-
{:get {:swagger {:tags ["Utility"]}
38-
:handler (fn [_] {:status 200 :body {:message "Pong!"}})}}]
92+
{:get {:handler (fn [_] {:status 200 :body {:message "Pong!"}})}}]
3993

4094
;; Auth routes
41-
["/auth" {:swagger {:tags ["Authentication"]}}
95+
["/auth"
4296
["/login"
4397
{:post {:handler api.auth/login}}]
4498
["/refresh"
4599
{:post {:handler api.auth/refresh}}]
46100
["/logout"
47-
{:post {:handler api.auth/logout
48-
:middleware [middleware/jwt-auth]}}]]
101+
{:post (with-auth {:handler api.auth/logout})}]]
49102

50103
;; Account routes
51-
["/account" {:swagger {:tags ["Account"]}}
52-
[""
53-
{:get {:handler api.account/get-account}
54-
:patch {:handler api.account/update-account}
55-
:delete {:handler api.account/delete-account}
56-
:middleware [middleware/jwt-auth]}]
104+
["/account"
57105
["/register"
58-
{:post {:handler api.account/register-account}}]]
106+
{:post {:handler api.account/register-account}}]
107+
[""
108+
(with-auth {:get {:handler api.account/get-account}
109+
:patch {:handler api.account/update-account}
110+
:delete {:handler api.account/delete-account}})]]
59111

60112
;; Systems routes
61-
;; TODO: Put these behind jwt-auth middleware once the UI is ready
62-
["/systems" {:swagger {:tags ["Systems"]}
63-
:middleware [middleware/jwt-auth]}
64-
["" {:get {:summary "List all systems for current user"
65-
:handler api.systems/list-systems}
66-
:post {:summary "Create new system"
67-
:handler api.systems/create-system}}]
113+
["/systems"
114+
["" {:get {:handler api.systems/list-systems}
115+
:post {:handler api.systems/create-system}}]
68116
["/:id" {:parameters {:path {:id string?}}}
69-
["" {:get {:summary "Get system by ID"
70-
:handler api.systems/get-system}
71-
:put {:summary "Update entire system"
72-
:handler api.systems/update-system}
73-
:delete {:summary "Delete system"
74-
:handler api.systems/delete-system}}]
75-
["/pdf" {:get {:summary "Generate PDF export of system"
76-
:handler api.systems/export-pdf}}]]]]])
77-
78-
(defn create-handler [routes middleware-config swagger?]
79-
(ring/ring-handler
80-
(ring/router routes middleware-config)
81-
(if swagger?
82-
(ring/routes
83-
(swagger-ui/create-swagger-ui-handler
84-
{:path "/swagger-ui"
85-
:url "/swagger.json"
86-
:config {:validatorUrl nil
87-
:operationsSorter "alpha"}})
88-
(ring/create-default-handler))
89-
(ring/create-default-handler))))
117+
["" {:get {:handler api.systems/get-system}
118+
:put {:handler api.systems/update-system}
119+
:delete {:handler api.systems/delete-system}}]
120+
["/pdf" {:get {:handler api.systems/export-pdf}}]]]]])
90121

91-
(def html-middleware
92-
{:data {:middleware [wrap-params
93-
middleware/exception
94-
middleware/logging
95-
middleware/wrap-html-response]}})
122+
;; ===== Router Configuration =====
96123

97-
(def api-middleware
98-
{:data {:middleware [wrap-params
99-
middleware/exception
100-
middleware/logging
101-
[wrap-json-body {:keywords? true}]
102-
wrap-json-response
103-
middleware/wrap-jwt-authentication]
104-
:coercion reitit.coercion.spec/coercion}})
105-
106-
(defn app []
107-
(let [routes (if (config/dev?)
108-
(concat html-routes api-routes)
109-
html-routes)
110-
middleware (if (config/dev?)
111-
(-> html-middleware
112-
(update-in [:data :middleware] concat
113-
(get-in api-middleware [:data :middleware]))
114-
(assoc-in [:data :coercion]
115-
(get-in api-middleware [:data :coercion])))
116-
html-middleware)]
117-
(middleware/wrap-default-middlewares
118-
(create-handler routes middleware (config/dev?)))))
124+
(defn app
125+
"Constructs the application's handler function by combining HTML and API routes
126+
with their appropriate middleware configurations. The function:
127+
128+
1. Creates a single router with all routes, but preserves the middleware
129+
configurations for each route type using Reitit's route data mechanism
130+
131+
2. Wraps the router with the default application middlewares
132+
133+
This approach ensures proper routing while maintaining the distinct middleware
134+
needs of HTML and API routes."
135+
[]
136+
(->
137+
;; Create a single router with both HTML and API routes
138+
;; Each route maintains its specific middleware config through route data
139+
(ring/router
140+
(concat
141+
;; Apply html-middleware to all HTML routes
142+
(for [route html-routes]
143+
(let [[path data] route]
144+
[path (merge-with merge data (:data html-middleware))]))
145+
146+
;; Apply api-middleware to all API routes
147+
api-routes)
148+
;; Include the api-middleware configuration for API routes
149+
api-middleware)
150+
151+
;; Create handler with default not-found
152+
(ring/ring-handler (ring/create-default-handler))
153+
154+
;; Apply application-wide middleware
155+
middleware/wrap-default-middlewares))
119156

120157
(defn schedule-token-cleanup
121158
"Schedule periodic cleanup of expired refresh tokens using core.async"
@@ -140,22 +177,34 @@
140177
stop-ch))
141178

142179
(defn start-server
143-
"Starts the web server"
180+
"Starts the web server with the configured application handler.
181+
Returns a function that can be called to stop the server."
144182
[port]
145183
(mulog/log ::starting-server :port port)
146184
(server/run-server (app) {:port port}))
147185

148186
(defn -main
149-
"Entry point into the application via clojure.main -M"
187+
"Entry point into the application via clojure.main -M.
188+
Initializes the application, starts the server, and returns a shutdown function.
189+
190+
Arguments:
191+
- Optional first argument: port number (defaults to 3000)"
150192
[& args]
151193
(let [port (or (some-> (first args) Integer/parseInt) 3000)]
194+
;; Set up global logging context
152195
(mulog/set-global-context!
153196
{:app-name "Parts" :version "0.1.0-SNAPSHOT"})
154197
(mulog/log ::application-startup :arguments args :port port)
198+
199+
;; Initialize database
155200
(db/init-db)
201+
202+
;; Start server and background processes
156203
(let [stop-fn (start-server port)
157204
cleanup-stop-ch (schedule-token-cleanup)]
158205
(println "Parts: Server started on port" port)
206+
207+
;; Return shutdown function
159208
(fn []
160209
(stop-fn)
161210
(async/close! cleanup-stop-ch) ; Signal the cleanup process to stop

0 commit comments

Comments
 (0)