diff --git a/Cargo.lock b/Cargo.lock index af75c7e..65ec581 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,6 +98,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "bytes" version = "1.6.0" @@ -270,9 +276,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "lib" -version = "1.0.0" +version = "1.1.0" dependencies = [ "axum", "derive", @@ -280,6 +292,10 @@ dependencies = [ "serde", "tokio", "tokio-util", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", ] [[package]] @@ -348,6 +364,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "object" version = "0.36.0" @@ -363,6 +389,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -490,6 +522,15 @@ dependencies = [ "serde", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -529,6 +570,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tokio" version = "1.38.0" @@ -584,6 +635,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -604,9 +672,21 @@ checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -614,6 +694,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -622,12 +728,40 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 83071fc..41f599d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lib" -version = "1.0.0" +version = "1.1.0" edition = "2021" authors = ["Martin Berg Alstad"] @@ -9,9 +9,14 @@ authors = ["Martin Berg Alstad"] [dependencies] # Api axum = { version = "0.7.5", optional = true } +tower = { version = "0.4.13", optional = true } +tower-http = { version = "0.5.2", optional = true, features = ["trace", "cors", "normalize-path"] } # Async tokio = { version = "1.38.0", optional = true, features = ["fs"] } tokio-util = { version = "0.7.11", optional = true, features = ["io"] } +# Logging +tracing = "0.1.40" +tracing-subscriber = "0.3.18" # Parsing nom = { version = "7.1.3", optional = true } # Serialization / Deserialization @@ -20,7 +25,7 @@ serde = { version = "1.0.203", optional = true, features = ["derive"] } derive = { path = "derive", optional = true } [features] -axum = ["dep:axum"] +axum = ["dep:axum", "dep:tower", "dep:tower-http"] tokio = ["dep:tokio", "dep:tokio-util"] vec = [] nom = ["dep:nom"] diff --git a/src/axum/app.rs b/src/axum/app.rs index 556826c..50ef134 100644 --- a/src/axum/app.rs +++ b/src/axum/app.rs @@ -1,3 +1,20 @@ +#[cfg(feature = "axum")] +use { + axum::{extract::Request, handler::Handler, Router, ServiceExt}, + std::net::Ipv4Addr, + tower::layer::Layer, + tower_http::{ + cors::CorsLayer, + normalize_path::NormalizePathLayer, + trace, + trace::{HttpMakeClassifier, TraceLayer}, + }, + tracing::{info, Level}, +}; +#[cfg(all(feature = "axum", feature = "tokio"))] +use {std::io, std::net::SocketAddr, tokio::net::TcpListener}; + +// TODO trim trailing slash into macro > let _app = NormalizePathLayer::trim_trailing_slash().layer(create_app!(routes)); #[macro_export] #[cfg(feature = "axum")] macro_rules! create_app { @@ -9,10 +26,141 @@ macro_rules! create_app { }; } +#[derive(Default)] +#[cfg(feature = "axum")] +pub struct AppBuilder { + router: Router, + socket: Option<(Ipv4Addr, u16)>, + cors: Option, + normalize_path: Option, + tracing: Option>, +} + +#[cfg(all(feature = "axum", feature = "tokio"))] +impl AppBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn routes(mut self, routes: &[Router]) -> Self { + self.router = routes.iter().cloned().fold(self.router, Router::merge); + self + } + + pub fn socket(mut self, socket: impl Into<(Ipv4Addr, u16)>) -> Self { + self.socket = Some(socket.into()); + self + } + + pub fn fallback(mut self, fallback: H) -> Self + where + H: Handler, + T: 'static, + { + self.router = self.router.fallback(fallback); + self + } + + pub fn cors(mut self, cors: CorsLayer) -> Self { + self.cors = Some(cors); + self + } + + pub fn normalize_path(mut self, normalize_path: bool) -> Self { + self.normalize_path = Some(normalize_path); + self + } + + pub fn tracing(mut self, tracing: TraceLayer) -> Self { + self.tracing = Some(tracing); + self + } + + pub async fn serve(self) -> io::Result<()> { + let listener = self.listener().await?; + let _ = fmt_trace(); + + if self.normalize_path.unwrap_or(true) { + let app = NormalizePathLayer::trim_trailing_slash().layer(self.create_app()); + axum::serve(listener, ServiceExt::::into_make_service(app)).await?; + } else { + let app = self.create_app(); + axum::serve(listener, app.into_make_service()).await?; + }; + Ok(()) + } + + async fn listener(&self) -> io::Result { + let addr = SocketAddr::from(self.socket.unwrap_or((Ipv4Addr::UNSPECIFIED, 8000))); + info!("Initializing server on: {addr}"); + TcpListener::bind(&addr).await + } + + fn create_app(self) -> Router { + let mut app = self.router; + if let Some(cors) = self.cors { + app = app.layer(cors); + } + app.layer( + self.tracing.unwrap_or( + TraceLayer::new_for_http() + .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) + .on_response(trace::DefaultOnResponse::new().level(Level::INFO)), + ), + ) + } +} + +fn fmt_trace() -> Result<(), String> { + tracing_subscriber::fmt() + .with_target(false) + .compact() + .try_init() + .map_err(|error| error.to_string()) +} + #[cfg(all(test, feature = "axum"))] mod tests { use axum::Router; + use super::*; + + #[cfg(feature = "tokio")] + mod tokio_tests { + use std::time::Duration; + + use tokio::time::sleep; + + use super::*; + + #[tokio::test] + async fn test_app_builder_serve() { + let handler = tokio::spawn(async { + AppBuilder::new().serve().await.unwrap(); + }); + sleep(Duration::from_secs(1)).await; + handler.abort(); + } + + #[tokio::test] + async fn test_app_builder_all() { + let handler = tokio::spawn(async { + AppBuilder::new() + .socket((Ipv4Addr::LOCALHOST, 8080)) + .routes(&[Router::new()]) + .fallback(|| async { "Fallback" }) + .cors(CorsLayer::new()) + .normalize_path(true) + .tracing(TraceLayer::new_for_http()) + .serve() + .await + .unwrap(); + }); + sleep(Duration::from_secs(1)).await; + handler.abort(); + } + } + #[test] fn test_create_app_router_only() { let _app: Router<()> = create_app!(Router::new());