More modularization

This commit is contained in:
Daniel Flanagan 2024-05-14 14:30:03 -05:00
parent 2052092796
commit 96575cf40b
14 changed files with 267 additions and 133 deletions

101
Cargo.lock generated
View file

@ -26,6 +26,55 @@ dependencies = [
"memchr", "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]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.80" version = "0.1.80"
@ -180,6 +229,33 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "color-eyre" name = "color-eyre"
version = "0.6.3" version = "0.6.3"
@ -207,6 +283,12 @@ dependencies = [
"tracing-error", "tracing-error",
] ]
[[package]]
name = "colorchoice"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
[[package]] [[package]]
name = "config" name = "config"
version = "0.14.0" version = "0.14.0"
@ -676,6 +758,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.11" version = "1.0.11"
@ -752,6 +840,7 @@ name = "lyrs"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"clap",
"color-eyre", "color-eyre",
"config", "config",
"futures", "futures",
@ -1365,6 +1454,12 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "syn" name = "syn"
version = "2.0.60" version = "2.0.60"
@ -1676,6 +1771,12 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"

View file

@ -7,6 +7,7 @@ edition = "2021"
[dependencies] [dependencies]
axum = { version = "0.7.5", features = ["macros", "tokio"] } axum = { version = "0.7.5", features = ["macros", "tokio"] }
clap = "4.5.4"
color-eyre = "0.6.3" color-eyre = "0.6.3"
config = "0.14.0" config = "0.14.0"
futures = "0.3.30" futures = "0.3.30"

1
src/cli.rs Normal file
View file

@ -0,0 +1 @@

28
src/error.rs Normal file
View file

