use std::fmt;
use crate::ids::RoomId;
pub const DEFAULT_BODY_LIMIT_BYTES: usize = 64 * 1024;
pub const PREFIX: &str = "/v1";
pub const WS: &str = "/v1/ws";
pub const INFO: &str = "/v1/info";
pub const HEALTH: &str = "/healthz";
pub const READY: &str = "/readyz";
pub mod auth {
pub const REGISTER: &str = "/v1/auth/register";
pub const LOGIN: &str = "/v1/auth/login";
pub const STEAM: &str = "/v1/auth/steam";
pub const REFRESH: &str = "/v1/auth/refresh";
pub const LOGOUT: &str = "/v1/auth/logout";
pub const VERIFY_EMAIL: &str = "/v1/auth/email/verify";
pub const RESEND_VERIFICATION: &str = "/v1/auth/email/resend";
pub const FORGOT_PASSWORD: &str = "/v1/auth/password/forgot";
pub const RESET_PASSWORD: &str = "/v1/auth/password/reset";
}
pub mod account {
pub const ME: &str = "/v1/account";
pub const PASSWORD: &str = "/v1/account/password";
pub const IDENTITY: &str = "/v1/account/identities/{provider}";
}
pub mod storage {
pub const COLLECTION: &str = "/v1/storage/{collection}";
pub const OBJECT: &str = "/v1/storage/{collection}/{key}";
pub const BATCH_GET: &str = "/v1/storage/_batch/get";
pub const BATCH_PUT: &str = "/v1/storage/_batch/put";
}
pub mod chat {
pub const ROOMS: &str = "/v1/chat/rooms";
pub const HISTORY: &str = "/v1/chat/rooms/{room}/messages";
pub const MESSAGE: &str = "/v1/chat/rooms/{room}/messages/{message}";
pub const DM: &str = "/v1/chat/dm";
pub const DMS: &str = "/v1/chat/dms";
}
pub mod admin {
pub const USERS: &str = "/v1/admin/users";
pub const USER: &str = "/v1/admin/users/{user}";
pub const BAN: &str = "/v1/admin/users/{user}/ban";
pub const UNBAN: &str = "/v1/admin/users/{user}/unban";
pub const SESSIONS: &str = "/v1/admin/users/{user}/sessions";
pub const IDENTITY: &str = "/v1/admin/users/{user}/identities/{provider}";
pub const ROLE: &str = "/v1/admin/users/{user}/roles/{role}";
pub const AUDIT: &str = "/v1/admin/audit";
pub const USER_STORAGE: &str = "/v1/admin/users/{user}/storage/{collection}";
pub const USER_OBJECT: &str = "/v1/admin/users/{user}/storage/{collection}/{key}";
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum HttpMethod {
Get,
Post,
Put,
Patch,
Delete,
}
impl HttpMethod {
pub const fn as_str(self) -> &'static str {
match self {
HttpMethod::Get => "GET",
HttpMethod::Post => "POST",
HttpMethod::Put => "PUT",
HttpMethod::Patch => "PATCH",
HttpMethod::Delete => "DELETE",
}
}
}
impl fmt::Display for HttpMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Route {
pub method: HttpMethod,
pub path: &'static str,
pub auth: bool,
}
impl Route {
pub const fn new(method: HttpMethod, path: &'static str, auth: bool) -> Self {
Route { method, path, auth }
}
}
const fn route(method: HttpMethod, path: &'static str, auth: bool) -> Route {
Route::new(method, path, auth)
}
pub const ALL: &[Route] = &[
route(HttpMethod::Get, INFO, false),
route(HttpMethod::Post, auth::REGISTER, false),
route(HttpMethod::Post, auth::LOGIN, false),
route(HttpMethod::Post, auth::STEAM, false),
route(HttpMethod::Post, auth::REFRESH, false),
route(HttpMethod::Post, auth::LOGOUT, false),
route(HttpMethod::Post, auth::VERIFY_EMAIL, false),
route(HttpMethod::Post, auth::RESEND_VERIFICATION, true),
route(HttpMethod::Post, auth::FORGOT_PASSWORD, false),
route(HttpMethod::Post, auth::RESET_PASSWORD, false),
route(HttpMethod::Get, account::ME, true),
route(HttpMethod::Patch, account::ME, true),
route(HttpMethod::Post, account::PASSWORD, true),
route(HttpMethod::Delete, account::IDENTITY, true),
route(HttpMethod::Get, storage::COLLECTION, true),
route(HttpMethod::Get, storage::OBJECT, true),
route(HttpMethod::Put, storage::OBJECT, true),
route(HttpMethod::Delete, storage::OBJECT, true),
route(HttpMethod::Post, storage::BATCH_GET, true),
route(HttpMethod::Post, storage::BATCH_PUT, true),
route(HttpMethod::Get, chat::ROOMS, true),
route(HttpMethod::Get, chat::HISTORY, true),
route(HttpMethod::Delete, chat::MESSAGE, true),
route(HttpMethod::Post, chat::DM, true),
route(HttpMethod::Get, chat::DMS, true),
route(HttpMethod::Get, admin::USERS, true),
route(HttpMethod::Get, admin::USER, true),
route(HttpMethod::Post, admin::BAN, true),
route(HttpMethod::Post, admin::UNBAN, true),
route(HttpMethod::Delete, admin::SESSIONS, true),
route(HttpMethod::Delete, admin::IDENTITY, true),
route(HttpMethod::Put, admin::ROLE, true),
route(HttpMethod::Delete, admin::ROLE, true),
route(HttpMethod::Get, admin::AUDIT, true),
route(HttpMethod::Get, admin::USER_STORAGE, true),
route(HttpMethod::Get, admin::USER_OBJECT, true),
route(HttpMethod::Put, admin::USER_OBJECT, true),
route(HttpMethod::Delete, admin::USER_OBJECT, true),
];
pub fn storage_object_path(collection: &str, key: &str) -> Option<String> {
(crate::storage::is_valid_name(collection) && crate::storage::is_valid_name(key)).then(|| format!("/v1/storage/{collection}/{key}"))
}
pub fn storage_collection_path(collection: &str) -> Option<String> {
crate::storage::is_valid_name(collection).then(|| format!("/v1/storage/{collection}"))
}
pub fn chat_history_path(room: RoomId) -> String {
format!("/v1/chat/rooms/{room}/messages")
}
pub fn chat_message_path(room: RoomId, message: crate::MessageId) -> String {
format!("/v1/chat/rooms/{room}/messages/{message}")
}
pub fn admin_storage_path(user: crate::UserId, collection: &str) -> Option<String> {
crate::storage::is_valid_name(collection).then(|| format!("/v1/admin/users/{user}/storage/{collection}"))
}
pub fn admin_object_path(user: crate::UserId, collection: &str, key: &str) -> Option<String> {
(crate::storage::is_valid_name(collection) && crate::storage::is_valid_name(key)).then(|| format!("/v1/admin/users/{user}/storage/{collection}/{key}"))
}
pub fn admin_user_path(user: crate::UserId) -> String {
format!("/v1/admin/users/{user}")
}
pub fn admin_ban_path(user: crate::UserId) -> String {
format!("/v1/admin/users/{user}/ban")
}
pub fn admin_unban_path(user: crate::UserId) -> String {
format!("/v1/admin/users/{user}/unban")
}
pub fn admin_sessions_path(user: crate::UserId) -> String {
format!("/v1/admin/users/{user}/sessions")
}
fn is_provider_name(provider: &str) -> bool {
provider.len() <= 32
&& provider.bytes().next().is_some_and(|b| b.is_ascii_lowercase())
&& provider.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
}
pub fn account_identity_path(provider: &str) -> Option<String> {
is_provider_name(provider).then(|| format!("/v1/account/identities/{provider}"))
}
pub fn admin_identity_path(user: crate::UserId, provider: &str) -> Option<String> {
is_provider_name(provider).then(|| format!("/v1/admin/users/{user}/identities/{provider}"))
}
pub fn admin_role_path(user: crate::UserId, role: &str) -> Option<String> {
crate::admin::is_valid_role(role).then(|| format!("/v1/admin/users/{user}/roles/{role}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn routes_are_versioned_and_unique() {
let mut seen = std::collections::HashSet::new();
for r in ALL {
assert!(r.path.starts_with("/v1/"), "{}", r.path);
assert!(seen.insert((r.method, r.path)), "duplicate {} {}", r.method, r.path);
}
assert!(WS.starts_with(PREFIX));
}
#[test]
fn path_helpers() {
assert_eq!(storage_object_path("saves", "slot-1").as_deref(), Some("/v1/storage/saves/slot-1"));
assert_eq!(storage_object_path("saves", "../x"), None);
assert_eq!(storage_object_path("_batch", "get"), None);
assert_eq!(storage_collection_path("saves").as_deref(), Some("/v1/storage/saves"));
assert_eq!(chat_history_path(RoomId(12)), "/v1/chat/rooms/12/messages");
assert_eq!(chat_message_path(RoomId(12), crate::MessageId(9)), "/v1/chat/rooms/12/messages/9");
assert_eq!(admin_storage_path(crate::UserId(4), "saves").as_deref(), Some("/v1/admin/users/4/storage/saves"));
assert_eq!(admin_object_path(crate::UserId(4), "saves", "a").as_deref(), Some("/v1/admin/users/4/storage/saves/a"));
assert_eq!(admin_object_path(crate::UserId(4), "saves", "../a"), None);
assert_eq!(HttpMethod::Patch.to_string(), "PATCH");
let user = crate::UserId(42);
assert_eq!(admin_user_path(user), "/v1/admin/users/42");
assert_eq!(admin_ban_path(user), "/v1/admin/users/42/ban");
assert_eq!(admin_unban_path(user), "/v1/admin/users/42/unban");
assert_eq!(admin_sessions_path(user), "/v1/admin/users/42/sessions");
assert_eq!(admin_role_path(user, "moderator").as_deref(), Some("/v1/admin/users/42/roles/moderator"));
assert_eq!(admin_role_path(user, "../x"), None);
assert_eq!(account_identity_path("steam").as_deref(), Some("/v1/account/identities/steam"));
assert_eq!(admin_identity_path(user, "steam").as_deref(), Some("/v1/admin/users/42/identities/steam"));
assert_eq!(account_identity_path("../x"), None);
}
}