This commit is contained in:
JMARyA 2024-12-18 14:33:53 +01:00
parent 3299d3cc4c
commit 5f9eec00bf
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
13 changed files with 484 additions and 192 deletions

View file

@ -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
}))
}

View file

@ -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
}
*/
*/

View file

@ -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);
}
}
}

View file

@ -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(),
})
}
}
}

View file

@ -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>));