#![warn(clippy::pedantic)] use std::{collections::HashMap, fmt::Display, sync::Arc}; use axum::http::Uri; use chrono::DateTime; use config::Config; use post::Post; use tag::Tag; use tera::Tera; use tower_http::{compression::CompressionLayer, cors::CorsLayer}; use tracing::{instrument, log::info}; use anyhow::{Error, Result}; mod feed; mod handlers; mod helpers; mod hilighting; mod markdown; mod observability; mod post; mod tag; #[derive(Default)] pub struct AppState { startup_time: DateTime, base_url: Uri, posts: HashMap, tags: HashMap, tera: Tera, } #[tokio::main] async fn main() -> Result<()> { let cfg = Config::builder() .add_source(config::File::with_name("config.toml")) .add_source(config::Environment::with_prefix("WEBSITE")) .build()?; observability::init_tracing(&cfg); info!("Starting server..."); let addr = cfg.get_string("bind_address")?; let app = init_app(&cfg).await?; let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, app.into_make_service()).await?; Ok(()) } #[instrument] async fn init_app(cfg: &Config) -> Result { let base_url: Uri = cfg.get_string("base_url")?.parse().unwrap(); let tera = Tera::new("templates/**/*")?; let mut state = AppState { startup_time: chrono::offset::Utc::now(), base_url, tera, ..Default::default() }; let posts = post::load_all(&state).await?; let tags = tag::get_tags(posts.values()); state.posts = posts; state.tags = tags; let state = Arc::new(state); info!("Listening at {}", state.base_url); Ok(handlers::routes(&state) .layer(CorsLayer::permissive()) .layer(CompressionLayer::new()) .layer( tower_http::trace::TraceLayer::new_for_http() .make_span_with(observability::make_span) .on_response(observability::on_response), ) .with_state(state.clone())) } #[derive(Debug)] pub enum WebsiteError { NotFound, InternalError(Error), } impl std::error::Error for WebsiteError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { WebsiteError::NotFound => None, WebsiteError::InternalError(e) => Some(e.as_ref()), } } } impl Display for WebsiteError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { WebsiteError::NotFound => write!(f, "Not found"), WebsiteError::InternalError(e) => write!(f, "Internal error: {e}"), } } } impl From for WebsiteError { fn from(value: tera::Error) -> Self { WebsiteError::InternalError(value.into()) } } impl From for WebsiteError { fn from(value: Error) -> Self { WebsiteError::InternalError(value) } }