diff --git a/Cargo.lock b/Cargo.lock index 7feb129..5e5f55e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -318,6 +318,7 @@ dependencies = [ "rss", "scc", "serde", + "serde-value", "serde_json", "syntect", "thiserror", @@ -1541,6 +1542,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "overload" version = "0.1.1" @@ -1930,6 +1940,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.206" diff --git a/Cargo.toml b/Cargo.toml index 3d1f7f0..55907a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ notify-debouncer-full = { version = "0.3.1", default-features = false } rss = "2.0.7" scc = { version = "2.1.0", features = ["serde"] } serde = { version = "1.0.197", features = ["derive"] } +serde-value = "0.7.0" serde_json = { version = "1.0.124", features = ["preserve_order"] } syntect = "5.2.0" thiserror = "1.0.58" diff --git a/src/app.rs b/src/app.rs index 772b8db..4a4a4a2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,6 +13,7 @@ use include_dir::{include_dir, Dir}; use rss::{Category, ChannelBuilder, ItemBuilder}; use serde::{Deserialize, Serialize}; use serde_json::Map; +use serde_value::Value; use tokio::sync::RwLock; use tower::service_fn; use tower_http::services::ServeDir; @@ -78,6 +79,8 @@ struct QueryParams { tag: Option, #[serde(rename = "n")] num_posts: Option, + #[serde(flatten)] + other: HashMap, } fn collect_tags(posts: &Vec) -> Map { @@ -130,7 +133,11 @@ async fn index( Query(query): Query, ) -> AppResult { let posts = posts - .get_max_n_post_metadata_with_optional_tag_sorted(query.num_posts, query.tag.as_deref()) + .get_max_n_post_metadata_with_optional_tag_sorted( + query.num_posts, + query.tag.as_deref(), + &query.other, + ) .await?; let tags = collect_tags(&posts); @@ -160,7 +167,11 @@ async fn all_posts( Query(query): Query, ) -> AppResult>> { let posts = posts - .get_max_n_post_metadata_with_optional_tag_sorted(query.num_posts, query.tag.as_deref()) + .get_max_n_post_metadata_with_optional_tag_sorted( + query.num_posts, + query.tag.as_deref(), + &query.other, + ) .await?; Ok(Json(posts)) @@ -181,6 +192,7 @@ async fn rss( .as_ref() .and(Some(Filter::Tags(query.tag.as_deref().as_slice()))) .as_slice(), + &query.other, ) .await?; @@ -234,8 +246,9 @@ async fn post( .. }): State, Path(name): Path, + Query(query): Query, ) -> AppResult { - match posts.get_post(&name).await? { + match posts.get_post(&name, &query.other).await? { ReturnedPost::Rendered(ref meta, rendered, rendered_in) => { let joined_tags = meta.tags.join(", "); @@ -256,7 +269,7 @@ async fn post( joined_tags, style: &config.style, raw_name: if config.markdown_access { - raw_name = posts.get_raw(&meta.name).await?; + raw_name = posts.as_raw(&meta.name).await?; raw_name.as_deref() } else { None diff --git a/src/post/blag.rs b/src/post/blag.rs index a704df3..196452a 100644 --- a/src/post/blag.rs +++ b/src/post/blag.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::Path; use std::process::Stdio; use std::sync::Arc; @@ -6,6 +7,7 @@ use axum::async_trait; use axum::http::HeaderValue; use futures::stream::FuturesUnordered; use futures::StreamExt; +use serde_value::Value; use tokio::fs::OpenOptions; use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader}; use tokio::time::Instant; @@ -32,6 +34,7 @@ impl PostManager for Blag { async fn get_all_posts( &self, filters: &[Filter<'_>], + query: &HashMap, ) -> Result, PostError> { let mut set = FuturesUnordered::new(); let mut meow = Vec::new(); @@ -57,7 +60,9 @@ impl PostManager for Blag { }; if name.ends_with(".sh") { - set.push(async move { self.get_post(name.trim_end_matches(".sh")).await }); + set.push( + async move { self.get_post(name.trim_end_matches(".sh"), query).await }, + ); } } } @@ -84,7 +89,11 @@ impl PostManager for Blag { Ok(meow) } - async fn get_post(&self, name: &str) -> Result { + async fn get_post( + &self, + name: &str, + _query: &HashMap, + ) -> Result { let mut path = self.root.join(name); if name.ends_with(".sh") { @@ -157,7 +166,7 @@ impl PostManager for Blag { )) } - async fn get_raw(&self, name: &str) -> Result, PostError> { + async fn as_raw(&self, name: &str) -> Result, PostError> { let mut buf = String::with_capacity(name.len() + 3); buf += name; buf += ".sh"; diff --git a/src/post/markdown_posts.rs b/src/post/markdown_posts.rs index 71414e6..0f7f31e 100644 --- a/src/post/markdown_posts.rs +++ b/src/post/markdown_posts.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashMap}; use std::hash::{DefaultHasher, Hash, Hasher}; use std::io; use std::path::Path; @@ -14,6 +14,7 @@ use color_eyre::eyre::{self, Context}; use comrak::plugins::syntect::SyntectAdapter; use fronma::parser::{parse, ParsedData}; use serde::Deserialize; +use serde_value::Value; use tokio::fs; use tokio::io::AsyncReadExt; use tracing::warn; @@ -140,6 +141,7 @@ impl PostManager for MarkdownPosts { async fn get_all_posts( &self, filters: &[Filter<'_>], + query: &HashMap, ) -> Result, PostError> { let mut posts = Vec::new(); @@ -156,7 +158,7 @@ impl PostManager for MarkdownPosts { .to_string_lossy() .to_string(); - let post = self.get_post(&name).await?; + let post = self.get_post(&name, query).await?; if let ReturnedPost::Rendered(meta, content, stats) = post && meta.apply_filters(filters) { @@ -171,6 +173,7 @@ impl PostManager for MarkdownPosts { async fn get_all_post_metadata( &self, filters: &[Filter<'_>], + _query: &HashMap, ) -> Result, PostError> { let mut posts = Vec::new(); @@ -211,7 +214,11 @@ impl PostManager for MarkdownPosts { Ok(posts) } - async fn get_post(&self, name: &str) -> Result { + async fn get_post( + &self, + name: &str, + _query: &HashMap, + ) -> Result { if self.config.markdown_access && name.ends_with(".md") { let path = self.config.dirs.posts.join(name); @@ -287,7 +294,7 @@ impl PostManager for MarkdownPosts { } } - async fn get_raw(&self, name: &str) -> Result, PostError> { + async fn as_raw(&self, name: &str) -> Result, PostError> { let mut buf = String::with_capacity(name.len() + 3); buf += name; buf += ".md"; diff --git a/src/post/mod.rs b/src/post/mod.rs index 9009ca4..1e51590 100644 --- a/src/post/mod.rs +++ b/src/post/mod.rs @@ -2,11 +2,12 @@ pub mod blag; pub mod cache; pub mod markdown_posts; -use std::time::Duration; +use std::{collections::HashMap, time::Duration}; use axum::{async_trait, http::HeaderValue}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use serde_value::Value; use crate::error::PostError; pub use blag::Blag; @@ -74,8 +75,9 @@ pub trait PostManager { async fn get_all_post_metadata( &self, filters: &[Filter<'_>], + query: &HashMap, ) -> Result, PostError> { - self.get_all_posts(filters) + self.get_all_posts(filters, query) .await .map(|vec| vec.into_iter().map(|(meta, ..)| meta).collect()) } @@ -83,15 +85,19 @@ pub trait PostManager { async fn get_all_posts( &self, filters: &[Filter<'_>], + query: &HashMap, ) -> Result, PostError>; async fn get_max_n_post_metadata_with_optional_tag_sorted( &self, n: Option, tag: Option<&str>, + query: &HashMap, ) -> Result, PostError> { let filters = tag.and(Some(Filter::Tags(tag.as_slice()))); - let mut posts = self.get_all_post_metadata(filters.as_slice()).await?; + let mut posts = self + .get_all_post_metadata(filters.as_slice(), query) + .await?; // we still want some semblance of order if created_at is None so sort by mtime as well posts.sort_unstable_by_key(|metadata| metadata.modified_at.unwrap_or_default()); posts.sort_by_key(|metadata| metadata.created_at.unwrap_or_default()); @@ -104,19 +110,27 @@ pub trait PostManager { } #[allow(unused)] - async fn get_post_metadata(&self, name: &str) -> Result { - match self.get_post(name).await? { + async fn get_post_metadata( + &self, + name: &str, + query: &HashMap, + ) -> Result { + match self.get_post(name, query).await? { ReturnedPost::Rendered(metadata, ..) => Ok(metadata), ReturnedPost::Raw(..) => Err(PostError::NotFound(name.to_string())), } } - async fn get_post(&self, name: &str) -> Result; + async fn get_post( + &self, + name: &str, + query: &HashMap, + ) -> Result; async fn cleanup(&self) {} #[allow(unused)] - async fn get_raw(&self, name: &str) -> Result, PostError> { + async fn as_raw(&self, name: &str) -> Result, PostError> { Ok(None) } }