diff --git a/Cargo.lock b/Cargo.lock index 1cbdc3a..89beff5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,6 +165,12 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.8.0" @@ -182,11 +188,14 @@ name = "churn" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "axum", "clap", "dotenv", + "notmad", "serde", "tokio", + "tokio-util", "tower-http", "tracing", "tracing-subscriber", @@ -260,6 +269,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -267,6 +291,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -275,6 +300,40 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -287,10 +346,16 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -482,6 +547,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "notmad" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "508d2cebf89bcea5803868fc39ecefe647cd8aa6c6955e40987b4fb7c7604326" +dependencies = [ + "anyhow", + "async-trait", + "futures", + "futures-util", + "rand", + "thiserror", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -554,6 +636,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -572,6 +663,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.5.7" @@ -677,6 +798,15 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -722,6 +852,26 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -761,6 +911,19 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.5.1" @@ -1000,3 +1163,24 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/crates/churn/Cargo.toml b/crates/churn/Cargo.toml index 50e15d8..01ec051 100644 --- a/crates/churn/Cargo.toml +++ b/crates/churn/Cargo.toml @@ -15,3 +15,6 @@ axum.workspace = true serde = { version = "1.0.197", features = ["derive"] } uuid = { version = "1.7.0", features = ["v4"] } tower-http = { version = "0.5.2", features = ["cors", "trace"] } +notmad = "0.5.0" +tokio-util = "0.7.12" +async-trait = "0.1.83" diff --git a/crates/churn/src/api.rs b/crates/churn/src/api.rs new file mode 100644 index 0000000..76f0159 --- /dev/null +++ b/crates/churn/src/api.rs @@ -0,0 +1,65 @@ +use std::net::SocketAddr; + +use axum::{extract::MatchedPath, http::Request, routing::get, Router}; +use tokio_util::sync::CancellationToken; +use tower_http::trace::TraceLayer; + +use crate::state::SharedState; + +pub struct Api { + state: SharedState, + host: SocketAddr, +} + +impl Api { + pub fn new(state: impl Into, host: impl Into) -> Self { + Self { + state: state.into(), + host: host.into(), + } + } + + pub async fn serve(&self) -> anyhow::Result<()> { + let app = Router::new() + .route("/", get(root)) + .with_state(self.state.clone()) + .layer( + TraceLayer::new_for_http().make_span_with(|request: &Request<_>| { + // Log the matched route's path (with placeholders not filled in). + // Use request.uri() or OriginalUri if you want the real path. + let matched_path = request + .extensions() + .get::() + .map(MatchedPath::as_str); + + tracing::info_span!( + "http_request", + method = ?request.method(), + matched_path, + some_other_field = tracing::field::Empty, + ) + }), // ... + ); + + tracing::info!("listening on {}", self.host); + let listener = tokio::net::TcpListener::bind(&self.host).await.unwrap(); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); + + Ok(()) + } +} + +async fn root() -> &'static str { + "Hello, churn!" +} + +#[async_trait::async_trait] +impl notmad::Component for Api { + async fn run(&self, _cancellation_token: CancellationToken) -> Result<(), notmad::MadError> { + self.serve().await.map_err(notmad::MadError::Inner)?; + + Ok(()) + } +} diff --git a/crates/churn/src/cli.rs b/crates/churn/src/cli.rs new file mode 100644 index 0000000..82e292c --- /dev/null +++ b/crates/churn/src/cli.rs @@ -0,0 +1,36 @@ +use std::net::SocketAddr; + +use clap::{Parser, Subcommand}; + +use crate::{api, state::SharedState}; + +pub async fn execute() -> anyhow::Result<()> { + let state = SharedState::new().await?; + + let cli = Command::parse(); + if let Some(Commands::Serve { host }) = cli.command { + tracing::info!("Starting service"); + + notmad::Mad::builder() + .add(api::Api::new(&state, host)) + .run() + .await?; + } + + Ok(()) +} + +#[derive(Parser)] +#[command(author, version, about, long_about = None, subcommand_required = true)] +struct Command { + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + Serve { + #[arg(env = "SERVICE_HOST", long, default_value = "127.0.0.1:3000")] + host: SocketAddr, + }, +} diff --git a/crates/churn/src/main.rs b/crates/churn/src/main.rs index 1dbaaba..06df04f 100644 --- a/crates/churn/src/main.rs +++ b/crates/churn/src/main.rs @@ -1,91 +1,13 @@ -use std::{net::SocketAddr, ops::Deref, sync::Arc}; - -use anyhow::Context; -use axum::extract::MatchedPath; -use axum::http::Request; -use axum::Router; -use axum::routing::get; -use clap::{Parser, Subcommand}; -use tower_http::trace::TraceLayer; - -#[derive(Parser)] -#[command(author, version, about, long_about = None, subcommand_required = true)] -struct Command { - #[command(subcommand)] - command: Option, -} - -#[derive(Subcommand)] -enum Commands { - Serve { - #[arg(env = "SERVICE_HOST", long, default_value = "127.0.0.1:3000")] - host: SocketAddr, - }, -} +mod api; +mod cli; +mod state; #[tokio::main] async fn main() -> anyhow::Result<()> { dotenv::dotenv().ok(); tracing_subscriber::fmt::init(); - let cli = Command::parse(); - - if let Some(Commands::Serve { host }) = cli.command { - tracing::info!("Starting service"); - - let state = SharedState(Arc::new(State::new().await?)); - - let app = Router::new() - .route("/", get(root)) - .with_state(state.clone()) - .layer( - TraceLayer::new_for_http().make_span_with(|request: &Request<_>| { - // Log the matched route's path (with placeholders not filled in). - // Use request.uri() or OriginalUri if you want the real path. - let matched_path = request - .extensions() - .get::() - .map(MatchedPath::as_str); - - tracing::info_span!( - "http_request", - method = ?request.method(), - matched_path, - some_other_field = tracing::field::Empty, - ) - }), // ... - ); - - tracing::info!("listening on {}", host); - let listener = tokio::net::TcpListener::bind(host).await.unwrap(); - axum::serve(listener, app.into_make_service()) - .await - .unwrap(); - } + cli::execute().await?; Ok(()) } - -async fn root() -> &'static str { - "Hello, churn!" -} - -#[derive(Clone)] -pub struct SharedState(Arc); - -impl Deref for SharedState { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -pub struct State {} - -impl State { - pub async fn new() -> anyhow::Result { - Ok(Self {}) - } -} - diff --git a/crates/churn/src/state.rs b/crates/churn/src/state.rs new file mode 100644 index 0000000..02a3bc9 --- /dev/null +++ b/crates/churn/src/state.rs @@ -0,0 +1,32 @@ +use std::{ops::Deref, sync::Arc}; + +#[derive(Clone)] +pub struct SharedState(Arc); + +impl SharedState { + pub async fn new() -> anyhow::Result { + Ok(Self(Arc::new(State::new().await?))) + } +} + +impl From<&SharedState> for SharedState { + fn from(value: &SharedState) -> Self { + value.clone() + } +} + +impl Deref for SharedState { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub struct State {} + +impl State { + pub async fn new() -> anyhow::Result { + Ok(Self {}) + } +}