use crate::{ datasources::posts::PostsDatasource, state::AppState, utils::{ datetime::*, rss, sitemap::{self, SitemapEntry}, }, }; use axum::http::{HeaderMap, HeaderValue}; use axum::{ extract::{Path, State}, http::{header, StatusCode}, response::IntoResponse, routing::get, Json, Router, }; use chrono::Utc; use fred::types::Expiration; use serde::{Deserialize, Serialize}; use std::collections::HashMap; #[derive(sqlx::FromRow, Deserialize, Serialize, Debug, Clone)] pub struct Post { pub post_id: i32, pub author_id: Option, pub first_name: Option, pub last_name: Option, pub title: String, pub body: String, #[serde(serialize_with = "serialize_datetime")] #[serde(deserialize_with = "deserialize_datetime")] pub created_at: Option>, } #[derive(sqlx::FromRow, Deserialize, Serialize, Debug, Clone)] pub struct PostFeaturedVariant { pub post_id: i32, pub author_id: Option, pub first_name: Option, pub last_name: Option, pub title: String, pub body: String, #[serde(serialize_with = "serialize_datetime")] #[serde(deserialize_with = "deserialize_datetime")] pub created_at: Option>, pub is_featured: Option, } #[derive(Deserialize)] pub struct PostGetOneParams { pub id: i32, } pub struct PostsRoute; impl PostsRoute { pub fn routes(app_state: &AppState) -> Router { // add more post routes here! Router::new() .route("/all", get(PostsRoute::get_all)) .route("/:id", get(PostsRoute::get_one)) .route("/recent", get(PostsRoute::get_recent_posts)) .route("/popular", get(PostsRoute::get_popular_posts)) .route("/hot", get(PostsRoute::get_hot_posts)) .route("/featured", get(PostsRoute::get_featured_posts)) .route("/rss", get(PostsRoute::get_rss_posts)) .route("/sitemap", get(PostsRoute::get_sitemap)) .with_state(app_state.clone()) } // get all posts async fn get_all(State(app_state): State) -> impl IntoResponse { let mut state = app_state.lock().await; let cached: Option> = state .cache .get(String::from("posts:all")) .await .unwrap_or(None); if let Some(posts) = cached { tracing::info!("grabbing all posts from cache"); return Ok(Json(posts)); }; match PostsDatasource::get_all(&state.database).await { Ok(posts) => { tracing::info!("grabbing all posts from database"); if let p = &posts { let posts = p.clone(); let state = app_state.clone(); tracing::info!("storing database data in cache"); tokio::spawn(async move { let mut s = state.lock().await; let _ = s .cache .set( String::from("posts:all"), &posts, Some(Expiration::EX(10)), None, false, ) .await; }); }; Ok(Json(posts)) } Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), } } // get one post async fn get_one( State(app_state): State, Path(params): Path, ) -> impl IntoResponse { let mut state = app_state.lock().await; let cached: Option = state .cache .get(format!("posts:{}", params.id)) .await .unwrap_or(None); if let Some(post) = cached { tracing::info!("grabbing one post from cache"); return Ok(Json(post)); }; match PostsDatasource::get_one(&state.database, params.id).await { Ok(post) => { tracing::info!("grabbing one post from database"); if let p = &post { let post = p.clone(); let state = app_state.clone(); tracing::info!("storing database data in cache"); tokio::spawn(async move { let mut s = state.lock().await; let _ = s .cache .set( format!("posts:{}", params.id), &post, Some(Expiration::EX(10)), None, false, ) .await; }); }; Ok(Json(post)) } Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), } } // get recent posts async fn get_recent_posts(State(app_state): State) -> impl IntoResponse { let mut state = app_state.lock().await; let cached: Option> = state .cache .get(String::from("posts:recent")) .await .unwrap_or(None); if let Some(posts) = cached { tracing::info!("grabbing recent posts from cache"); return Ok(Json(posts)); }; match PostsDatasource::get_recent(&state.database).await { Ok(posts) => { tracing::info!("grabbing recent posts from database"); if let p = &posts { let posts = p.clone(); let state = app_state.clone(); tracing::info!("storing database data in cache"); tokio::spawn(async move { let mut s = state.lock().await; let _ = s .cache .set( String::from("posts:recent"), &posts, Some(Expiration::EX(5)), None, false, ) .await; }); }; Ok(Json(posts)) } Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), } } // get the top three posts with the highest view count async fn get_popular_posts(State(app_state): State) -> impl IntoResponse { let mut state = app_state.lock().await; let cached: Option> = state .cache .get(String::from("posts:popular")) .await .unwrap_or(None); if let Some(posts) = cached { tracing::info!("grabbing popular posts from cache"); return Ok(Json(posts)); }; match PostsDatasource::get_popular(&state.database).await { Ok(posts) => { tracing::info!("grabbing popular posts from database"); if let p = &posts { let posts = p.clone(); let state = app_state.clone(); tracing::info!("storing database data in cache"); tokio::spawn(async move { let mut s = state.lock().await; let _ = s .cache .set( String::from("posts:popular"), &posts, Some(Expiration::EX(5)), None, false, ) .await; }); }; Ok(Json(posts)) } Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), } } // get the top three posts with the most comments async fn get_hot_posts(State(app_state): State) -> impl IntoResponse { let mut state = app_state.lock().await; let cached: Option> = state .cache .get(String::from("posts:hot")) .await .unwrap_or(None); if let Some(posts) = cached { tracing::info!("grabbing hot posts from cache"); return Ok(Json(posts)); }; match PostsDatasource::get_hot(&state.database).await { Ok(posts) => { tracing::info!("grabbing hot posts from database"); if let p = &posts { let posts = p.clone(); let state = app_state.clone(); tracing::info!("storing database data in cache"); tokio::spawn(async move { let mut s = state.lock().await; let _ = s .cache .set( String::from("posts:hot"), &posts, Some(Expiration::EX(5)), None, false, ) .await; }); }; Ok(Json(posts)) } Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), } } // get posts that are featured async fn get_featured_posts(State(app_state): State) -> impl IntoResponse { let mut state = app_state.lock().await; let cached: Option> = state .cache .get(String::from("posts:featured")) .await .unwrap_or(None); if let Some(posts) = cached { tracing::info!("grabbing featured posts from cache"); return Ok(Json(posts)); }; match PostsDatasource::get_featured(&state.database).await { Ok(posts) => { tracing::info!("grabbing featured posts from database"); if let p = &posts { let posts = p.clone(); let state = app_state.clone(); tracing::info!("storing database data in cache"); tokio::spawn(async move { let mut s = state.lock().await; let _ = s .cache .set( String::from("posts:featured"), &posts, Some(Expiration::EX(5)), None, false, ) .await; }); }; Ok(Json(posts)) } Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), } } // get rss posts async fn get_rss_posts(State(app_state): State) -> impl IntoResponse { let state = app_state.lock().await; match PostsDatasource::get_all(&state.database).await { Ok(posts) => { let web_url = std::env::var("BASE_URI_WEB").expect("Environment BASE_URI_WEB variable found"); let mapped_posts: HashMap = posts .into_iter() .map(|post| (post.post_id.to_string(), post)) .collect(); let xml: String = rss::generate_rss( "Wyatt's blog", "Wyatt and friends", web_url.as_str(), &mapped_posts, ); let mut headers = HeaderMap::new(); headers.insert( header::CONTENT_TYPE, HeaderValue::from_static("application/xml"), ); (headers, xml) } Err(e) => { let mut headers = HeaderMap::new(); headers.insert("Content-Type", HeaderValue::from_static("text/plain")); (headers, e.to_string()) } } } async fn get_sitemap(State(app_state): State) -> impl IntoResponse { let state = app_state.lock().await; // let cached: Option> = None; // TODO: maybe implement cache, later?? match PostsDatasource::get_all(&state.database).await { Ok(posts) => { let web_url = std::env::var("BASE_URI_WEB").expect("Environment BASE_URI_WEB variable found"); let mut entries: HashMap = posts .into_iter() .map(|p| { ( p.post_id.to_string(), SitemapEntry { location: format!("{}/posts/{}", web_url, p.post_id.to_string()), lastmod: p.created_at.unwrap_or_else(|| chrono::Utc::now()), }, ) }) .collect(); sitemap::get_static_pages(&mut entries, &web_url); let xml: String = sitemap::generate_sitemap(&entries); let mut headers = HeaderMap::new(); headers.insert( header::CONTENT_TYPE, HeaderValue::from_static("application/xml"), ); (headers, xml) } Err(e) => { let mut headers = HeaderMap::new(); headers.insert("Content-Type", HeaderValue::from_static("text/plain")); (headers, e.to_string()) } } } }