diff --git a/acrate_utils/Cargo.toml b/acrate_utils/Cargo.toml index 099c1d6..1497ca2 100644 --- a/acrate_utils/Cargo.toml +++ b/acrate_utils/Cargo.toml @@ -15,6 +15,8 @@ axum-extra = { version = "0.9.4", features = ["query"] } log = { version = "0.4.22", features = ["std", "max_level_trace", "release_max_level_debug"] } pretty_env_logger = "0.5.0" mediatype = { version = "0.19.18", features = ["serde"] } +minijinja = "2.5.0" +tokio = { version = "1.41.1", features = ["net"] } [lints.clippy] tabs-in-doc-comments = "allow" diff --git a/acrate_utils/src/ext.rs b/acrate_utils/src/ext.rs new file mode 100644 index 0000000..99fef0c --- /dev/null +++ b/acrate_utils/src/ext.rs @@ -0,0 +1,6 @@ +use std::sync::Arc; +use axum::Extension; + +pub use minijinja; + +pub type ExtMj = Extension>>; diff --git a/acrate_utils/src/init.rs b/acrate_utils/src/init.rs new file mode 100644 index 0000000..e5c73d2 --- /dev/null +++ b/acrate_utils/src/init.rs @@ -0,0 +1,49 @@ +use std::net::SocketAddr; +use std::sync::Arc; +use axum::Extension; + +/// Initialize logging with [`pretty_env_logger::init`]. +pub fn init_logging() { + log::trace!("Initializing logging..."); + pretty_env_logger::init(); + log::trace!("Initialized logging!"); +} + +/// Initialize a [`tokio::net::TcpListener`] bound to the given [`SocketAddr`]. +/// +/// # Panics +/// +/// If unable to bind to the given [`SocketAddr`]. +/// +pub async fn init_listener(bind: SocketAddr) -> tokio::net::TcpListener { + log::trace!("Creating Tokio listener bound to: {bind:#?}"); + let listener = tokio::net::TcpListener::bind(bind) + .await + .unwrap_or_else(|_| panic!("Failed to bind to: {bind:#?}")); + log::trace!("Created Tokio listener: {listener:#?}"); + + listener +} + +/// Initialize a static [`minijinja::Environment`]. +pub fn init_minijinja() -> minijinja::Environment<'static> { + log::trace!("Creating Minijinja environment..."); + let mj = minijinja::Environment::<'static>::new(); + log::trace!("Created Minijinja environment: {mj:#?}"); + + mj +} + +/// Initialize an [`axum::Router`] with the given [`minijinja::Environment`] available. +pub fn init_router(mj: minijinja::Environment<'static>) -> axum::Router { + log::trace!("Creating Arc for the minijinja Environment..."); + let mj = Arc::new(mj); + log::trace!("Created Arc for the minijinja Environment: {mj:#?}"); + + log::trace!("Creating Axum router..."); + let router = axum::Router::new() + .layer(Extension(mj)); + log::trace!("Created Axum router: {router:#?}"); + + router +} diff --git a/acrate_utils/src/lib.rs b/acrate_utils/src/lib.rs index b93cf3f..1187539 100644 --- a/acrate_utils/src/lib.rs +++ b/acrate_utils/src/lib.rs @@ -1,14 +1,50 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right +pub mod ext; +pub mod init; +pub mod run; + + +/// Add the template file at the given path to the given [`minijinja::Environment`]. +#[macro_export] +macro_rules! add_minijinja_template { + ($mj:ident, $path:literal) => { + log::trace!("Adding template to minijinja Environment: {:?} ← {:#?}", $mj, $path); + $mj.add_template($path, include_str!($path)) + .expect(concat!("Invalid Minijinja template: ", $path)); + log::trace!("Added template to minijinja Environment: {:?} ← {:#?}", $mj, $path); + }; } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } +/// Add the given route to the [`axum::Router`]. +#[macro_export] +macro_rules! add_axum_route { + ($router:ident, $path:literal, $route:expr) => { + log::trace!("Adding route to axum Router: {:?} ← {:#?}", $router, $path); + let $router = $router.route($path, $route); + log::trace!("Added route to axum Router: {:?} ← {:#?}", $router, $path); + }; +} + +#[macro_export] +macro_rules! web_server { + ( + on: $socket_addr:expr, + templates: [ + $( $template_path:literal ),+ + ], + routes: { + $( $path:literal => $route:expr ),+ + } + ) => { + $crate::init::init_logging(); + let listener = $crate::init::init_listener($socket_addr).await; + let mut mj = $crate::init::init_minijinja(); + $( + $crate::add_minijinja_template!(mj, $template_path); + )+ + let router = $crate::init::init_router(mj); + $( + $crate::add_axum_route!(router, $path, $route); + )+ + $crate::run::run_server(listener, router).await + }; } diff --git a/acrate_utils/src/run.rs b/acrate_utils/src/run.rs new file mode 100644 index 0000000..3185be8 --- /dev/null +++ b/acrate_utils/src/run.rs @@ -0,0 +1,25 @@ +/// Run a web server with [`axum::serve`], using the given [`tokio::net::TcpListener`] and [`axum::Router`]. +/// +/// # Exits +/// +/// Once the server is terminated, the process will exit with either: +/// +/// - `0`, if [`axum::serve`] returned [`Ok`]; +/// - `1`, if [`axum::serve`] returned [`Err`]. +/// +pub async fn run_server(listener: tokio::net::TcpListener, application: axum::Router) -> std::convert::Infallible { + log::trace!("Serving application: {listener:#?} → {application:#?}"); + let result = axum::serve(listener, application) + .await; + + match result { + Ok(()) => { + log::error!("Server exited gracefully."); + std::process::exit(0); + } + Err(e) => { + log::error!("Server exited with an error: {e:#?}"); + std::process::exit(1); + } + } +}