@ -0,0 +1,28 @@
use crate::prelude::*;
use axum::{http::StatusCode, response::IntoResponse};
#[derive(Debug)]
pub struct Error(pub Box<dyn std::error::Error>);
impl IntoResponse for Error {
fn into_response(self) -> axum::http::Response<axum::body::Body> {
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<E> From<E> for Error
where
E: Into<Box<dyn std::error::Error>>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}

View file

@ -1,22 +1,24 @@
use std::path::Path; use crate::prelude::*;
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
use tokio::sync::mpsc::channel; use std::path::Path;
use tracing::{error, info, instrument}; use tokio::{sync::mpsc::channel, task::JoinHandle};
use tracing::{error, info};
use crate::Besult; pub type WatcherType = RecommendedWatcher;
pub type FileWatcher = (WatcherType, JoinHandle<()>);
pub type FileWatcher = RecommendedWatcher;
pub mod prelude { pub mod prelude {
pub use super::{file_watcher, FileWatcher}; #![allow(unused_imports)]
pub use super::{file_monitor, file_watcher, FileWatcher};
} }
pub fn file_watcher<P, F>(dir: P, cb: F) -> Besult<FileWatcher> /// Notifies your callback for each individual event
pub fn file_watcher<P, F>(dir: P, cb: F) -> Result<FileWatcher>
where where
P: AsRef<Path>, P: AsRef<Path>,
F: Fn(Event) -> () + std::marker::Send + 'static, F: Fn(Event) -> () + std::marker::Send + 'static,
{ {
// TODO: debounce?
let (tx, mut rx) = channel(1); let (tx, mut rx) = channel(1);
let mut watcher = RecommendedWatcher::new( let mut watcher = RecommendedWatcher::new(
move |res| match res { move |res| match res {
@ -30,7 +32,7 @@ where
info!("Watching directory '{}'", dir.as_ref().display()); info!("Watching directory '{}'", dir.as_ref().display());
tokio::spawn(async move { let handle = tokio::spawn(async move {
while let Some(ev) = rx.recv().await { while let Some(ev) = rx.recv().await {
cb(ev) cb(ev)
} }
@ -38,5 +40,37 @@ where
watcher.watch(dir.as_ref(), RecursiveMode::Recursive)?; watcher.watch(dir.as_ref(), RecursiveMode::Recursive)?;
Ok(watcher) Ok((watcher, handle))
}
/// Only know when something changes
pub fn file_monitor<P, F>(dir: P, cb: F) -> Result<FileWatcher>
where
P: AsRef<Path>,
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))
} }

View file

@ -1,5 +0,0 @@
use tower_livereload::LiveReloadLayer;
pub fn live_reload_layer() -> LiveReloadLayer {
LiveReloadLayer::new()
}

View file

@ -1,85 +1,21 @@
use axum::extract::State; use crate::prelude::*;
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<dyn std::error::Error>);
type Besult<T> = std::result::Result<T, Berr>;
mod cli;
mod error;
mod file_watcher; mod file_watcher;
mod live_reload;
mod observe; mod observe;
mod prelude;
mod result;
mod router;
mod state; mod state;
mod static_files; mod static_files;
mod tailwind; mod tailwind;
mod templates; mod templates;
mod webserver;
#[tokio::main] #[tokio::main]
async fn main() -> Besult<()> { async fn main() -> Result<()> {
// load configuration? // load configuration?
let _setup_logging = observe::setup_logging(); let _setup_logging = observe::setup_logging();
webserver::webserver(router::router().await?).await
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<AppState>) -> Besult<Html<String>> {
Ok(Html(
state
.templates
.render("pages/index.html.jinja", context!())
.await?,
))
}
impl IntoResponse for Berr {
fn into_response(self) -> axum::http::Response<axum::body::Body> {
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<E> From<E> for Berr
where
E: Into<Box<dyn std::error::Error>>,
{
fn from(err: E) -> Self {
Self(err.into())
}
} }

5
src/prelude.rs Normal file
View file

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

3
src/result.rs Normal file
View file

@ -0,0 +1,3 @@
use crate::error::Error;
pub type Result<T> = std::result::Result<T, Error>;

31
src/router.rs Normal file
View file

@ -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<Router> {
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<AppState>) -> Result<Html<String>> {
Ok(Html(
state
.templates
.render("pages/index.html.jinja", context!())
.await?,
))
}

View file

@ -1,5 +1,4 @@
use crate::templates::Templates; use crate::{prelude::*, templates::Templates};
use crate::Besult;
use std::sync::Arc; use std::sync::Arc;
#[derive(Clone)] #[derive(Clone)]
@ -8,7 +7,7 @@ pub struct State {
} }
impl State { impl State {
pub async fn try_new() -> Besult<Self> { pub async fn try_new() -> Result<Self> {
let templates = Arc::new(Templates::try_load("src/templates").await?); let templates = Arc::new(Templates::try_load("src/templates").await?);
Ok(Self { templates }) Ok(Self { templates })

View file

@ -1,24 +1,25 @@
use crate::file_watcher::prelude::*; use crate::{
file_watcher::{file_monitor, prelude::*},
prelude::*,
};
use crate::Besult;
use axum::Router; use axum::Router;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::sync::OnceLock; use std::sync::OnceLock;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tower_livereload::Reloader; use tower_livereload::Reloader;
use tracing::{info, instrument};
fn static_file_dir() -> &'static PathBuf { fn static_file_dir() -> &'static PathBuf {
static STATIC_FILE_DIR: OnceLock<PathBuf> = OnceLock::new(); static STATIC_FILE_DIR: OnceLock<PathBuf> = OnceLock::new();
STATIC_FILE_DIR.get_or_init(|| PathBuf::from_str("static").unwrap()) STATIC_FILE_DIR.get_or_init(|| PathBuf::from_str("static").unwrap())
} }
#[instrument] pub fn router(reloader: Option<Reloader>) -> Result<(Router, Option<FileWatcher>)> {
pub fn router(reloader: Option<Reloader>) -> Besult<(Router, Option<FileWatcher>)> {
let watcher = if let Some(rl) = reloader { let watcher = if let Some(rl) = reloader {
Some(file_watcher(static_file_dir(), move |ev| { // TODO: debounce?
info!("Static File Watcher Event: {ev:#?}"); Some(file_monitor(static_file_dir(), move || {
info!("Static File Watcher Event");
rl.reload() rl.reload()
})?) })?)
} else { } else {

View file

@ -1,22 +1,17 @@
use std::{path::PathBuf, sync::Arc}; use crate::{file_watcher::prelude::*, prelude::*};
use minijinja::Environment; use minijinja::Environment;
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use pathdiff::diff_paths; 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 tower_livereload::Reloader;
use tracing::{error, info}; use tracing::{info, instrument};
use crate::{file_watcher::file_watcher, Besult}; #[derive(Clone, Debug)]
#[derive(Clone)]
pub struct Templates { pub struct Templates {
env: Arc<Mutex<Environment<'static>>>, env: Arc<Mutex<Environment<'static>>>,
dir: PathBuf, dir: PathBuf,
} }
pub type Error = Box<dyn std::error::Error>;
impl Templates { impl Templates {
pub fn for_dir<P: Into<PathBuf>>(dir: P) -> Self { pub fn for_dir<P: Into<PathBuf>>(dir: P) -> Self {
let env = Arc::new(Mutex::new(Environment::new())); let env = Arc::new(Mutex::new(Environment::new()));
@ -26,7 +21,7 @@ impl Templates {
} }
} }
pub async fn try_load<P: Into<PathBuf>>(dir: P) -> Result<Self, Error> { pub async fn try_load<P: Into<PathBuf>>(dir: P) -> Result<Self> {
let result = Self::for_dir(dir); let result = Self::for_dir(dir);
result.load_env().await?; result.load_env().await?;
Ok(result) Ok(result)
@ -35,7 +30,7 @@ impl Templates {
pub async fn start_watcher( pub async fn start_watcher(
self: Arc<Self>, self: Arc<Self>,
reloader: Option<Reloader>, reloader: Option<Reloader>,
) -> Besult<Option<RecommendedWatcher>> { ) -> Result<Option<FileWatcher>> {
if let Some(rl) = reloader { if let Some(rl) = reloader {
Ok(Some(self.watch(rl).await?)) Ok(Some(self.watch(rl).await?))
} else { } else {
@ -43,28 +38,21 @@ impl Templates {
} }
} }
async fn watch(self: Arc<Self>, reloader: Reloader) -> Besult<RecommendedWatcher> { async fn watch(self: Arc<Self>, reloader: Reloader) -> Result<FileWatcher> {
let (tx, mut rx) = channel(1); // TODO: only reload template that changed?
let watcher = file_watcher(self.dir.clone(), move |ev| { let watcher = file_monitor(self.dir.clone(), move || {
info!("Template File Watcher Event: {ev:#?}"); futures::executor::block_on(async {
match tx.blocking_send(ev) { self.load_env()
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()
.await .await
.expect("Failed to reload templates after template changed during runtime"); .expect("Failed to reload templates after template changed during runtime");
reloader.reload() reloader.reload();
} });
}); })?;
Ok(watcher) Ok(watcher)
} }
pub async fn load_env(&self) -> Result<(), Error> { #[instrument]
pub async fn load_env(&self) -> Result<()> {
info!("Loading templates..."); info!("Loading templates...");
for d in walkdir::WalkDir::new(&self.dir) { for d in walkdir::WalkDir::new(&self.dir) {
match d { match d {
@ -76,7 +64,7 @@ impl Templates {
.unwrap() .unwrap()
.to_string_lossy() .to_string_lossy()
.into_owned(); .into_owned();
info!("Loading template {filename:#?} ({d:#?})"); info!("Loading template {filename:?} ({d:?})");
self.env self.env
.lock() .lock()
.await .await
@ -85,7 +73,6 @@ impl Templates {
Err(_) => todo!(), Err(_) => todo!(),
} }
} }
info!("Done loading templates!");
Ok(()) Ok(())
} }
@ -93,11 +80,12 @@ impl Templates {
&self, &self,
template_name: &str, template_name: &str,
context: S, context: S,
) -> Result<String, minijinja::Error> { ) -> Result<String> {
self.env Ok(self
.env
.lock() .lock()
.await .await
.get_template(template_name)? .get_template(template_name)?
.render(context) .render(context)?)
} }
} }

11
src/webserver.rs Normal file
View file

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