diff --git a/Cargo.lock b/Cargo.lock index 84503ec..4574dac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,6 +155,7 @@ version = "0.1.0" dependencies = [ "bcrypt", "chrono", + "dashmap", "data-encoding", "env_logger", "futures", @@ -396,6 +397,20 @@ dependencies = [ "typenum", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.6.0" diff --git a/Cargo.toml b/Cargo.toml index 762fa02..2666f07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,4 +23,8 @@ sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-native-tls", "d maud = "0.26.0" rand = "0.8.5" data-encoding = "2.6.0" -bcrypt = "0.16.0" \ No newline at end of file +bcrypt = "0.16.0" +dashmap = "6.1.0" + +[features] +cache = [] diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..4b883c5 --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,45 @@ +use data_encoding::HEXUPPER; +use rand::RngCore; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; + +mod user; +pub use user::User; + +fn gen_token(token_length: usize) -> String { + let mut token_bytes = vec![0u8; token_length]; + + rand::thread_rng().fill_bytes(&mut token_bytes); + + HEXUPPER.encode(&token_bytes) +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Session { + /// The unique ID of the session token + pub id: uuid::Uuid, + /// The generated session token + pub token: String, + /// The username associated with the session token + pub user: String, +} + +/// A macro to check if a user has admin privileges. +/// +/// This macro checks whether the provided user has admin privileges by calling the `is_admin` method on it. +/// If the user is not an admin, it returns a `Forbidden` error with a message indicating the restriction. +/// +/// # Arguments +/// * `$u` - The user to check. +/// +/// # Returns +/// The macro does not return a value directly but controls the flow of execution. If the user is not an admin, +/// it returns a `Forbidden` error immediately and prevents further execution. +#[macro_export] +macro_rules! check_admin { + ($u:ident) => { + if !$u.is_admin() { + return Err(api_error("Forbidden")); + } + }; +} diff --git a/src/user/mod.rs b/src/auth/user.rs similarity index 82% rename from src/user/mod.rs rename to src/auth/user.rs index 96cda11..08b2b12 100644 --- a/src/user/mod.rs +++ b/src/auth/user.rs @@ -1,20 +1,11 @@ -use data_encoding::HEXUPPER; -use rand::RngCore; -use rocket::{http::Status, outcome::Outcome, request::FromRequest, Request}; +use rocket::{Request, http::Status, outcome::Outcome, request::FromRequest}; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::FromRow; +use super::{Session, gen_token}; use crate::{get_pg, request::api::ToAPI}; -fn gen_token(token_length: usize) -> String { - let mut token_bytes = vec![0u8; token_length]; - - rand::thread_rng().fill_bytes(&mut token_bytes); - - HEXUPPER.encode(&token_bytes) -} - #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct User { /// The username chosen by the user @@ -35,6 +26,11 @@ pub enum UserRole { } impl User { + // Get a user from session ID + pub async fn from_session(session: &str) -> Option { + sqlx::query_as("SELECT * FROM users WHERE username = (SELECT \"user\" FROM user_session WHERE token = $1)").bind(session).fetch_optional(get_pg!()).await.unwrap() + } + /// Find a user by their username pub async fn find(username: &str) -> Option { sqlx::query_as("SELECT * FROM users WHERE username = $1") @@ -143,34 +139,22 @@ impl ToAPI for User { } } -#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] -pub struct Session { - /// The unique ID of the session token - pub id: uuid::Uuid, - /// The generated session token - pub token: String, - /// The username associated with the session token - pub user: String, -} - -#[macro_export] -macro_rules! check_admin { - ($u:ident) => { - if !$u.is_admin() { - return Err(api_error("Forbidden")); - } - }; -} - #[rocket::async_trait] impl<'r> FromRequest<'r> for User { type Error = (); async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome { - // todo : cookie auth + if let Some(session_id) = request.cookies().get("session_id") { + if let Some(user) = User::from_session(session_id.value()).await { + return Outcome::Success(user); + } else { + return Outcome::Error((Status::Unauthorized, ())); + } + } + match request.headers().get_one("token") { Some(key) => { - if let Some(user) = sqlx::query_as("SELECT * FROM users WHERE username = (SELECT \"user\" FROM user_session WHERE token = $1)").bind(key).fetch_optional(get_pg!()).await.unwrap() { + if let Some(user) = User::from_session(key).await { Outcome::Success(user) } else { Outcome::Error((Status::Unauthorized, ())) @@ -180,4 +164,3 @@ impl<'r> FromRequest<'r> for User { } } } - diff --git a/src/format.rs b/src/format.rs index f995d16..68177a6 100644 --- a/src/format.rs +++ b/src/format.rs @@ -1,17 +1,72 @@ - - - +/// Formats a `chrono::NaiveDate` into a representable `String`. +/// +/// This function converts a `NaiveDate` object into a human-readable string representation. +/// +/// # Arguments +/// * `date` - A reference to a `chrono::NaiveDate` instance. +/// +/// # Returns +/// A `String` representation of the date. +/// +/// # Example +/// ``` +/// use based::format::format_date; +/// +/// let date = chrono::NaiveDate::from_ymd(2023, 12, 18); +/// let formatted = format_date(&date); +/// assert_eq!(formatted, "2023-12-18"); +/// ``` pub fn format_date(date: &chrono::NaiveDate) -> String { - // TODO : Implement + // TODO : Implement custom formatting date.to_string() } +/// Formats an integer into a representable `String`. +/// +/// # Arguments +/// * `num` - An integer to format. +/// +/// # Returns +/// A `String` representation of the number. +/// +/// # Example +/// ``` +/// use based::format::format_number; +/// +/// let number = 12345; +/// let formatted = format_number(number); +/// assert_eq!(formatted, "12345"); +/// ``` pub fn format_number(num: i32) -> String { - // TODO : Implement + // TODO : Implement custom formatting num.to_string() } -fn format_seconds_to_hhmmss(seconds: f64) -> String { +/// Converts a number of seconds into a formatted string in `HH:MM:SS` or `MM:SS` format. +/// +/// This function takes a floating-point number representing seconds and formats it +/// into a human-readable time string. If the duration is less than one hour, the +/// output is in `MM:SS` format. Otherwise, it is in `HH:MM:SS` format. +/// +/// # Arguments +/// * `seconds` - A floating-point number representing the duration in seconds. +/// +/// # Returns +/// A `String` formatted as `HH:MM:SS` or `MM:SS` depending on the input value. +/// +/// # Example +/// ``` +/// use based::format::format_seconds_to_hhmmss; +/// +/// let duration = 3661.5; // 1 hour, 1 minute, and 1.5 seconds +/// let formatted = format_seconds_to_hhmmss(duration); +/// assert_eq!(formatted, "01:01:01"); +/// +/// let short_duration = 59.9; // Less than a minute +/// let formatted = format_seconds_to_hhmmss(short_duration); +/// assert_eq!(formatted, "00:59"); +/// ``` +pub fn format_seconds_to_hhmmss(seconds: f64) -> String { let total_seconds = seconds as u64; let hours = total_seconds / 3600; let minutes = (total_seconds % 3600) / 60; @@ -21,4 +76,4 @@ fn format_seconds_to_hhmmss(seconds: f64) -> String { } else { format!("{:02}:{:02}", minutes, seconds) } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 7c6c48a..a028ecb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,26 @@ use tokio::sync::OnceCell; -pub mod result; -pub mod request; -pub mod user; +pub mod auth; +pub mod format; pub mod page; +pub mod request; +pub mod result; // Postgres pub static PG: OnceCell = OnceCell::const_new(); +/// A macro to retrieve or initialize the PostgreSQL connection pool. +/// +/// This macro provides a convenient way to access the `PgPool`. If the pool is not already initialized, +/// it creates a new pool using the connection string from the `$DATABASE_URL` environment variable. +/// +/// # Example +/// ```ignore +/// use based::get_pg; +/// +/// let pool = get_pg!(); +/// ``` #[macro_export] macro_rules! get_pg { () => { diff --git a/src/page/mod.rs b/src/page/mod.rs index 2d9d0ee..2ada00d 100644 --- a/src/page/mod.rs +++ b/src/page/mod.rs @@ -1,14 +1,124 @@ -use core::num; +use maud::{PreEscaped, html}; -use maud::{html, PreEscaped}; +use crate::request::{RequestContext, StringResponse}; -use crate::{request::context::RequestContext, user::User}; +use rocket::http::{ContentType, Status}; -use rocket::{ - http::{ContentType, Status}, - request::{self, FromRequest, Request}, -}; +/// Represents the HTML structure of a page shell, including the head, body class, and body content. +/// +/// This structure is used to construct the overall HTML structure of a page, making it easier to generate consistent HTML pages dynamically. +pub struct Shell { + /// The HTML content for the `` section of the page. + head: PreEscaped, + /// An optional class attribute for the `` element. + body_class: Option, + /// The HTML content for the static body portion. + body_content: PreEscaped, +} +impl Shell { + /// Constructs a new `Shell` instance with the given head content, body content, and body class. + /// + /// # Arguments + /// * `head` - The HTML content for the page's head. + /// * `body_content` - The HTML content for the body of the page. + /// * `body_class` - An optional class to apply to the `` element. + /// + /// # Returns + /// A `Shell` instance encapsulating the provided HTML content and attributes. + pub fn new( + head: PreEscaped, + body_content: PreEscaped, + body_class: Option, + ) -> Self { + Self { + head, + body_class, + body_content, + } + } + + /// Renders the full HTML page using the shell structure, with additional content and a title. + /// + /// # Arguments + /// * `content` - The additional HTML content to render inside the main content div. + /// * `title` - The title of the page, rendered inside the `` element. + /// + /// # Returns + /// A `PreEscaped<String>` containing the full HTML page content. + pub fn render(&self, content: PreEscaped<String>, title: &str) -> PreEscaped<String> { + html! { + html { + head { + title { (title) }; + (self.head) + }; + @if self.body_class.is_some() { + body class=(self.body_class.as_ref().unwrap()) { + (self.body_content); + + div id="main_content" { + (content) + }; + }; + } @else { + body { + (self.body_content); + + div id="main_content" { + (content) + }; + }; + } + } + } + } +} + +/// Renders a full page or an HTMX-compatible fragment based on the request context. +/// +/// If the request is not an HTMX request, this function uses the provided shell to generate +/// a full HTML page. If it is an HTMX request, only the provided content is rendered. +/// +/// # Arguments +/// * `content` - The HTML content to render. +/// * `title` - The title of the page for full-page rendering. +/// * `ctx` - The `RequestContext` containing request metadata. +/// * `shell` - The `Shell` instance used for full-page rendering. +/// +/// # Returns +/// A `StringResponse` +pub async fn render_page( + content: PreEscaped<String>, + title: &str, + ctx: RequestContext, + shell: &Shell, +) -> StringResponse { + if !ctx.is_htmx { + ( + Status::Ok, + ( + ContentType::HTML, + shell.render(content, title).into_string(), + ), + ) + } else { + (Status::Ok, (ContentType::HTML, content.into_string())) + } +} + +/// Generates an HTML link with HTMX attributes for dynamic behavior. +/// +/// This function creates an `<a>` element with attributes that enable HTMX behavior for navigation without reload. +/// +/// # Arguments +/// * `url` - The URL to link to. +/// * `class` - The CSS class for styling the link. +/// * `onclick` - The JavaScript `onclick` handler for the link. +/// * `content` - The content inside the link element. +/// +/// # Returns +/// A `PreEscaped<String>` containing the rendered HTML link element. pub fn htmx_link( url: &str, class: &str, @@ -22,6 +132,16 @@ pub fn htmx_link( ) } +/// Generates a `<script>` element containing the provided JavaScript code. +/// +/// This function wraps the provided JavaScript code in a `<script>` tag, +/// allowing for easy inclusion of custom scripts in the rendered HTML. +/// +/// # Arguments +/// * `script` - The JavaScript code to include. +/// +/// # Returns +/// A `PreEscaped<String>` containing the rendered `<script>` element. pub fn script(script: &str) -> PreEscaped<String> { html!( script { @@ -29,56 +149,3 @@ pub fn script(script: &str) -> PreEscaped<String> { }; ) } - -pub fn shell(content: PreEscaped<String>, title: &str, user: Option<User>) -> PreEscaped<String> { - html! { - html { - head { - title { (title) }; - script src="https://cdn.tailwindcss.com" {}; - script src="https://unpkg.com/htmx.org@2.0.3" integrity="sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq" crossorigin="anonymous" {}; - meta name="viewport" content="width=device-width, initial-scale=1.0"; - }; - body class="bg-black text-white" { - header class="bg-gray-800 text-white shadow-md py-2" { - - div class="flex justify-between px-6" { - - a href="/" class="flex items-center space-x-2" { - img src="/favicon" alt="Logo" class="w-10 h-10 rounded-md"; - span class="font-semibold text-xl" { "WatchDogs" }; - }; - - @if user.is_some() { - p { (user.unwrap().username) }; - }; - - }; - - }; - }; - - div id="main_content" { - (content) - }; - }; - } -} - -pub async fn render_page( - htmx: RequestContext, - content: PreEscaped<String>, - title: &str, - user: Option<User>, -) -> (Status, (ContentType, String)) { - if !htmx.is_htmx { - ( - Status::Ok, - (ContentType::HTML, shell(content, title, user).into_string()), - ) - } else { - (Status::Ok, (ContentType::HTML, content.into_string())) - } -} - - diff --git a/src/request/api.rs b/src/request/api.rs index f7cf478..f2c26a2 100644 --- a/src/request/api.rs +++ b/src/request/api.rs @@ -1,12 +1,36 @@ -use rocket::http::{ContentType, Status}; +use rocket::response::status::BadRequest; +use serde_json::json; +use std::str::FromStr; + +/// API error response with a JSON payload. +pub type ApiError = BadRequest<serde_json::Value>; + +/// Fallible API response. +/// +/// This type represents the result of an API operation that can either succeed with a +/// JSON payload (`serde_json::Value`) or fail with an `ApiError`. +pub type FallibleApiResponse = Result<serde_json::Value, ApiError>; /// A trait to generate a Model API representation in JSON format. +/// +/// This trait is intended for data models that need to be serialized +/// into a JSON representation suitable for public APIs. pub trait ToAPI: Sized { - /// Generate public API JSON + /// Generate a JSON representation of the model for public API use. fn api(&self) -> impl std::future::Future<Output = serde_json::Value>; } /// Converts a slice of items implementing the `ToAPI` trait into a `Vec` of JSON values. +/// +/// This function is asynchronous and iterates over a slice of items, calling the `api` +/// method on each to generate its JSON representation. The results are collected into +/// a `Vec<serde_json::Value>`. +/// +/// # Arguments +/// * `items` - A slice of items implementing the `ToAPI` trait. +/// +/// # Returns +/// An asynchronous computation that resolves to a `Vec` of JSON values. pub async fn vec_to_api(items: &[impl ToAPI]) -> Vec<serde_json::Value> { let mut ret = Vec::with_capacity(items.len()); @@ -17,9 +41,43 @@ pub async fn vec_to_api(items: &[impl ToAPI]) -> Vec<serde_json::Value> { ret } -pub fn api_response(json: &serde_json::Value) -> (Status, (ContentType, String)) { - ( - Status::Ok, - (ContentType::JSON, serde_json::to_string(json).unwrap()), - ) +/// Converts a string into a `Uuid`. +/// +/// This function attempts to parse a string as a UUID. If the parsing fails, +/// it returns an `ApiError` indicating the failure. +/// +/// # Arguments +/// * `id` - A string slice representing the UUID to be parsed. +/// +/// # Returns +/// * `Ok(uuid::Uuid)` if the string is a valid UUID. +/// * `Err(ApiError)` if the string is not a valid UUID, containing a descriptive error message. +pub fn to_uuid(id: &str) -> Result<uuid::Uuid, ApiError> { + uuid::Uuid::from_str(id).map_err(|_| no_uuid_error()) +} + +/// Generates an `ApiError` indicating that the input is not a valid UUID. +/// +/// This function is used internally to standardize error responses for invalid UUIDs. +/// +/// # Returns +/// * `ApiError` - A `BadRequest` error with a JSON payload describing the issue. +pub fn no_uuid_error() -> ApiError { + api_error("No valid UUID") +} + +/// Generates a custom `ApiError` with a given message. +/// +/// This function creates a `BadRequest` response with a JSON payload containing +/// an error message, making it useful for consistent error reporting. +/// +/// # Arguments +/// * `msg` - A string slice containing the error message to be included in the response. +/// +/// # Returns +/// * `ApiError` - A `BadRequest` error with a JSON payload describing the issue. +pub fn api_error(msg: &str) -> ApiError { + BadRequest(json!({ + "error": msg + })) } diff --git a/src/request/assets.rs b/src/request/assets.rs index 8278267..3b3e2e8 100644 --- a/src/request/assets.rs +++ b/src/request/assets.rs @@ -1,11 +1,4 @@ -use rocket::{ - fs::NamedFile, - get, - http::{ContentType, Status}, - State, -}; - -use tokio::{fs::File, io::AsyncReadExt}; +// TODO : Implement /* @@ -53,4 +46,4 @@ pub async fn video_thumbnail( None } -*/ \ No newline at end of file +*/ diff --git a/src/request/cache.rs b/src/request/cache.rs index 8439f04..3e9b20c 100644 --- a/src/request/cache.rs +++ b/src/request/cache.rs @@ -1,7 +1,22 @@ -use std::collections::HashMap; - -use rocket::tokio::sync::RwLock; +use dashmap::DashMap; +/// A macro to simplify using a cache for API responses. +/// +/// This macro checks if a value for a given route and ID exists in the cache. +/// If the value is found, it deserializes it into JSON and immediately returns it. +/// +/// # Parameters +/// * `$route` - A literal string representing the route. +/// * `$id` - A variable or literal string representing the unique identifier for the cached value. +/// * `$cache` - The cache instance to query. +/// +/// # Example +/// +/// ```ignore +/// use based::use_api_cache; +/// +/// use_api_cache!("/user", user_id, cache); +/// ``` #[macro_export] macro_rules! use_api_cache { ($route:literal, $id:ident, $cache:ident) => { @@ -16,49 +31,72 @@ macro_rules! use_api_cache { }; } +/// A structure for managing cached API responses in memory. +/// +/// The cache uses a nested `HashMap` structure to store values. Each route +/// maps to another `HashMap` where keys are IDs and values are cached results. +/// The cache supports asynchronous reads and writes using `RwLock`. pub struct RouteCache { - inner: RwLock<HashMap<String, HashMap<String, Option<String>>>>, + // Route cache with outer HashMap storing routes, inner HashMap storing IDs + inner: DashMap<String, DashMap<String, Option<String>>>, } impl RouteCache { + /// Creates a new `RouteCache` instance. + /// + /// # Returns + /// A new `RouteCache` instance with an empty cache. pub fn new() -> Self { Self { - inner: RwLock::new(HashMap::new()), + inner: DashMap::new(), } } + /// Retrieves a cached value or generates and caches it if not found. + /// + /// This method first checks if the value exists in the cache. If not, it uses the provided + /// generator function to compute the value, caches it, and then returns it. + /// + /// # Arguments + /// * `route` - The route associated with the cached value. + /// * `id` - The unique identifier for the value. + /// * `generator` - A function that generates the value if it's not in the cache. + /// + /// # Returns + /// A `String` containing the cached or generated value. pub async fn get<F, Fut>(&self, route: &str, id: &str, generator: F) -> String where F: FnOnce() -> Fut, Fut: std::future::Future<Output = String>, { - { - // Try to get a read lock first. - let lock = self.inner.read().await; - if let Some(inner_map) = lock.get(route) { - if let Some(cached_value) = inner_map.get(id) { - log::info!("Using cached value for {route} / {id}"); - return cached_value.clone().unwrap(); - } + if let Some(inner_map) = self.inner.get(route) { + if let Some(cached_value) = inner_map.get(id) { + log::info!("Using cached value for {route} / {id}"); + return cached_value.clone().unwrap(); } } - // If the value was not found, acquire a write lock to insert the computed value. - let mut lock = self.inner.write().await; - log::info!("Computing value for {route} / {id}"); let computed = generator().await; - lock.entry(route.to_string()) - .or_insert_with(HashMap::new) + self.inner + .entry(route.to_string()) + .or_insert_with(DashMap::new) .insert(id.to_string(), Some(computed.clone())); computed } + /// Retrieves a cached value if it exists, without generating a new one. + /// + /// # Arguments + /// * `route` - The route associated with the cached value. + /// * `id` - The unique identifier for the value. + /// + /// # Returns + /// An `Option<String>` containing the cached value, or `None` if not found. pub async fn get_only(&self, route: &str, id: &str) -> Option<String> { - let lock = self.inner.read().await; - if let Some(inner_map) = lock.get(route) { + if let Some(inner_map) = self.inner.get(route) { if let Some(cached_value) = inner_map.get(id) { log::info!("Using cached value for {route} / {id}"); return cached_value.clone(); @@ -68,53 +106,68 @@ impl RouteCache { None } + /// Retrieves a cached value or generates it conditionally if not found. + /// + /// This method works similarly to `get`, but the generator can return an + /// `Option<String>` to handle cases where the value might not always be computable. + /// + /// # Arguments + /// * `route` - The route associated with the cached value. + /// * `id` - The unique identifier for the value. + /// * `generator` - A function that generates the value if it's not in the cache. + /// + /// # Returns + /// An `Option<String>` containing the cached or generated value. pub async fn get_option<F, Fut>(&self, route: &str, id: &str, generator: F) -> Option<String> where F: FnOnce() -> Fut, Fut: std::future::Future<Output = Option<String>>, { - { - // Try to get a read lock first. - let lock = self.inner.read().await; - if let Some(inner_map) = lock.get(route) { - if let Some(cached_value) = inner_map.get(id) { - log::info!("Using cached value for {route} / {id}"); - return cached_value.clone(); - } + if let Some(inner_map) = self.inner.get(route) { + if let Some(cached_value) = inner_map.get(id) { + log::info!("Using cached value for {route} / {id}"); + return cached_value.clone(); } } - // If the value was not found, acquire a write lock to insert the computed value. - let mut lock = self.inner.write().await; - log::info!("Computing value for {route} / {id}"); let computed = generator().await; - lock.entry(route.to_string()) - .or_insert_with(HashMap::new) + self.inner + .entry(route.to_string()) + .or_insert_with(DashMap::new) .insert(id.to_string(), computed.clone()); computed } + /// Inserts a value into the cache. + /// + /// # Arguments + /// * `route` - The route associated with the value. + /// * `id` - The unique identifier for the value. + /// * `value` - The value to insert into the cache. pub async fn insert(&self, route: &str, id: &str, value: String) { - let mut lock = self.inner.write().await; - log::info!("Inserting value for {route} / {id}"); - lock.entry(route.to_string()) - .or_insert_with(HashMap::new) + self.inner + .entry(route.to_string()) + .or_insert_with(DashMap::new) .insert(id.to_string(), Some(value)); } + /// Invalidates a cached value, removing it from the cache. + /// + /// # Arguments + /// * `route` - The route associated with the value to remove. + /// * `id` - The unique identifier for the value to remove. pub async fn invalidate(&self, route: &str, id: &str) { - let mut lock = self.inner.write().await; - if let Some(inner_map) = lock.get_mut(route) { + if let Some(inner_map) = self.inner.get_mut(route) { inner_map.remove(id); // If the inner map is empty, remove the route entry as well. if inner_map.is_empty() { - lock.remove(route); + self.inner.remove(route); } } } diff --git a/src/request/context.rs b/src/request/context.rs index bc0d1bf..dc1e3f6 100644 --- a/src/request/context.rs +++ b/src/request/context.rs @@ -1,9 +1,14 @@ -use rocket::{request::{self, FromRequest}, Request}; - -use crate::user::{Session, User}; +use rocket::{ + Request, + request::{self, FromRequest}, +}; +/// Represents contextual information about an HTTP request. pub struct RequestContext { - pub is_htmx: bool + /// A flag indicating if the request is an HTMX request. + /// + /// This is determined by checking the presence of the `HX-Request` header. + pub is_htmx: bool, } #[rocket::async_trait] @@ -16,7 +21,7 @@ impl<'r> FromRequest<'r> for RequestContext { .headers() .get("HX-Request") .collect::<Vec<&str>>() - .is_empty() + .is_empty(), }) } -} \ No newline at end of file +} diff --git a/src/request/mod.rs b/src/request/mod.rs index 8896beb..d322842 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -1,27 +1,16 @@ -use std::str::FromStr; +use rocket::http::{ContentType, Status}; -use rocket::response::status::BadRequest; -use serde_json::json; - -pub mod context; -pub mod assets; +#[cfg(feature = "cache")] pub mod cache; + pub mod api; +pub mod assets; +mod context; +pub use context::RequestContext; -pub fn to_uuid(id: &str) -> Result<uuid::Uuid, ApiError> { - uuid::Uuid::from_str(id).map_err(|_| no_uuid_error()) -} +/// HTTP response containing a string payload. +pub type StringResponse = (Status, (ContentType, String)); -type ApiError = BadRequest<serde_json::Value>; -type FallibleApiResponse = Result<serde_json::Value, ApiError>; - -pub fn no_uuid_error() -> ApiError { - api_error("No valid UUID") -} - -pub fn api_error(msg: &str) -> ApiError { - BadRequest(json!({ - "error": msg - })) -} +/// HTTP response containing raw binary data. +pub type RawResponse = (Status, (ContentType, Vec<u8>)); diff --git a/src/result.rs b/src/result.rs index a359a57..53d31e1 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,14 +1,14 @@ pub trait LogAndIgnore { - fn log_and_ignore(self, msg: &str); + fn log_err_and_ignore(self, msg: &str); + fn log_warn_and_ignore(self, msg: &str); } impl<T, E: std::fmt::Debug> LogAndIgnore for Result<T, E> { /// Handles the result by ignoring and logging it if it contains an error. /// /// If the result is `Ok`, does nothing. - /// If the result is `Err(e)` - /// logs the message provided (`msg`) along with the error. - fn log_and_ignore(self, msg: &str) { + /// If the result is `Err(e)`, logs the message provided (`msg`) along with the error as error. + fn log_err_and_ignore(self, msg: &str) { match self { Ok(_) => {} Err(e) => { @@ -16,4 +16,17 @@ impl<T, E: std::fmt::Debug> LogAndIgnore for Result<T, E> { } } } -} \ No newline at end of file + + /// Handles the result by ignoring and logging it if it contains an error. + /// + /// If the result is `Ok`, does nothing. + /// If the result is `Err(e)`, logs the message provided (`msg`) along with the error as warning. + fn log_warn_and_ignore(self, msg: &str) { + match self { + Ok(_) => {} + Err(e) => { + log::warn!("{msg} : {:?}", e); + } + } + } +}