From d5a3d1582d8028079eba00f6af112b06ec6d6c01 Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Wed, 15 May 2024 16:48:23 -0500 Subject: [PATCH] Error handling I think --- Cargo.lock | 9 ++-- Cargo.toml | 1 + src/cli.rs | 43 ++++++++++++++---- src/error.rs | 28 ------------ src/file_watcher.rs | 20 +++++---- src/main.rs | 7 +-- src/prelude.rs | 6 +-- src/result.rs | 3 -- src/router.rs | 54 +++++++++++++++++++--- src/state.rs | 15 ++++++- src/static_files.rs | 2 +- src/tailwind.rs | 7 +-- src/templates.rs | 84 ++++++++++++++++++++++------------- src/templates/page.jinja.html | 10 ++--- src/webserver.rs | 17 +++++-- 15 files changed, 196 insertions(+), 110 deletions(-) delete mode 100644 src/error.rs delete mode 100644 src/result.rs diff --git a/Cargo.lock b/Cargo.lock index f067369..3637d2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -816,6 +816,7 @@ dependencies = [ "pathdiff", "redact", "serde", + "thiserror", "tokio", "tower", "tower-http", @@ -1401,18 +1402,18 @@ checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index bc6553e..7861f31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ notify = "6.1.1" pathdiff = "0.2.1" redact = { version = "0.1.9", features = ["serde"] } serde = "1.0.201" +thiserror = "1.0.60" tokio = { version = "1.37.0", features = ["full"] } tower = "0.4.13" tower-http = { version = "0.5.2", features = ["fs"] } diff --git a/src/cli.rs b/src/cli.rs index 7138072..e1a14f2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,6 @@ -use crate::prelude::*; +use crate::{prelude::*, router::NewRouterError}; use prelude::*; +use thiserror::Error; mod prelude { pub use clap::{Args, Parser, Subcommand}; @@ -23,26 +24,52 @@ enum Commands { /// Doc comment #[derive(Args)] struct Run { - /// Doc comment + /// Whether or not to watch certain resource files for changes and reload accordingly #[arg(short, long, default_value = None)] pub watch: bool, + + /// The address to bind to - you almost certainly want to use :: or 0.0.0.0 instead of the default + #[arg(short = 'H', long, default_value = "::1")] + pub host: String, + + /// The port to bind to + #[arg(short, long, default_value = "3000")] + pub port: u16, +} + +#[derive(Error, Debug)] +pub enum RunError { + #[error("router error: {0}")] + Router(#[from] NewRouterError), + + #[error("io error: {0}")] + Io(#[from] std::io::Error), } impl Run { - pub async fn run(&self) -> Result<()> { + pub async fn run(&self) -> Result<(), RunError> { let (router, _watchers) = crate::router::router(self.watch).await?; - crate::webserver::webserver(router, self.watch).await + Ok( + crate::webserver::webserver(router, self.watch, Some(&self.host), Some(self.port)) + .await?, + ) } } -pub fn cli() -> Result { - Ok(Cli::parse()) +pub fn cli() -> Cli { + Cli::parse() +} + +#[derive(Error, Debug)] +pub enum ExecError { + #[error("run error: {0}")] + Run(#[from] RunError), } impl Cli { - pub async fn exec(self) -> Result<()> { + pub async fn exec(self) -> Result<(), ExecError> { match self.command { - Commands::Run(args) => args.run().await, + Commands::Run(args) => Ok(args.run().await?), } } } diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 863bc9c..0000000 --- a/src/error.rs +++ /dev/null @@ -1,28 +0,0 @@ -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 b462153..323cca0 100644 --- a/src/file_watcher.rs +++ b/src/file_watcher.rs @@ -2,7 +2,6 @@ use crate::prelude::*; use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; use std::path::Path; use tokio::{sync::mpsc::channel, task::JoinHandle}; -use tracing::{error, info}; pub type WatcherType = RecommendedWatcher; pub type FileWatcher = (WatcherType, JoinHandle<()>); @@ -13,9 +12,10 @@ pub mod prelude { } /// Notifies your callback for each individual event -pub fn file_watcher(dir: P, cb: F) -> Result +#[instrument(skip(callback))] +pub fn file_watcher(dir: P, callback: F) -> Result where - P: AsRef, + P: AsRef + std::fmt::Debug, F: Fn(Event) -> () + std::marker::Send + 'static, { // TODO: debounce? @@ -23,6 +23,7 @@ where let mut watcher = RecommendedWatcher::new( move |res| match res { Ok(e) => futures::executor::block_on(async { + trace!("Sending event: {e:?}"); tx.send(e).await.unwrap(); }), Err(e) => error!("Error from file_watcher: {e}"), @@ -30,11 +31,11 @@ where Config::default(), )?; - info!("Watching directory '{}'", dir.as_ref().display()); + info!("watching"); let handle = tokio::spawn(async move { while let Some(ev) = rx.recv().await { - cb(ev) + callback(ev) } }); @@ -44,9 +45,10 @@ where } /// Only know when something changes -pub fn file_monitor(dir: P, cb: F) -> Result +#[instrument(skip(callback))] +pub fn file_monitor(dir: P, callback: F) -> Result where - P: AsRef, + P: AsRef + std::fmt::Debug, F: Fn() -> () + std::marker::Send + 'static, { let (tx, mut rx) = tokio::sync::watch::channel(()); @@ -60,11 +62,11 @@ where Config::default(), )?; - info!("Watching directory '{}'", dir.as_ref().display()); + info!("watching"); let handle = tokio::spawn(async move { while let Ok(_) = rx.changed().await { - cb(); + callback(); // "good enough" debouncing rx.mark_unchanged(); } diff --git a/src/main.rs b/src/main.rs index 694fbaf..9b470ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,9 @@ use crate::prelude::*; mod cli; -mod error; mod file_watcher; mod observe; mod prelude; -mod result; mod router; mod state; mod static_files; @@ -14,7 +12,6 @@ mod templates; mod webserver; #[tokio::main] -async fn main() -> Result<()> { - let _setup_logging = observe::setup_logging(); - cli::cli()?.exec().await +async fn main() -> AnonResult<()> { + Ok(cli::cli().exec().await?) } diff --git a/src/prelude.rs b/src/prelude.rs index 68e176b..cf2a02b 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,5 +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}; +pub use color_eyre::eyre::Result as AnonResult; +pub use std::result::Result; +pub use tracing::{debug, error, event, info, instrument, span, trace, warn, Level}; diff --git a/src/result.rs b/src/result.rs deleted file mode 100644 index 8207680..0000000 --- a/src/result.rs +++ /dev/null @@ -1,3 +0,0 @@ -use crate::error::Error; - -pub type Result = std::result::Result; diff --git a/src/router.rs b/src/router.rs index b2611d4..f177579 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,9 +1,53 @@ -use crate::{file_watcher::FileWatcher, prelude::*, state::State as AppState, static_files}; -use axum::{extract::State, response::Html, routing::get, Router}; +use crate::{ + file_watcher::FileWatcher, + prelude::*, + state::{NewStateError, State as AppState}, + static_files, +}; +use axum::{ + extract::State, + http::StatusCode, + response::{Html, IntoResponse}, + routing::get, + Router, +}; use minijinja::context; use tower_livereload::LiveReloadLayer; -pub async fn router(with_watchers: bool) -> Result<(Router, Vec>)> { +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum NewRouterError { + #[error("new state error: {0}")] + State(#[from] NewStateError), + + #[error("watcher error: {0}")] + Watcher(#[from] notify::Error), +} + +#[derive(Error, Debug)] +pub enum ReqError { + #[error("template error: {0}")] + Template(#[from] minijinja::Error), +} + +impl IntoResponse for ReqError { + fn into_response(self) -> axum::http::Response { + error!("webserver error: {:?}", self); + ( + StatusCode::INTERNAL_SERVER_ERROR, + // TODO: don't expose raw errors over the internet? + format!("internal server error: {}", self), + ) + .into_response() + } +} + +pub type ReqResult = Result; + +pub async fn router( + with_watchers: bool, +) -> Result<(Router, Vec>), NewRouterError> { let state = AppState::try_new().await?; let live_reload_layer: Option = if with_watchers { @@ -38,7 +82,7 @@ pub async fn router(with_watchers: bool) -> Result<(Router, Vec) -> Result> { +async fn index(State(state): State) -> ReqResult> { Ok(Html( state .templates @@ -46,7 +90,7 @@ async fn index(State(state): State) -> Result> { .await?, )) } -async fn about(State(state): State) -> Result> { +async fn about(State(state): State) -> ReqResult> { Ok(Html( state .templates diff --git a/src/state.rs b/src/state.rs index 5dfabe9..55e660a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,4 +1,7 @@ -use crate::{prelude::*, templates::Templates}; +use crate::{ + prelude::*, + templates::{TemplateLoadError, Templates}, +}; use std::sync::Arc; #[derive(Clone)] @@ -7,9 +10,17 @@ pub struct State { } impl State { - pub async fn try_new() -> Result { + pub async fn try_new() -> Result { let templates = Arc::new(Templates::try_load("src/templates").await?); Ok(Self { templates }) } } + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum NewStateError { + #[error("template load error: {0}")] + TemplateLoad(#[from] TemplateLoadError), +} diff --git a/src/static_files.rs b/src/static_files.rs index f198e6f..43e34a6 100644 --- a/src/static_files.rs +++ b/src/static_files.rs @@ -15,7 +15,7 @@ fn static_file_dir() -> &'static PathBuf { STATIC_FILE_DIR.get_or_init(|| PathBuf::from_str("static").unwrap()) } -pub fn router(reloader: Option) -> Result<(Router, Option)> { +pub fn router(reloader: Option) -> Result<(Router, Option), notify::Error> { let watcher = if let Some(rl) = reloader { // TODO: debounce? Some(file_monitor(static_file_dir(), move || { diff --git a/src/tailwind.rs b/src/tailwind.rs index 23080d3..3374876 100644 --- a/src/tailwind.rs +++ b/src/tailwind.rs @@ -1,12 +1,13 @@ +use crate::prelude::*; use std::process::Stdio; use tokio::{ io::{AsyncBufReadExt, BufReader}, process::Command, }; -use tracing::{error, event, info, Level}; +#[instrument] pub fn start_watcher() { - info!("Starting tailwind..."); + info!("starting"); match Command::new("tailwindcss") .args(["-i", "src/style.css", "-o", "static/style.css", "--watch"]) .stdout(Stdio::piped()) @@ -14,7 +15,7 @@ pub fn start_watcher() { .spawn() { Ok(mut tw) => { - info!("Tailwind spawned!"); + info!("spawned"); let mut stdout_reader = BufReader::new(tw.stdout.take().unwrap()).lines(); tokio::spawn(async move { while let Ok(Some(l)) = stdout_reader.next_line().await { diff --git a/src/templates.rs b/src/templates.rs index e460b59..6b3c1d7 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,10 +1,23 @@ use crate::{file_watcher::prelude::*, prelude::*}; use minijinja::Environment; use pathdiff::diff_paths; -use std::{path::PathBuf, sync::Arc}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; +use thiserror::Error; use tokio::sync::Mutex; use tower_livereload::Reloader; +#[derive(Error, Debug)] +pub enum TemplateLoadError { + #[error("template error: {0}")] + Minijinja(#[from] minijinja::Error), + + #[error("io error: {0}")] + Io(#[from] std::io::Error), +} + #[derive(Clone, Debug)] pub struct Templates { env: Arc>>, @@ -20,7 +33,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) @@ -29,7 +42,7 @@ impl Templates { pub async fn start_watcher( self: Arc, reloader: Option, - ) -> Result> { + ) -> Result, notify::Error> { if let Some(rl) = reloader { Ok(Some(self.watch(rl).await?)) } else { @@ -37,41 +50,50 @@ impl Templates { } } - async fn watch(self: Arc, reloader: Reloader) -> Result { - // TODO: only reload template that changed? - let watcher = file_monitor(self.dir.clone(), move || { + async fn watch(self: Arc, reloader: Reloader) -> Result { + let watcher = file_watcher(self.dir.clone(), move |ev| { futures::executor::block_on(async { - self.load_env() - .await - .expect("Failed to reload templates after template changed during runtime"); - reloader.reload(); + if ev.kind.is_create() || ev.kind.is_modify() { + for p in ev.paths { + let _ = self.load_template(&p).await; + } + reloader.reload(); + } }); })?; Ok(watcher) } - pub async fn load_env(&self) -> Result<()> { - info!("Loading templates..."); + #[instrument(skip(self, p))] + pub async fn load_template

(&self, p: P) -> Result<(), TemplateLoadError> + where + P: AsRef + Copy, + { + let p = p.as_ref(); + if p.is_dir() { + return Ok(()); + } + let filename: String = diff_paths(p, "src/templates") + .unwrap() + .to_string_lossy() + .into_owned(); + if [".bck", ".tmp"].iter().any(|s| filename.ends_with(s)) { + debug!("skipping temporary file"); + return Ok(()); + } + info!(filename); + Ok(self + .env + .lock() + .await + .add_template_owned(filename, std::fs::read_to_string(p)?)?) + } + + #[instrument(skip(self))] + pub async fn load_env(&self) -> Result<(), TemplateLoadError> { for d in walkdir::WalkDir::new(&self.dir) { match d { - Ok(d) => { - // ignore editor temporary files - if [".bck", ".tmp"].iter().any(|s| d.path().ends_with(s)) { - continue; - } - if d.file_type().is_dir() { - continue; - } - let filename: String = diff_paths(d.path(), "src/templates") - .unwrap() - .to_string_lossy() - .into_owned(); - info!("Loading template {filename:?} ({d:?})"); - self.env - .lock() - .await - .add_template_owned(filename, std::fs::read_to_string(d.path())?)?; - } + Ok(d) => self.load_template(d.path()).await?, Err(_) => todo!(), } } @@ -82,7 +104,7 @@ impl Templates { &self, template_name: &str, context: S, - ) -> Result { + ) -> Result { Ok(self .env .lock() diff --git a/src/templates/page.jinja.html b/src/templates/page.jinja.html index aee3c40..94db782 100644 --- a/src/templates/page.jinja.html +++ b/src/templates/page.jinja.html @@ -1,21 +1,21 @@ - + Sup + Page Template -