use std::time::Duration; use crate::{ file_watcher::FileWatcher, prelude::*, state::{NewStateError, State as AppState}, static_files, }; use axum::{ body::Bytes, extract::{MatchedPath, State}, http::{HeaderMap, Request, Response, StatusCode}, response::{Html, IntoResponse}, routing::get, Router, }; use minijinja::context; use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer}; use tower_livereload::LiveReloadLayer; use thiserror::Error; use tracing::{info_span, Span}; #[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 { Some(LiveReloadLayer::new()) } else { None }; 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() .route("/", get(index)) .route("/about", get(about)) .nest_service("/static", static_file_service) // `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::() .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:?}"); // ... }, ), ) .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)) } #[instrument(skip(state))] async fn index(State(state): State) -> ReqResult> { Ok(Html( state .templates .render("pages/index.jinja.html", context!()) .await?, )) } #[instrument(skip(state))] async fn about(State(state): State) -> ReqResult> { Ok(Html( state .templates .render("pages/about.jinja.html", context!()) .await?, )) }