Error handling I think

This commit is contained in:
Daniel Flanagan 2024-05-15 16:48:23 -05:00
parent f408a87195
commit d5a3d1582d
15 changed files with 196 additions and 110 deletions

9
Cargo.lock generated
View file

@ -816,6 +816,7 @@ dependencies = [
"pathdiff", "pathdiff",
"redact", "redact",
"serde", "serde",
"thiserror",
"tokio", "tokio",
"tower", "tower",
"tower-http", "tower-http",
@ -1401,18 +1402,18 @@ checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.59" version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.59" version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -29,6 +29,7 @@ notify = "6.1.1"
pathdiff = "0.2.1" pathdiff = "0.2.1"
redact = { version = "0.1.9", features = ["serde"] } redact = { version = "0.1.9", features = ["serde"] }
serde = "1.0.201" serde = "1.0.201"
thiserror = "1.0.60"
tokio = { version = "1.37.0", features = ["full"] } tokio = { version = "1.37.0", features = ["full"] }
tower = "0.4.13" tower = "0.4.13"
tower-http = { version = "0.5.2", features = ["fs"] } tower-http = { version = "0.5.2", features = ["fs"] }

View file

@ -1,5 +1,6 @@
use crate::prelude::*; use crate::{prelude::*, router::NewRouterError};
use prelude::*; use prelude::*;
use thiserror::Error;
mod prelude { mod prelude {
pub use clap::{Args, Parser, Subcommand}; pub use clap::{Args, Parser, Subcommand};
@ -23,26 +24,52 @@ enum Commands {
/// Doc comment /// Doc comment
#[derive(Args)] #[derive(Args)]
struct Run { struct Run {
/// Doc comment /// Whether or not to watch certain resource files for changes and reload accordingly
#[arg(short, long, default_value = None)] #[arg(short, long, default_value = None)]
pub watch: bool, 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 { impl Run {
pub async fn run(&self) -> Result<()> { pub async fn run(&self) -> Result<(), RunError> {
let (router, _watchers) = crate::router::router(self.watch).await?; 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<Cli> { pub fn cli() -> Cli {
Ok(Cli::parse()) Cli::parse()
}
#[derive(Error, Debug)]
pub enum ExecError {
#[error("run error: {0}")]
Run(#[from] RunError),
} }
impl Cli { impl Cli {
pub async fn exec(self) -> Result<()> { pub async fn exec(self) -> Result<(), ExecError> {
match self.command { match self.command {
Commands::Run(args) => args.run().await, Commands::Run(args) => Ok(args.run().await?),
} }
} }
} }

View file

@ -1,28 +0,0 @@
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

@ -2,7 +2,6 @@ use crate::prelude::*;
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::Path; use std::path::Path;
use tokio::{sync::mpsc::channel, task::JoinHandle}; use tokio::{sync::mpsc::channel, task::JoinHandle};
use tracing::{error, info};
pub type WatcherType = RecommendedWatcher; pub type WatcherType = RecommendedWatcher;
pub type FileWatcher = (WatcherType, JoinHandle<()>); pub type FileWatcher = (WatcherType, JoinHandle<()>);
@ -13,9 +12,10 @@ pub mod prelude {
} }
/// Notifies your callback for each individual event /// Notifies your callback for each individual event
pub fn file_watcher<P, F>(dir: P, cb: F) -> Result<FileWatcher> #[instrument(skip(callback))]
pub fn file_watcher<P, F>(dir: P, callback: F) -> Result<FileWatcher, notify::Error>
where where
P: AsRef<Path>, P: AsRef<Path> + std::fmt::Debug,
F: Fn(Event) -> () + std::marker::Send + 'static, F: Fn(Event) -> () + std::marker::Send + 'static,
{ {
// TODO: debounce? // TODO: debounce?
@ -23,6 +23,7 @@ where
let mut watcher = RecommendedWatcher::new( let mut watcher = RecommendedWatcher::new(
move |res| match res { move |res| match res {
Ok(e) => futures::executor::block_on(async { Ok(e) => futures::executor::block_on(async {
trace!("Sending event: {e:?}");
tx.send(e).await.unwrap(); tx.send(e).await.unwrap();
}), }),
Err(e) => error!("Error from file_watcher: {e}"), Err(e) => error!("Error from file_watcher: {e}"),
@ -30,11 +31,11 @@ where
Config::default(), Config::default(),
)?; )?;
info!("Watching directory '{}'", dir.as_ref().display()); info!("watching");
let handle = 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) callback(ev)
} }
}); });
@ -44,9 +45,10 @@ where
} }
/// Only know when something changes /// Only know when something changes
pub fn file_monitor<P, F>(dir: P, cb: F) -> Result<FileWatcher> #[instrument(skip(callback))]
pub fn file_monitor<P, F>(dir: P, callback: F) -> Result<FileWatcher, notify::Error>
where where
P: AsRef<Path>, P: AsRef<Path> + std::fmt::Debug,
F: Fn() -> () + std::marker::Send + 'static, F: Fn() -> () + std::marker::Send + 'static,
{ {
let (tx, mut rx) = tokio::sync::watch::channel(()); let (tx, mut rx) = tokio::sync::watch::channel(());
@ -60,11 +62,11 @@ where
Config::default(), Config::default(),
)?; )?;
info!("Watching directory '{}'", dir.as_ref().display()); info!("watching");
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
while let Ok(_) = rx.changed().await { while let Ok(_) = rx.changed().await {
cb(); callback();
// "good enough" debouncing // "good enough" debouncing
rx.mark_unchanged(); rx.mark_unchanged();
} }

View file

@ -1,11 +1,9 @@
use crate::prelude::*; use crate::prelude::*;
mod cli; mod cli;
mod error;
mod file_watcher; mod file_watcher;
mod observe; mod observe;
mod prelude; mod prelude;
mod result;
mod router; mod router;
mod state; mod state;
mod static_files; mod static_files;
@ -14,7 +12,6 @@ mod templates;
mod webserver; mod webserver;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> AnonResult<()> {
let _setup_logging = observe::setup_logging(); Ok(cli::cli().exec().await?)
cli::cli()?.exec().await
} }

View file

@ -1,5 +1,5 @@
#![allow(unused_imports)] #![allow(unused_imports)]
pub use crate::error::Error; pub use color_eyre::eyre::Result as AnonResult;
pub use crate::result::Result; pub use std::result::Result;
pub use tracing::{debug, error, event, info, span, warn, Level}; pub use tracing::{debug, error, event, info, instrument, span, trace, warn, Level};

View file

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

View file

@ -1,9 +1,53 @@
use crate::{file_watcher::FileWatcher, prelude::*, state::State as AppState, static_files}; use crate::{
use axum::{extract::State, response::Html, routing::get, Router}; 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 minijinja::context;
use tower_livereload::LiveReloadLayer; use tower_livereload::LiveReloadLayer;
pub async fn router(with_watchers: bool) -> Result<(Router, Vec<Option<FileWatcher>>)> { 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<axum::body::Body> {
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<T> = Result<T, ReqError>;
pub async fn router(
with_watchers: bool,
) -> Result<(Router, Vec<Option<FileWatcher>>), NewRouterError> {
let state = AppState::try_new().await?; let state = AppState::try_new().await?;
let live_reload_layer: Option<LiveReloadLayer> = if with_watchers { let live_reload_layer: Option<LiveReloadLayer> = if with_watchers {
@ -38,7 +82,7 @@ pub async fn router(with_watchers: bool) -> Result<(Router, Vec<Option<FileWatch
Ok((result, watchers)) Ok((result, watchers))
} }
async fn index(State(state): State<AppState>) -> Result<Html<String>> { async fn index(State(state): State<AppState>) -> ReqResult<Html<String>> {
Ok(Html( Ok(Html(
state state
.templates .templates
@ -46,7 +90,7 @@ async fn index(State(state): State<AppState>) -> Result<Html<String>> {
.await?, .await?,
)) ))
} }
async fn about(State(state): State<AppState>) -> Result<Html<String>> { async fn about(State(state): State<AppState>) -> ReqResult<Html<String>> {
Ok(Html( Ok(Html(
state state
.templates .templates

View file

@ -1,4 +1,7 @@
use crate::{prelude::*, templates::Templates}; use crate::{
prelude::*,
templates::{TemplateLoadError, Templates},
};
use std::sync::Arc; use std::sync::Arc;
#[derive(Clone)] #[derive(Clone)]
@ -7,9 +10,17 @@ pub struct State {
} }
impl State { impl State {
pub async fn try_new() -> Result<Self> { pub async fn try_new() -> Result<Self, NewStateError> {
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 })
} }
} }
use thiserror::Error;
#[derive(Error, Debug)]
pub enum NewStateError {
#[error("template load error: {0}")]
TemplateLoad(#[from] TemplateLoadError),
}

View file

@ -15,7 +15,7 @@ fn static_file_dir() -> &'static PathBuf {
STATIC_FILE_DIR.get_or_init(|| PathBuf::from_str("static").unwrap()) STATIC_FILE_DIR.get_or_init(|| PathBuf::from_str("static").unwrap())
} }
pub fn router(reloader: Option<Reloader>) -> Result<(Router, Option<FileWatcher>)> { pub fn router(reloader: Option<Reloader>) -> Result<(Router, Option<FileWatcher>), notify::Error> {
let watcher = if let Some(rl) = reloader { let watcher = if let Some(rl) = reloader {
// TODO: debounce? // TODO: debounce?
Some(file_monitor(static_file_dir(), move || { Some(file_monitor(static_file_dir(), move || {

View file

@ -1,12 +1,13 @@
use crate::prelude::*;
use std::process::Stdio; use std::process::Stdio;
use tokio::{ use tokio::{
io::{AsyncBufReadExt, BufReader}, io::{AsyncBufReadExt, BufReader},
process::Command, process::Command,
}; };
use tracing::{error, event, info, Level};
#[instrument]
pub fn start_watcher() { pub fn start_watcher() {
info!("Starting tailwind..."); info!("starting");
match Command::new("tailwindcss") match Command::new("tailwindcss")
.args(["-i", "src/style.css", "-o", "static/style.css", "--watch"]) .args(["-i", "src/style.css", "-o", "static/style.css", "--watch"])
.stdout(Stdio::piped()) .stdout(Stdio::piped())
@ -14,7 +15,7 @@ pub fn start_watcher() {
.spawn() .spawn()
{ {
Ok(mut tw) => { Ok(mut tw) => {
info!("Tailwind spawned!"); info!("spawned");
let mut stdout_reader = BufReader::new(tw.stdout.take().unwrap()).lines(); let mut stdout_reader = BufReader::new(tw.stdout.take().unwrap()).lines();
tokio::spawn(async move { tokio::spawn(async move {
while let Ok(Some(l)) = stdout_reader.next_line().await { while let Ok(Some(l)) = stdout_reader.next_line().await {

View file

@ -1,10 +1,23 @@
use crate::{file_watcher::prelude::*, prelude::*}; use crate::{file_watcher::prelude::*, prelude::*};
use minijinja::Environment; use minijinja::Environment;
use pathdiff::diff_paths; 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 tokio::sync::Mutex;
use tower_livereload::Reloader; 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)] #[derive(Clone, Debug)]
pub struct Templates { pub struct Templates {
env: Arc<Mutex<Environment<'static>>>, env: Arc<Mutex<Environment<'static>>>,
@ -20,7 +33,7 @@ impl Templates {
} }
} }
pub async fn try_load<P: Into<PathBuf>>(dir: P) -> Result<Self> { pub async fn try_load<P: Into<PathBuf>>(dir: P) -> Result<Self, TemplateLoadError> {
let result = Self::for_dir(dir); let result = Self::for_dir(dir);
result.load_env().await?; result.load_env().await?;
Ok(result) Ok(result)
@ -29,7 +42,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>,
) -> Result<Option<FileWatcher>> { ) -> Result<Option<FileWatcher>, notify::Error> {
if let Some(rl) = reloader { if let Some(rl) = reloader {
Ok(Some(self.watch(rl).await?)) Ok(Some(self.watch(rl).await?))
} else { } else {
@ -37,41 +50,50 @@ impl Templates {
} }
} }
async fn watch(self: Arc<Self>, reloader: Reloader) -> Result<FileWatcher> { async fn watch(self: Arc<Self>, reloader: Reloader) -> Result<FileWatcher, notify::Error> {
// TODO: only reload template that changed? let watcher = file_watcher(self.dir.clone(), move |ev| {
let watcher = file_monitor(self.dir.clone(), move || {
futures::executor::block_on(async { futures::executor::block_on(async {
self.load_env() if ev.kind.is_create() || ev.kind.is_modify() {
.await for p in ev.paths {
.expect("Failed to reload templates after template changed during runtime"); let _ = self.load_template(&p).await;
reloader.reload(); }
reloader.reload();
}
}); });
})?; })?;
Ok(watcher) Ok(watcher)
} }
pub async fn load_env(&self) -> Result<()> { #[instrument(skip(self, p))]
info!("Loading templates..."); pub async fn load_template<P>(&self, p: P) -> Result<(), TemplateLoadError>
where
P: AsRef<Path> + 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) { for d in walkdir::WalkDir::new(&self.dir) {
match d { match d {
Ok(d) => { Ok(d) => self.load_template(d.path()).await?,
// 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())?)?;
}
Err(_) => todo!(), Err(_) => todo!(),
} }
} }
@ -82,7 +104,7 @@ impl Templates {
&self, &self,
template_name: &str, template_name: &str,
context: S, context: S,
) -> Result<String> { ) -> Result<String, minijinja::Error> {
Ok(self Ok(self
.env .env
.lock() .lock()

View file

@ -1,21 +1,21 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head hx-preserve="true">
<title>Sup</title> <title>Sup</title>
<link rel="stylesheet" type="text/css" href="/static/style.css" /> <link rel="stylesheet" type="text/css" href="/static/style.css" />
<script hx-preserve="true" src="https://unpkg.com/htmx.org@1.9.12"
integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2"
crossorigin="anonymous" defer></script>
</head> </head>
<body class="bg-base text-text"> <body class="bg-base text-text">
Page Template Page Template
<nav hx-boost="true"> <nav>
<a href="/">Index</a> <a href="/">Index</a>
<a href="/about">About</a> <a href="/about">About</a>
</nav> </nav>
{% block body %}{% endblock %} {% block body %}{% endblock %}
<script src="https://unpkg.com/htmx.org@1.9.12"
integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2"
crossorigin="anonymous"></script>
</body> </body>
</html> </html>

View file

@ -1,12 +1,23 @@
use std::io;
use crate::{prelude::*, tailwind}; use crate::{prelude::*, tailwind};
use axum::{serve, Router}; use axum::{serve, Router};
pub async fn webserver(router: Router, with_watchers: bool) -> Result<()> { #[instrument(skip(router))]
pub async fn webserver(
router: Router,
with_watchers: bool,
host: Option<&str>,
port: Option<u16>,
) -> Result<(), io::Error> {
if with_watchers { if with_watchers {
tokio::spawn(async move { tailwind::start_watcher() }); tokio::spawn(async move { tailwind::start_watcher() });
} }
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); let listener = tokio::net::TcpListener::bind((host.unwrap_or("::1"), port.unwrap_or(3000)))
info!("Listening on {listener:?}"); .await
.unwrap();
let addr = listener.local_addr()?;
info!(%addr);
Ok(serve(listener, router).await?) Ok(serve(listener, router).await?)
} }