use parse_rust_core::ParseError;
use serde_json::Value as Json;
use crate::auth::Authority;
use crate::params::Params;
use crate::request::RequestContext;
use crate::response::HttpError;
use crate::routes::{classes, schemas, sessions, users};
use crate::state::AppState;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Route {
Health,
ServerInfo,
Users,
UsersMe,
Login,
Logout,
Classes {
class_name: String,
},
ClassObject {
class_name: String,
object_id: String,
},
Roles,
RoleObject {
object_id: String,
},
Sessions,
SessionsMe,
SessionObject {
object_id: String,
},
Schemas,
SchemaClass {
class_name: String,
},
Purge {
class_name: String,
},
Batch,
}
pub struct RouteResponse {
pub status: http::StatusCode,
pub body: Json,
}
impl RouteResponse {
fn ok(body: Json) -> Self {
Self {
status: http::StatusCode::OK,
body,
}
}
fn created(body: Json) -> Self {
Self {
status: http::StatusCode::CREATED,
body,
}
}
}
pub enum RouteError {
Parse(ParseError),
Http(HttpError),
NotFound {
method: http::Method,
path: String,
},
}
impl From<ParseError> for RouteError {
fn from(e: ParseError) -> Self {
RouteError::Parse(e)
}
}
pub fn route_of(path: &str) -> Option<Route> {
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
Some(match segments.as_slice() {
["health"] => Route::Health,
["serverInfo"] => Route::ServerInfo,
["batch"] => Route::Batch,
["users"] => Route::Users,
["users", "me"] => Route::UsersMe,
["login"] => Route::Login,
["logout"] => Route::Logout,
["classes", class_name] => Route::Classes {
class_name: (*class_name).to_string(),
},
["classes", class_name, object_id] => Route::ClassObject {
class_name: (*class_name).to_string(),
object_id: (*object_id).to_string(),
},
["roles"] => Route::Roles,
["roles", object_id] => Route::RoleObject {
object_id: (*object_id).to_string(),
},
["sessions"] => Route::Sessions,
["sessions", "me"] => Route::SessionsMe,
["sessions", object_id] => Route::SessionObject {
object_id: (*object_id).to_string(),
},
["schemas"] => Route::Schemas,
["schemas", class_name] => Route::SchemaClass {
class_name: (*class_name).to_string(),
},
["purge", class_name] => Route::Purge {
class_name: (*class_name).to_string(),
},
_ => return None,
})
}
pub struct Incoming {
pub method: http::Method,
pub route: Route,
pub path: String,
pub params: Params,
pub body: Option<Json>,
}
pub async fn dispatch(
state: &AppState,
rc: &RequestContext,
authority: &Authority,
incoming: &Incoming,
) -> Result<RouteResponse, RouteError> {
use http::Method as M;
let Incoming {
method,
route,
path,
params,
body,
} = incoming;
let path = path.as_str();
let body = || -> Result<&Json, RouteError> {
body.as_ref().ok_or_else(|| {
RouteError::Parse(ParseError::invalid_json("body must be a JSON object"))
})
};
let response = match (route, method) {
(Route::Health, &M::GET | &M::POST) => RouteResponse::ok(crate::routes::health::body()),
(Route::ServerInfo, &M::GET) => {
master_only(state, authority)?;
RouteResponse::ok(crate::routes::features::server_info_body(state.config()))
}
(Route::Users, &M::POST) => {
RouteResponse::created(users::signup_core(state, rc, authority, body()?).await?)
}
(Route::UsersMe, &M::GET) => RouteResponse::ok(users::me_core(state, rc).await?),
(Route::Login, &M::POST) => {
RouteResponse::ok(users::login_core(state, rc, authority, body()?).await?)
}
(Route::Logout, &M::POST) => RouteResponse::ok(users::logout_core(state, rc).await?),
(Route::Classes { class_name }, &M::GET) => {
RouteResponse::ok(classes::find_core(state, rc, authority, class_name, params).await?)
}
(Route::Classes { class_name }, &M::POST) => RouteResponse::created(
classes::create_core(state, rc, authority, class_name, body()?).await?,
),
(
Route::ClassObject {
class_name,
object_id,
},
&M::GET,
) => RouteResponse::ok(
classes::get_core(state, rc, authority, class_name, object_id, params).await?,
),
(
Route::ClassObject {
class_name,
object_id,
},
&M::PUT,
) => RouteResponse::ok(
classes::update_core(state, rc, authority, class_name, object_id, body()?).await?,
),
(
Route::ClassObject {
class_name,
object_id,
},
&M::DELETE,
) => RouteResponse::ok(
classes::delete_core(state, rc, authority, class_name, object_id).await?,
),
(Route::Roles, &M::GET) => {
RouteResponse::ok(classes::find_core(state, rc, authority, ROLE_CLASS, params).await?)
}
(Route::Roles, &M::POST) => RouteResponse::created(
classes::create_core(state, rc, authority, ROLE_CLASS, body()?).await?,
),
(Route::RoleObject { object_id }, &M::GET) => RouteResponse::ok(
classes::get_core(state, rc, authority, ROLE_CLASS, object_id, params).await?,
),
(Route::RoleObject { object_id }, &M::PUT) => RouteResponse::ok(
classes::update_core(state, rc, authority, ROLE_CLASS, object_id, body()?).await?,
),
(Route::RoleObject { object_id }, &M::DELETE) => RouteResponse::ok(
classes::delete_core(state, rc, authority, ROLE_CLASS, object_id).await?,
),
(Route::SessionsMe, &M::GET) => RouteResponse::ok(sessions::me_core(state, rc).await?),
(Route::Sessions, &M::GET) => RouteResponse::ok(
classes::find_core(state, rc, authority, classes::SESSION_CLASS, params).await?,
),
(Route::SessionObject { object_id }, &M::GET) => RouteResponse::ok(
classes::get_core(
state,
rc,
authority,
classes::SESSION_CLASS,
object_id,
params,
)
.await?,
),
(Route::SessionObject { object_id }, &M::DELETE) => {
RouteResponse::ok(classes::delete_session_core(state, rc, object_id).await?)
}
(Route::Schemas, &M::GET) => {
master_only(state, authority)?;
RouteResponse::ok(schemas::get_all(state).await?)
}
(Route::Schemas, &M::POST) => {
master_only(state, authority)?;
RouteResponse::ok(schemas::create(state, path, None, body()?).await?)
}
(Route::SchemaClass { class_name }, &M::GET) => {
master_only(state, authority)?;
RouteResponse::ok(schemas::get_one(state, class_name).await?)
}
(Route::SchemaClass { class_name }, &M::POST) => {
master_only(state, authority)?;
RouteResponse::ok(schemas::create(state, path, Some(class_name), body()?).await?)
}
(Route::SchemaClass { class_name }, &M::PUT) => {
master_only(state, authority)?;
RouteResponse::ok(schemas::update(state, class_name, body()?).await?)
}
(Route::SchemaClass { class_name }, &M::DELETE) => {
master_only(state, authority)?;
RouteResponse::ok(schemas::delete(state, class_name).await?)
}
(Route::Purge { class_name }, &M::DELETE) => {
master_only(state, authority)?;
RouteResponse::ok(schemas::purge(state, class_name).await?)
}
_ => return Err(unroutable(method, path)),
};
Ok(response)
}
const ROLE_CLASS: &str = "_Role";
fn unroutable(method: &http::Method, path: &str) -> RouteError {
RouteError::NotFound {
method: method.clone(),
path: path.to_string(),
}
}
fn master_only(state: &AppState, authority: &Authority) -> Result<(), RouteError> {
if authority.is_master() {
return Ok(());
}
Err(RouteError::Http(HttpError::master_key_required(
state.config().error_detail(),
)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_literal_me_routes_win_over_the_parameterized_ones() {
assert_eq!(route_of("/users/me"), Some(Route::UsersMe));
assert_eq!(route_of("/sessions/me"), Some(Route::SessionsMe));
assert_eq!(
route_of("/sessions/abc123"),
Some(Route::SessionObject {
object_id: "abc123".into()
})
);
}
#[test]
fn class_and_object_paths_are_distinguished_by_depth() {
assert_eq!(
route_of("/classes/Post"),
Some(Route::Classes {
class_name: "Post".into()
})
);
assert_eq!(
route_of("/classes/Post/abc"),
Some(Route::ClassObject {
class_name: "Post".into(),
object_id: "abc".into()
})
);
}
#[test]
fn everything_outside_the_milestone_surface_is_unroutable() {
for path in [
"/upgradeToRevocableSession",
"/functions/foo",
"/config",
"/hooks/functions",
"/aggregate/Post",
"/classes",
"/classes/Post/a/b",
"",
"/",
] {
assert!(route_of(path).is_none(), "{path} must not route");
}
}
#[test]
fn a_leading_and_trailing_slash_do_not_change_the_match() {
assert_eq!(route_of("/schemas/"), Some(Route::Schemas));
assert_eq!(
route_of("/purge/Post"),
Some(Route::Purge {
class_name: "Post".into()
})
);
}
}