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",
"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",

View file

@ -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"] }

View file

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

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 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<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
P: AsRef<Path>,
P: AsRef<Path> + 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<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
P: AsRef<Path>,
P: AsRef<Path> + 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();
}

View file

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

View file

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

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 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<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 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))
}
async fn index(State(state): State<AppState>) -> Result<Html<String>> {
async fn index(State(state): State<AppState>) -> ReqResult<Html<String>> {
Ok(Html(
state
.templates
@ -46,7 +90,7 @@ async fn index(State(state): State<AppState>) -> Result<Html<String>> {
.await?,
))
}
async fn about(State(state): State<AppState>) -> Result<Html<String>> {
async fn about(State(state): State<AppState>) -> ReqResult<Html<String>> {
Ok(Html(
state
.templates

View file

@ -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<Self> {
pub async fn try_new() -> Result<Self, NewStateError> {
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),
}

View file

@ -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<Reloader>) -> Result<(Router, Option<FileWatcher>)> {
pub fn router(reloader: Option<Reloader>) -> Result<(Router, Option<FileWatcher>), notify::Error> {
let watcher = if let Some(rl) = reloader {
// TODO: debounce?
Some(file_monitor(static_file_dir(), move || {

View file

@ -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 {

View file

@ -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<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);
result.load_env().await?;
Ok(result)
@ -29,7 +42,7 @@ impl Templates {
pub async fn start_watcher(
self: Arc<Self>,
reloader: Option<Reloader>,
) -> Result<Option<FileWatcher>> {
) -> Result<Option<FileWatcher>, 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<Self>, reloader: Reloader) -> Result<FileWatcher> {
// TODO: only reload template that changed?
let watcher = file_monitor(self.dir.clone(), move || {
async fn watch(self: Arc<Self>, reloader: Reloader) -> Result<FileWatcher, notify::Error> {
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<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) {
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<String> {
) -> Result<String, minijinja::Error> {
Ok(self
.env
.lock()

View file

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

View file

@ -1,12 +1,23 @@
use std::io;
use crate::{prelude::*, tailwind};
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 {
tokio::spawn(async move { tailwind::start_watcher() });
}
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
info!("Listening on {listener:?}");
let listener = tokio::net::TcpListener::bind((host.unwrap_or("::1"), port.unwrap_or(3000)))
.await
.unwrap();
let addr = listener.local_addr()?;
info!(%addr);
Ok(serve(listener, router).await?)
}