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 OAUTH: &str = "/v1/auth/oauth/{provider}";
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 const PLAYER_COLLECTION: &str = "/v1/users/{user}/storage/{collection}";
pub const PLAYER_OBJECT: &str = "/v1/users/{user}/storage/{collection}/{key}";
}
pub mod chat {
pub const ROOMS: &str = "/v1/chat/rooms";
pub const ROOMS_MINE: &str = "/v1/chat/rooms/mine";
pub const ROOMS_PUBLIC: &str = "/v1/chat/rooms/public";
pub const ROOM: &str = "/v1/chat/rooms/{room}";
pub const ROOM_JOIN: &str = "/v1/chat/rooms/{room}/join";
pub const ROOM_LEAVE: &str = "/v1/chat/rooms/{room}/leave";
pub const ROOM_MEMBERS: &str = "/v1/chat/rooms/{room}/members";
pub const ROOM_INVITES: &str = "/v1/chat/rooms/{room}/invites";
pub const ROOM_MEMBER: &str = "/v1/chat/rooms/{room}/members/{user}";
pub const ROOM_ROLE: &str = "/v1/chat/rooms/{room}/members/{user}/role";
pub const ROOM_OWNER: &str = "/v1/chat/rooms/{room}/owner";
pub const READ: &str = "/v1/chat/rooms/{room}/read";
pub const RECEIPTS: &str = "/v1/chat/rooms/{room}/receipts";
pub const UNREAD: &str = "/v1/chat/unread";
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 leaderboards {
pub const BOARDS: &str = "/v1/leaderboards";
pub const BOARD: &str = "/v1/leaderboards/{board}";
pub const SCORES: &str = "/v1/leaderboards/{board}/scores";
pub const ME: &str = "/v1/leaderboards/{board}/me";
pub const AROUND: &str = "/v1/leaderboards/{board}/around";
}
pub mod notifications {
pub const LIST: &str = "/v1/notifications";
pub const COUNT: &str = "/v1/notifications/count";
pub const MARK: &str = "/v1/notifications/mark";
pub const ONE: &str = "/v1/notifications/{id}";
}
pub mod friends {
pub const LIST: &str = "/v1/friends";
pub const ONE: &str = "/v1/friends/{user}";
pub const REQUESTS: &str = "/v1/friends/requests";
pub const REQUEST: &str = "/v1/friends/requests/{user}";
pub const ACCEPT: &str = "/v1/friends/requests/{user}/accept";
pub const DECLINE: &str = "/v1/friends/requests/{user}/decline";
pub const BLOCKS: &str = "/v1/friends/blocks";
pub const BLOCK: &str = "/v1/friends/blocks/{user}";
pub const CODE: &str = "/v1/friends/code";
pub const PRESENCE: &str = "/v1/friends/presence";
pub const STEAM: &str = "/v1/friends/steam";
pub const SETTINGS: &str = "/v1/friends/settings";
}
pub mod groups {
pub const LIST: &str = "/v1/groups";
pub const MINE: &str = "/v1/groups/mine";
pub const INVITES: &str = "/v1/groups/invites";
pub const ONE: &str = "/v1/groups/{group}";
pub const MEMBERS: &str = "/v1/groups/{group}/members";
pub const JOIN: &str = "/v1/groups/{group}/join";
pub const LEAVE: &str = "/v1/groups/{group}/leave";
pub const GROUP_INVITES: &str = "/v1/groups/{group}/invites";
pub const ACCEPT: &str = "/v1/groups/{group}/invites/accept";
pub const DECLINE: &str = "/v1/groups/{group}/invites/decline";
pub const INVITE: &str = "/v1/groups/{group}/invites/{user}";
pub const MEMBER: &str = "/v1/groups/{group}/members/{user}";
pub const ROLE: &str = "/v1/groups/{group}/members/{user}/role";
pub const TRANSFER: &str = "/v1/groups/{group}/transfer";
}
pub mod lobbies {
pub const LIST: &str = "/v1/lobbies";
pub const MINE: &str = "/v1/lobbies/mine";
pub const SEARCH: &str = "/v1/lobbies/search";
pub const JOIN_CODE: &str = "/v1/lobbies/join";
pub const ONE: &str = "/v1/lobbies/{lobby}";
pub const JOIN: &str = "/v1/lobbies/{lobby}/join";
pub const LEAVE: &str = "/v1/lobbies/{lobby}/leave";
pub const READY: &str = "/v1/lobbies/{lobby}/ready";
pub const CODE: &str = "/v1/lobbies/{lobby}/code";
pub const HOST: &str = "/v1/lobbies/{lobby}/host";
pub const MEMBER: &str = "/v1/lobbies/{lobby}/members/{user}";
}
pub mod matchmaking {
pub const QUEUES: &str = "/v1/matchmaking/queues";
pub const TICKET: &str = "/v1/matchmaking/ticket";
}
pub mod files {
pub const LIST: &str = "/v1/files";
pub const USAGE: &str = "/v1/files/usage";
pub const ONE: &str = "/v1/files/{file}";
pub const CONTENT: &str = "/v1/files/{file}/content";
}
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::OAUTH, 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, storage::PLAYER_COLLECTION, true),
route(HttpMethod::Get, storage::PLAYER_OBJECT, 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::Patch, chat::MESSAGE, true),
route(HttpMethod::Put, chat::READ, true),
route(HttpMethod::Get, chat::RECEIPTS, true),
route(HttpMethod::Post, chat::UNREAD, true),
route(HttpMethod::Post, chat::ROOMS, true),
route(HttpMethod::Get, chat::ROOMS_MINE, true),
route(HttpMethod::Get, chat::ROOMS_PUBLIC, true),
route(HttpMethod::Get, chat::ROOM, true),
route(HttpMethod::Patch, chat::ROOM, true),
route(HttpMethod::Delete, chat::ROOM, true),
route(HttpMethod::Post, chat::ROOM_JOIN, true),
route(HttpMethod::Post, chat::ROOM_LEAVE, true),
route(HttpMethod::Get, chat::ROOM_MEMBERS, true),
route(HttpMethod::Post, chat::ROOM_INVITES, true),
route(HttpMethod::Delete, chat::ROOM_MEMBER, true),
route(HttpMethod::Put, chat::ROOM_ROLE, true),
route(HttpMethod::Post, chat::ROOM_OWNER, true),
route(HttpMethod::Get, leaderboards::BOARDS, true),
route(HttpMethod::Get, leaderboards::BOARD, true),
route(HttpMethod::Post, leaderboards::SCORES, true),
route(HttpMethod::Get, leaderboards::ME, true),
route(HttpMethod::Get, leaderboards::AROUND, true),
route(HttpMethod::Get, notifications::LIST, true),
route(HttpMethod::Get, notifications::COUNT, true),
route(HttpMethod::Post, notifications::MARK, true),
route(HttpMethod::Delete, notifications::ONE, true),
route(HttpMethod::Get, friends::LIST, true),
route(HttpMethod::Delete, friends::ONE, true),
route(HttpMethod::Get, friends::REQUESTS, true),
route(HttpMethod::Post, friends::REQUESTS, true),
route(HttpMethod::Delete, friends::REQUEST, true),
route(HttpMethod::Post, friends::ACCEPT, true),
route(HttpMethod::Post, friends::DECLINE, true),
route(HttpMethod::Get, friends::BLOCKS, true),
route(HttpMethod::Put, friends::BLOCK, true),
route(HttpMethod::Delete, friends::BLOCK, true),
route(HttpMethod::Get, friends::CODE, true),
route(HttpMethod::Post, friends::CODE, true),
route(HttpMethod::Post, friends::PRESENCE, true),
route(HttpMethod::Post, friends::STEAM, true),
route(HttpMethod::Get, friends::SETTINGS, true),
route(HttpMethod::Put, friends::SETTINGS, true),
route(HttpMethod::Get, groups::LIST, true),
route(HttpMethod::Post, groups::LIST, true),
route(HttpMethod::Get, groups::MINE, true),
route(HttpMethod::Get, groups::INVITES, true),
route(HttpMethod::Get, groups::ONE, true),
route(HttpMethod::Patch, groups::ONE, true),
route(HttpMethod::Delete, groups::ONE, true),
route(HttpMethod::Get, groups::MEMBERS, true),
route(HttpMethod::Post, groups::JOIN, true),
route(HttpMethod::Post, groups::LEAVE, true),
route(HttpMethod::Post, groups::GROUP_INVITES, true),
route(HttpMethod::Post, groups::ACCEPT, true),
route(HttpMethod::Post, groups::DECLINE, true),
route(HttpMethod::Delete, groups::INVITE, true),
route(HttpMethod::Delete, groups::MEMBER, true),
route(HttpMethod::Put, groups::ROLE, true),
route(HttpMethod::Post, groups::TRANSFER, true),
route(HttpMethod::Post, lobbies::LIST, true),
route(HttpMethod::Get, lobbies::MINE, true),
route(HttpMethod::Post, lobbies::SEARCH, true),
route(HttpMethod::Post, lobbies::JOIN_CODE, true),
route(HttpMethod::Get, lobbies::ONE, true),
route(HttpMethod::Patch, lobbies::ONE, true),
route(HttpMethod::Post, lobbies::JOIN, true),
route(HttpMethod::Post, lobbies::LEAVE, true),
route(HttpMethod::Put, lobbies::READY, true),
route(HttpMethod::Post, lobbies::CODE, true),
route(HttpMethod::Post, lobbies::HOST, true),
route(HttpMethod::Delete, lobbies::MEMBER, true),
route(HttpMethod::Get, matchmaking::QUEUES, true),
route(HttpMethod::Post, matchmaking::TICKET, true),
route(HttpMethod::Get, matchmaking::TICKET, true),
route(HttpMethod::Delete, matchmaking::TICKET, 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),
route(HttpMethod::Get, files::LIST, true),
route(HttpMethod::Get, files::USAGE, true),
route(HttpMethod::Get, files::ONE, true),
route(HttpMethod::Patch, files::ONE, true),
route(HttpMethod::Delete, files::ONE, true),
];
pub const BINARY: &[Route] = &[route(HttpMethod::Post, files::LIST, true), route(HttpMethod::Get, files::CONTENT, true)];
pub fn file_path(file: crate::FileId) -> String {
format!("/v1/files/{file}")
}
pub fn file_content_path(file: crate::FileId) -> String {
format!("/v1/files/{file}/content")
}
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 oauth_login_path(provider: &str) -> Option<String> {
is_provider_name(provider).then(|| format!("/v1/auth/oauth/{provider}"))
}
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));
for r in BINARY {
assert!(r.path.starts_with("/v1/") && !ALL.contains(r), "{}", r.path);
}
assert_eq!(file_content_path(crate::FileId(12)), "/v1/files/12/content");
assert_eq!(file_path(crate::FileId(12)), "/v1/files/12");
}
#[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!(oauth_login_path("google").as_deref(), Some("/v1/auth/oauth/google"));
assert_eq!(oauth_login_path("../x"), None);
assert_eq!(admin_identity_path(user, "steam").as_deref(), Some("/v1/admin/users/42/identities/steam"));
assert_eq!(account_identity_path("../x"), None);
}
}