2024-05-15 17:13:41 -05:00
|
|
|
use std::time::Duration;
|
|
|
|
|
2024-05-15 16:48:23 -05:00
|
|
|
use crate::{
|
|
|
|
file_watcher::FileWatcher,
|
|
|
|
prelude::*,
|
|
|
|
state::{NewStateError, State as AppState},
|
|
|
|
static_files,
|
|
|
|
};
|
|
|
|
use axum::{
|
2024-05-15 17:13:41 -05:00
|
|
|
body::Bytes,
|
|
|
|
extract::{MatchedPath, State},
|
|
|
|
http::{HeaderMap, Request, Response, StatusCode},
|
2024-05-15 16:48:23 -05:00
|
|
|
response::{Html, IntoResponse},
|
|
|
|
routing::get,
|
|
|
|
Router,
|
|
|
|
};
|
2024-05-14 14:30:03 -05:00
|
|
|
use minijinja::context;
|
2024-05-15 17:13:41 -05:00
|
|
|
use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer};
|
2024-05-14 14:30:03 -05:00
|
|
|
use tower_livereload::LiveReloadLayer;
|
|
|
|
|
2024-05-15 16:48:23 -05:00
|
|
|
use thiserror::Error;
|
2024-05-15 17:13:41 -05:00
|
|
|
use tracing::{info_span, Span};
|
2024-05-15 16:48:23 -05:00
|
|
|
|
|
|
|
#[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> {
|
2024-05-14 14:30:03 -05:00
|
|
|
let state = AppState::try_new().await?;
|
|
|
|
|
2024-05-14 15:33:49 -05:00
|
|
|
let live_reload_layer: Option<LiveReloadLayer> = if with_watchers {
|
|
|
|
Some(LiveReloadLayer::new())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
2024-05-14 14:30:03 -05:00
|
|
|
|
2024-05-14 15:33:49 -05:00
|
|
|
let orl = || {
|
|
|
|
if let Some(lr) = &live_reload_layer {
|
|
|
|
Some(lr.reloader())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let template_file_watcher = state.clone().templates.start_watcher(orl()).await?;
|
|
|
|
let (static_file_service, static_file_watcher) = static_files::router(orl())?;
|
|
|
|
|
|
|
|
let mut result = Router::new()
|
2024-05-14 14:30:03 -05:00
|
|
|
.route("/", get(index))
|
2024-05-14 16:56:22 -05:00
|
|
|
.route("/about", get(about))
|
2024-05-14 14:30:03 -05:00
|
|
|
.nest_service("/static", static_file_service)
|
2024-05-15 17:13:41 -05:00
|
|
|
// `TraceLayer` is provided by tower-http so you have to add that as a dependency.
|
|
|
|
// It provides good defaults but is also very customizable.
|
|
|
|
//
|
|
|
|
// See https://docs.rs/tower-http/0.1.1/tower_http/trace/index.html for more details.
|
|
|
|
//
|
|
|
|
// If you want to customize the behavior using closures here is how.
|
|
|
|
.layer(
|
|
|
|
TraceLayer::new_for_http()
|
|
|
|
.make_span_with(|request: &Request<_>| {
|
|
|
|
// Log the matched route's path (with placeholders not filled in).
|
|
|
|
// Use request.uri() or OriginalUri if you want the real path.
|
|
|
|
let matched_path = request
|
|
|
|
.extensions()
|
|
|
|
.get::<MatchedPath>()
|
|
|
|
.map(MatchedPath::as_str);
|
|
|
|
|
|
|
|
info_span!(
|
|
|
|
"http_request",
|
|
|
|
method = ?request.method(),
|
|
|
|
matched_path,
|
|
|
|
some_other_field = tracing::field::Empty,
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.on_request(|_request: &Request<_>, _span: &Span| {
|
|
|
|
// You can use `_span.record("some_other_field", value)` in one of these
|
|
|
|
// closures to attach a value to the initially empty field in the info_span
|
|
|
|
// created above.
|
|
|
|
})
|
|
|
|
.on_response(|response: &Response<_>, latency: Duration, _span: &Span| {
|
|
|
|
trace!("on_response: {response:?} in {latency:?}");
|
|
|
|
})
|
|
|
|
.on_body_chunk(|_chunk: &Bytes, _latency: Duration, _span: &Span| {
|
|
|
|
// ...
|
|
|
|
})
|
|
|
|
.on_eos(
|
|
|
|
|_trailers: Option<&HeaderMap>, _stream_duration: Duration, _span: &Span| {
|
|
|
|
// ...
|
|
|
|
},
|
|
|
|
)
|
|
|
|
.on_failure(
|
|
|
|
|error: ServerErrorsFailureClass, latency: Duration, _span: &Span| {
|
|
|
|
error!("on_failure: {error:?} in {latency:?}");
|
|
|
|
// ...
|
|
|
|
},
|
|
|
|
),
|
|
|
|
)
|
2024-05-14 15:33:49 -05:00
|
|
|
.with_state(state.clone());
|
|
|
|
|
|
|
|
if let Some(lr) = live_reload_layer {
|
|
|
|
result = result.clone().layer(lr);
|
|
|
|
}
|
|
|
|
|
|
|
|
let watchers = vec![template_file_watcher, static_file_watcher];
|
|
|
|
|
|
|
|
Ok((result, watchers))
|
2024-05-14 14:30:03 -05:00
|
|
|
}
|
|
|
|
|
2024-05-15 17:13:41 -05:00
|
|
|
#[instrument(skip(state))]
|
2024-05-15 16:48:23 -05:00
|
|
|
async fn index(State(state): State<AppState>) -> ReqResult<Html<String>> {
|
2024-05-14 14:30:03 -05:00
|
|
|
Ok(Html(
|
|
|
|
state
|
|
|
|
.templates
|
2024-05-14 17:14:37 -05:00
|
|
|
.render("pages/index.jinja.html", context!())
|
2024-05-14 14:30:03 -05:00
|
|
|
.await?,
|
|
|
|
))
|
|
|
|
}
|
2024-05-15 17:13:41 -05:00
|
|
|
|
|
|
|
#[instrument(skip(state))]
|
2024-05-15 16:48:23 -05:00
|
|
|
async fn about(State(state): State<AppState>) -> ReqResult<Html<String>> {
|
2024-05-14 16:56:22 -05:00
|
|
|
Ok(Html(
|
|
|
|
state
|
|
|
|
.templates
|
2024-05-14 17:14:37 -05:00
|
|
|
.render("pages/about.jinja.html", context!())
|
2024-05-14 16:56:22 -05:00
|
|
|
.await?,
|
|
|
|
))
|
|
|
|
}
|