1use parse_rust_core::ParseError;
15use serde_json::Value as Json;
16
17use crate::auth::Authority;
18use crate::params::Params;
19use crate::request::RequestContext;
20use crate::response::HttpError;
21use crate::routes::{classes, schemas, sessions, users};
22use crate::state::AppState;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum Route {
32 Health,
33 ServerInfo,
34 Users,
35 UsersMe,
36 Login,
37 Logout,
38 Classes {
39 class_name: String,
40 },
41 ClassObject {
42 class_name: String,
43 object_id: String,
44 },
45 Roles,
46 RoleObject {
47 object_id: String,
48 },
49 Sessions,
50 SessionsMe,
51 SessionObject {
52 object_id: String,
53 },
54 Schemas,
55 SchemaClass {
56 class_name: String,
57 },
58 Purge {
59 class_name: String,
60 },
61 Batch,
62}
63
64pub struct RouteResponse {
66 pub status: http::StatusCode,
67 pub body: Json,
68}
69
70impl RouteResponse {
71 fn ok(body: Json) -> Self {
72 Self {
73 status: http::StatusCode::OK,
74 body,
75 }
76 }
77
78 fn created(body: Json) -> Self {
79 Self {
80 status: http::StatusCode::CREATED,
81 body,
82 }
83 }
84}
85
86pub enum RouteError {
93 Parse(ParseError),
94 Http(HttpError),
95 NotFound {
103 method: http::Method,
104 path: String,
105 },
106}
107
108impl From<ParseError> for RouteError {
109 fn from(e: ParseError) -> Self {
110 RouteError::Parse(e)
111 }
112}
113
114pub fn route_of(path: &str) -> Option<Route> {
122 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
123 Some(match segments.as_slice() {
124 ["health"] => Route::Health,
125 ["serverInfo"] => Route::ServerInfo,
126 ["batch"] => Route::Batch,
127
128 ["users"] => Route::Users,
129 ["users", "me"] => Route::UsersMe,
130 ["login"] => Route::Login,
131 ["logout"] => Route::Logout,
132
133 ["classes", class_name] => Route::Classes {
134 class_name: (*class_name).to_string(),
135 },
136 ["classes", class_name, object_id] => Route::ClassObject {
137 class_name: (*class_name).to_string(),
138 object_id: (*object_id).to_string(),
139 },
140
141 ["roles"] => Route::Roles,
142 ["roles", object_id] => Route::RoleObject {
143 object_id: (*object_id).to_string(),
144 },
145
146 ["sessions"] => Route::Sessions,
147 ["sessions", "me"] => Route::SessionsMe,
148 ["sessions", object_id] => Route::SessionObject {
149 object_id: (*object_id).to_string(),
150 },
151
152 ["schemas"] => Route::Schemas,
153 ["schemas", class_name] => Route::SchemaClass {
154 class_name: (*class_name).to_string(),
155 },
156 ["purge", class_name] => Route::Purge {
157 class_name: (*class_name).to_string(),
158 },
159
160 _ => return None,
161 })
162}
163
164pub struct Incoming {
169 pub method: http::Method,
170 pub route: Route,
171 pub path: String,
174 pub params: Params,
175 pub body: Option<Json>,
176}
177
178pub async fn dispatch(
180 state: &AppState,
181 rc: &RequestContext,
182 authority: &Authority,
183 incoming: &Incoming,
184) -> Result<RouteResponse, RouteError> {
185 use http::Method as M;
186
187 let Incoming {
188 method,
189 route,
190 path,
191 params,
192 body,
193 } = incoming;
194 let path = path.as_str();
195
196 let body = || -> Result<&Json, RouteError> {
200 body.as_ref().ok_or_else(|| {
201 RouteError::Parse(ParseError::invalid_json("body must be a JSON object"))
202 })
203 };
204
205 let response = match (route, method) {
206 (Route::Health, &M::GET | &M::POST) => RouteResponse::ok(crate::routes::health::body()),
207
208 (Route::ServerInfo, &M::GET) => {
209 master_only(state, authority)?;
210 RouteResponse::ok(crate::routes::features::server_info_body(state.config()))
211 }
212
213 (Route::Users, &M::POST) => {
214 RouteResponse::created(users::signup_core(state, rc, authority, body()?).await?)
215 }
216 (Route::UsersMe, &M::GET) => RouteResponse::ok(users::me_core(state, rc).await?),
217 (Route::Login, &M::POST) => {
218 RouteResponse::ok(users::login_core(state, rc, authority, body()?).await?)
219 }
220 (Route::Logout, &M::POST) => RouteResponse::ok(users::logout_core(state, rc).await?),
221
222 (Route::Classes { class_name }, &M::GET) => {
223 RouteResponse::ok(classes::find_core(state, rc, authority, class_name, params).await?)
224 }
225 (Route::Classes { class_name }, &M::POST) => RouteResponse::created(
226 classes::create_core(state, rc, authority, class_name, body()?).await?,
227 ),
228 (
229 Route::ClassObject {
230 class_name,
231 object_id,
232 },
233 &M::GET,
234 ) => RouteResponse::ok(
235 classes::get_core(state, rc, authority, class_name, object_id, params).await?,
236 ),
237 (
238 Route::ClassObject {
239 class_name,
240 object_id,
241 },
242 &M::PUT,
243 ) => RouteResponse::ok(
244 classes::update_core(state, rc, authority, class_name, object_id, body()?).await?,
245 ),
246 (
247 Route::ClassObject {
248 class_name,
249 object_id,
250 },
251 &M::DELETE,
252 ) => RouteResponse::ok(
253 classes::delete_core(state, rc, authority, class_name, object_id).await?,
254 ),
255
256 (Route::Roles, &M::GET) => {
261 RouteResponse::ok(classes::find_core(state, rc, authority, ROLE_CLASS, params).await?)
262 }
263 (Route::Roles, &M::POST) => RouteResponse::created(
264 classes::create_core(state, rc, authority, ROLE_CLASS, body()?).await?,
265 ),
266 (Route::RoleObject { object_id }, &M::GET) => RouteResponse::ok(
267 classes::get_core(state, rc, authority, ROLE_CLASS, object_id, params).await?,
268 ),
269 (Route::RoleObject { object_id }, &M::PUT) => RouteResponse::ok(
270 classes::update_core(state, rc, authority, ROLE_CLASS, object_id, body()?).await?,
271 ),
272 (Route::RoleObject { object_id }, &M::DELETE) => RouteResponse::ok(
273 classes::delete_core(state, rc, authority, ROLE_CLASS, object_id).await?,
274 ),
275
276 (Route::SessionsMe, &M::GET) => RouteResponse::ok(sessions::me_core(state, rc).await?),
277 (Route::Sessions, &M::GET) => RouteResponse::ok(
278 classes::find_core(state, rc, authority, classes::SESSION_CLASS, params).await?,
279 ),
280 (Route::SessionObject { object_id }, &M::GET) => RouteResponse::ok(
281 classes::get_core(
282 state,
283 rc,
284 authority,
285 classes::SESSION_CLASS,
286 object_id,
287 params,
288 )
289 .await?,
290 ),
291 (Route::SessionObject { object_id }, &M::DELETE) => {
292 RouteResponse::ok(classes::delete_session_core(state, rc, object_id).await?)
293 }
294
295 (Route::Schemas, &M::GET) => {
296 master_only(state, authority)?;
297 RouteResponse::ok(schemas::get_all(state).await?)
298 }
299 (Route::Schemas, &M::POST) => {
300 master_only(state, authority)?;
301 RouteResponse::ok(schemas::create(state, path, None, body()?).await?)
302 }
303 (Route::SchemaClass { class_name }, &M::GET) => {
304 master_only(state, authority)?;
305 RouteResponse::ok(schemas::get_one(state, class_name).await?)
306 }
307 (Route::SchemaClass { class_name }, &M::POST) => {
308 master_only(state, authority)?;
309 RouteResponse::ok(schemas::create(state, path, Some(class_name), body()?).await?)
310 }
311 (Route::SchemaClass { class_name }, &M::PUT) => {
312 master_only(state, authority)?;
313 RouteResponse::ok(schemas::update(state, class_name, body()?).await?)
314 }
315 (Route::SchemaClass { class_name }, &M::DELETE) => {
316 master_only(state, authority)?;
317 RouteResponse::ok(schemas::delete(state, class_name).await?)
318 }
319 (Route::Purge { class_name }, &M::DELETE) => {
320 master_only(state, authority)?;
321 RouteResponse::ok(schemas::purge(state, class_name).await?)
322 }
323
324 _ => return Err(unroutable(method, path)),
327 };
328 Ok(response)
329}
330
331const ROLE_CLASS: &str = "_Role";
333
334fn unroutable(method: &http::Method, path: &str) -> RouteError {
335 RouteError::NotFound {
336 method: method.clone(),
337 path: path.to_string(),
338 }
339}
340
341fn master_only(state: &AppState, authority: &Authority) -> Result<(), RouteError> {
344 if authority.is_master() {
345 return Ok(());
346 }
347 Err(RouteError::Http(HttpError::master_key_required(
348 state.config().error_detail(),
349 )))
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn the_literal_me_routes_win_over_the_parameterized_ones() {
358 assert_eq!(route_of("/users/me"), Some(Route::UsersMe));
362 assert_eq!(route_of("/sessions/me"), Some(Route::SessionsMe));
363 assert_eq!(
364 route_of("/sessions/abc123"),
365 Some(Route::SessionObject {
366 object_id: "abc123".into()
367 })
368 );
369 }
370
371 #[test]
372 fn class_and_object_paths_are_distinguished_by_depth() {
373 assert_eq!(
374 route_of("/classes/Post"),
375 Some(Route::Classes {
376 class_name: "Post".into()
377 })
378 );
379 assert_eq!(
380 route_of("/classes/Post/abc"),
381 Some(Route::ClassObject {
382 class_name: "Post".into(),
383 object_id: "abc".into()
384 })
385 );
386 }
387
388 #[test]
389 fn everything_outside_the_milestone_surface_is_unroutable() {
390 for path in [
391 "/upgradeToRevocableSession",
392 "/functions/foo",
393 "/config",
394 "/hooks/functions",
395 "/aggregate/Post",
396 "/classes",
397 "/classes/Post/a/b",
398 "",
399 "/",
400 ] {
401 assert!(route_of(path).is_none(), "{path} must not route");
402 }
403 }
404
405 #[test]
406 fn a_leading_and_trailing_slash_do_not_change_the_match() {
407 assert_eq!(route_of("/schemas/"), Some(Route::Schemas));
408 assert_eq!(
409 route_of("/purge/Post"),
410 Some(Route::Purge {
411 class_name: "Post".into()
412 })
413 );
414 }
415}