From 96575cf40bf191ec6b7ed8f00795b4072cfa3599 Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Tue, 14 May 2024 14:30:03 -0500 Subject: [PATCH] More modularization --- Cargo.lock | 101 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/cli.rs | 1 + src/error.rs | 28 ++++++++++++ src/file_watcher.rs | 56 +++++++++++++++++++----- src/live_reload.rs | 5 --- src/main.rs | 82 ++++------------------------------- src/prelude.rs | 5 +++ src/result.rs | 3 ++ src/router.rs | 31 ++++++++++++++ src/state.rs | 5 +-- src/static_files.rs | 15 ++++--- src/templates.rs | 56 ++++++++++-------------- src/webserver.rs | 11 +++++ 14 files changed, 267 insertions(+), 133 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/error.rs delete mode 100644 src/live_reload.rs create mode 100644 src/prelude.rs create mode 100644 src/result.rs create mode 100644 src/router.rs create mode 100644 src/webserver.rs diff --git a/Cargo.lock b/Cargo.lock index 89d1244..f30c306 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,55 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "async-trait" version = "0.1.80" @@ -180,6 +229,33 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + [[package]] name = "color-eyre" version = "0.6.3" @@ -207,6 +283,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + [[package]] name = "config" version = "0.14.0" @@ -676,6 +758,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itoa" version = "1.0.11" @@ -752,6 +840,7 @@ name = "lyrs" version = "0.1.0" dependencies = [ "axum", + "clap", "color-eyre", "config", "futures", @@ -1365,6 +1454,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.60" @@ -1676,6 +1771,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index a6d93d8..99491cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] axum = { version = "0.7.5", features = ["macros", "tokio"] } +clap = "4.5.4" color-eyre = "0.6.3" config = "0.14.0" futures = "0.3.30" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1 @@ + diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..863bc9c --- /dev/null +++ b/src/error.rs @@ -0,0 +1,28 @@ +use crate::prelude::*; +use axum::{http::StatusCode, response::IntoResponse}; + +#[derive(Debug)] +pub struct Error(pub Box); + +impl IntoResponse for Error { + fn into_response(self) -> axum::http::Response { + error!("webserver error: {:?}", self.0); + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("internal server error: {}", self.0), + ) + .into_response() + } +} + +// This enables using `?` on functions that return `Result<_, anyhow::Error>` +// to turn them into `Result<_, AppError>`. That way you don't need to do that +// manually. +impl From for Error +where + E: Into>, +{ + fn from(err: E) -> Self { + Self(err.into()) + } +} diff --git a/src/file_watcher.rs b/src/file_watcher.rs index 6e16046..b462153 100644 --- a/src/file_watcher.rs +++ b/src/file_watcher.rs @@ -1,22 +1,24 @@ -use std::path::Path; - +use crate::prelude::*; use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; -use tokio::sync::mpsc::channel; -use tracing::{error, info, instrument}; +use std::path::Path; +use tokio::{sync::mpsc::channel, task::JoinHandle}; +use tracing::{error, info}; -use crate::Besult; - -pub type FileWatcher = RecommendedWatcher; +pub type WatcherType = RecommendedWatcher; +pub type FileWatcher = (WatcherType, JoinHandle<()>); pub mod prelude { - pub use super::{file_watcher, FileWatcher}; + #![allow(unused_imports)] + pub use super::{file_monitor, file_watcher, FileWatcher}; } -pub fn file_watcher(dir: P, cb: F) -> Besult +/// Notifies your callback for each individual event +pub fn file_watcher(dir: P, cb: F) -> Result where P: AsRef, F: Fn(Event) -> () + std::marker::Send + 'static, { + // TODO: debounce? let (tx, mut rx) = channel(1); let mut watcher = RecommendedWatcher::new( move |res| match res { @@ -30,7 +32,7 @@ where info!("Watching directory '{}'", dir.as_ref().display()); - tokio::spawn(async move { + let handle = tokio::spawn(async move { while let Some(ev) = rx.recv().await { cb(ev) } @@ -38,5 +40,37 @@ where watcher.watch(dir.as_ref(), RecursiveMode::Recursive)?; - Ok(watcher) + Ok((watcher, handle)) +} + +/// Only know when something changes +pub fn file_monitor(dir: P, cb: F) -> Result +where + P: AsRef, + F: Fn() -> () + std::marker::Send + 'static, +{ + let (tx, mut rx) = tokio::sync::watch::channel(()); + let mut watcher = RecommendedWatcher::new( + move |res| match res { + Ok(_e) => futures::executor::block_on(async { + tx.send_replace(()); + }), + Err(e) => error!("Error from file_watcher: {e}"), + }, + Config::default(), + )?; + + info!("Watching directory '{}'", dir.as_ref().display()); + + let handle = tokio::spawn(async move { + while let Ok(_) = rx.changed().await { + cb(); + // "good enough" debouncing + rx.mark_unchanged(); + } + }); + + watcher.watch(dir.as_ref(), RecursiveMode::Recursive)?; + + Ok((watcher, handle)) } diff --git a/src/live_reload.rs b/src/live_reload.rs deleted file mode 100644 index 573c55b..0000000 --- a/src/live_reload.rs +++ /dev/null @@ -1,5 +0,0 @@ -use tower_livereload::LiveReloadLayer; - -pub fn live_reload_layer() -> LiveReloadLayer { - LiveReloadLayer::new() -} diff --git a/src/main.rs b/src/main.rs index 6c3afa9..a28b8b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,85 +1,21 @@ -use axum::extract::State; -use axum::response::IntoResponse; -use axum::routing::get; -use axum::{http::StatusCode, response::Html, serve, Router}; -use minijinja::context; -use state::State as AppState; -pub use tracing::{debug, error, event, info, span, warn, Level}; - -use crate::live_reload::live_reload_layer; - -#[derive(Debug)] -struct Berr(Box); -type Besult = std::result::Result; +use crate::prelude::*; +mod cli; +mod error; mod file_watcher; -mod live_reload; mod observe; +mod prelude; +mod result; +mod router; mod state; mod static_files; mod tailwind; mod templates; +mod webserver; #[tokio::main] -async fn main() -> Besult<()> { +async fn main() -> Result<()> { // load configuration? let _setup_logging = observe::setup_logging(); - - let state = AppState::try_new().await?; - - // TODO: only start tailwind if in dev mode? - tokio::spawn(async move { tailwind::start_watcher() }); - - let lr = live_reload_layer(); - - { - // TODO: only start watcher for dev mode? - let state = state.clone(); - let lr = lr.reloader(); - tokio::spawn(async move { state.templates.start_watcher(Some(lr)) }); - } - - let (static_file_service, _static_file_watcher) = static_files::router(Some(lr.reloader()))?; - - let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); - - info!("Listening on {listener:?}"); - let router = Router::new() - .route("/", get(index)) - .nest_service("/static", static_file_service) - .with_state(state); - - Ok(serve(listener, router).await?) -} - -async fn index(State(state): State) -> Besult> { - Ok(Html( - state - .templates - .render("pages/index.html.jinja", context!()) - .await?, - )) -} - -impl IntoResponse for Berr { - fn into_response(self) -> axum::http::Response { - error!("webserver error: {:?}", self.0); - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("internal server error: {}", self.0), - ) - .into_response() - } -} - -// This enables using `?` on functions that return `Result<_, anyhow::Error>` -// to turn them into `Result<_, AppError>`. That way you don't need to do that -// manually. -impl From for Berr -where - E: Into>, -{ - fn from(err: E) -> Self { - Self(err.into()) - } + webserver::webserver(router::router().await?).await } diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 0000000..68e176b --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,5 @@ +#![allow(unused_imports)] + +pub use crate::error::Error; +pub use crate::result::Result; +pub use tracing::{debug, error, event, info, span, warn, Level}; diff --git a/src/result.rs b/src/result.rs new file mode 100644 index 0000000..8207680 --- /dev/null +++ b/src/result.rs @@ -0,0 +1,3 @@ +use crate::error::Error; + +pub type Result = std::result::Result; diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 0000000..5222182 --- /dev/null +++ b/src/router.rs @@ -0,0 +1,31 @@ +use crate::{prelude::*, state::State as AppState, static_files}; +use axum::{extract::State, response::Html, routing::get, Router}; +use minijinja::context; +use tower_livereload::LiveReloadLayer; + +pub async fn router() -> Result { + let state = AppState::try_new().await?; + + let lr = LiveReloadLayer::new(); + let _template_file_watcher = state + .clone() + .templates + .start_watcher(Some(lr.reloader())) + .await?; + let (static_file_service, _static_file_watcher) = static_files::router(Some(lr.reloader()))?; + + Ok(Router::new() + .route("/", get(index)) + .nest_service("/static", static_file_service) + .layer(lr) + .with_state(state)) +} + +async fn index(State(state): State) -> Result> { + Ok(Html( + state + .templates + .render("pages/index.html.jinja", context!()) + .await?, + )) +} diff --git a/src/state.rs b/src/state.rs index 43b1a63..5dfabe9 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,5 +1,4 @@ -use crate::templates::Templates; -use crate::Besult; +use crate::{prelude::*, templates::Templates}; use std::sync::Arc; #[derive(Clone)] @@ -8,7 +7,7 @@ pub struct State { } impl State { - pub async fn try_new() -> Besult { + pub async fn try_new() -> Result { let templates = Arc::new(Templates::try_load("src/templates").await?); Ok(Self { templates }) diff --git a/src/static_files.rs b/src/static_files.rs index 1abd792..f198e6f 100644 --- a/src/static_files.rs +++ b/src/static_files.rs @@ -1,24 +1,25 @@ -use crate::file_watcher::prelude::*; +use crate::{ + file_watcher::{file_monitor, prelude::*}, + prelude::*, +}; -use crate::Besult; use axum::Router; use std::path::PathBuf; use std::str::FromStr; use std::sync::OnceLock; use tower_http::services::ServeDir; use tower_livereload::Reloader; -use tracing::{info, instrument}; fn static_file_dir() -> &'static PathBuf { static STATIC_FILE_DIR: OnceLock = OnceLock::new(); STATIC_FILE_DIR.get_or_init(|| PathBuf::from_str("static").unwrap()) } -#[instrument] -pub fn router(reloader: Option) -> Besult<(Router, Option)> { +pub fn router(reloader: Option) -> Result<(Router, Option)> { let watcher = if let Some(rl) = reloader { - Some(file_watcher(static_file_dir(), move |ev| { - info!("Static File Watcher Event: {ev:#?}"); + // TODO: debounce? + Some(file_monitor(static_file_dir(), move || { + info!("Static File Watcher Event"); rl.reload() })?) } else { diff --git a/src/templates.rs b/src/templates.rs index 049303d..4a242d4 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,22 +1,17 @@ -use std::{path::PathBuf, sync::Arc}; - +use crate::{file_watcher::prelude::*, prelude::*}; use minijinja::Environment; -use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; use pathdiff::diff_paths; -use tokio::sync::{mpsc::channel, Mutex}; +use std::{path::PathBuf, sync::Arc}; +use tokio::sync::Mutex; use tower_livereload::Reloader; -use tracing::{error, info}; +use tracing::{info, instrument}; -use crate::{file_watcher::file_watcher, Besult}; - -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Templates { env: Arc>>, dir: PathBuf, } -pub type Error = Box; - impl Templates { pub fn for_dir>(dir: P) -> Self { let env = Arc::new(Mutex::new(Environment::new())); @@ -26,7 +21,7 @@ impl Templates { } } - pub async fn try_load>(dir: P) -> Result { + pub async fn try_load>(dir: P) -> Result { let result = Self::for_dir(dir); result.load_env().await?; Ok(result) @@ -35,7 +30,7 @@ impl Templates { pub async fn start_watcher( self: Arc, reloader: Option, - ) -> Besult> { + ) -> Result> { if let Some(rl) = reloader { Ok(Some(self.watch(rl).await?)) } else { @@ -43,28 +38,21 @@ impl Templates { } } - async fn watch(self: Arc, reloader: Reloader) -> Besult { - let (tx, mut rx) = channel(1); - let watcher = file_watcher(self.dir.clone(), move |ev| { - info!("Template File Watcher Event: {ev:#?}"); - match tx.blocking_send(ev) { - Ok(_) => {} - Err(err) => error!("Error sending template file watcher event: {err}"), - }; - })?; - let us = self.clone(); - tokio::spawn(async move { - while let Some(_) = rx.recv().await { - us.load_env() + async fn watch(self: Arc, reloader: Reloader) -> Result { + // TODO: only reload template that changed? + let watcher = file_monitor(self.dir.clone(), move || { + futures::executor::block_on(async { + self.load_env() .await .expect("Failed to reload templates after template changed during runtime"); - reloader.reload() - } - }); + reloader.reload(); + }); + })?; Ok(watcher) } - pub async fn load_env(&self) -> Result<(), Error> { + #[instrument] + pub async fn load_env(&self) -> Result<()> { info!("Loading templates..."); for d in walkdir::WalkDir::new(&self.dir) { match d { @@ -76,7 +64,7 @@ impl Templates { .unwrap() .to_string_lossy() .into_owned(); - info!("Loading template {filename:#?} ({d:#?})"); + info!("Loading template {filename:?} ({d:?})"); self.env .lock() .await @@ -85,7 +73,6 @@ impl Templates { Err(_) => todo!(), } } - info!("Done loading templates!"); Ok(()) } @@ -93,11 +80,12 @@ impl Templates { &self, template_name: &str, context: S, - ) -> Result { - self.env + ) -> Result { + Ok(self + .env .lock() .await .get_template(template_name)? - .render(context) + .render(context)?) } } diff --git a/src/webserver.rs b/src/webserver.rs new file mode 100644 index 0000000..916c3ad --- /dev/null +++ b/src/webserver.rs @@ -0,0 +1,11 @@ +use crate::{prelude::*, tailwind}; +use axum::{serve, Router}; + +pub async fn webserver(router: Router) -> Result<()> { + // TODO: only start tailwind if in dev mode? + tokio::spawn(async move { tailwind::start_watcher() }); + + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); + info!("Listening on {listener:?}"); + Ok(serve(listener, router).await?) +}