feat: entry file parsing and directory reading
This commit is contained in:
parent
16a9699fc6
commit
7346141de5
2 changed files with 187 additions and 0 deletions
186
src/entries.rs
Normal file
186
src/entries.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Status {
|
||||
Pending,
|
||||
Approved,
|
||||
Denied,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EntryMeta {
|
||||
pub name: String,
|
||||
pub date: String,
|
||||
#[serde(default)]
|
||||
pub website: String,
|
||||
pub status: Status,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Entry {
|
||||
pub id: String,
|
||||
pub meta: EntryMeta,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
/// Parse an entry from file contents. `id` is derived from the filename.
|
||||
pub fn parse(id: &str, contents: &str) -> Result<Self, String> {
|
||||
let contents = contents.trim();
|
||||
if !contents.starts_with("+++") {
|
||||
return Err("missing opening +++".into());
|
||||
}
|
||||
let rest = &contents[3..];
|
||||
let end = rest.find("+++").ok_or("missing closing +++")?;
|
||||
let frontmatter = &rest[..end];
|
||||
let body = rest[end + 3..].trim_start_matches('\n').to_string();
|
||||
let meta: EntryMeta =
|
||||
toml::from_str(frontmatter).map_err(|e| format!("bad frontmatter: {e}"))?;
|
||||
Ok(Entry {
|
||||
id: id.to_string(),
|
||||
meta,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
/// Serialize entry back to file format.
|
||||
pub fn to_file_contents(&self) -> String {
|
||||
let frontmatter = toml::to_string_pretty(&self.meta).unwrap();
|
||||
format!("+++\n{frontmatter}+++\n{}", self.body)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read all entries from the given directory.
|
||||
pub fn read_entries(dir: &Path) -> Vec<Entry> {
|
||||
let mut entries = Vec::new();
|
||||
let read_dir = match std::fs::read_dir(dir) {
|
||||
Ok(rd) => rd,
|
||||
Err(_) => return entries,
|
||||
};
|
||||
for item in read_dir {
|
||||
let Ok(item) = item else { continue };
|
||||
let path = item.path();
|
||||
if path.extension().and_then(|e| e.to_str()) != Some("txt") {
|
||||
continue;
|
||||
}
|
||||
let id = path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let Ok(contents) = std::fs::read_to_string(&path) else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(entry) = Entry::parse(&id, &contents) {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
entries.sort_by(|a, b| b.meta.date.cmp(&a.meta.date));
|
||||
entries
|
||||
}
|
||||
|
||||
/// Read approved entries only.
|
||||
pub fn read_approved(dir: &Path) -> Vec<Entry> {
|
||||
read_entries(dir)
|
||||
.into_iter()
|
||||
.filter(|e| e.meta.status == Status::Approved)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Find an entry file by short ID prefix and update its status.
|
||||
pub fn set_status(dir: &Path, short_id: &str, status: Status) -> Result<String, String> {
|
||||
let read_dir = std::fs::read_dir(dir).map_err(|e| e.to_string())?;
|
||||
for item in read_dir {
|
||||
let Ok(item) = item else { continue };
|
||||
let path = item.path();
|
||||
let fname = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
|
||||
if fname.contains(short_id) && fname.ends_with(".txt") {
|
||||
let contents = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||
let id = path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let mut entry = Entry::parse(&id, &contents)?;
|
||||
entry.meta.status = status;
|
||||
std::fs::write(&path, entry.to_file_contents()).map_err(|e| e.to_string())?;
|
||||
return Ok(entry.meta.name.clone());
|
||||
}
|
||||
}
|
||||
Err("Not found.".into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_entry() {
|
||||
let contents = r#"+++
|
||||
name = "alice"
|
||||
date = "2026-04-09"
|
||||
website = "https://example.com"
|
||||
status = "approved"
|
||||
+++
|
||||
Hello world!"#;
|
||||
let entry = Entry::parse("2026-04-09-abcd1234", contents).unwrap();
|
||||
assert_eq!(entry.id, "2026-04-09-abcd1234");
|
||||
assert_eq!(entry.meta.name, "alice");
|
||||
assert_eq!(entry.meta.date, "2026-04-09");
|
||||
assert_eq!(entry.meta.website, "https://example.com");
|
||||
assert_eq!(entry.meta.status, Status::Approved);
|
||||
assert_eq!(entry.body, "Hello world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_entry_no_website() {
|
||||
let contents = r#"+++
|
||||
name = "bob"
|
||||
date = "2026-04-09"
|
||||
status = "pending"
|
||||
+++
|
||||
Just a message."#;
|
||||
let entry = Entry::parse("2026-04-09-ef567890", contents).unwrap();
|
||||
assert_eq!(entry.meta.website, "");
|
||||
assert_eq!(entry.meta.status, Status::Pending);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_entry_with_html() {
|
||||
let contents = r#"+++
|
||||
name = "carol"
|
||||
date = "2026-04-09"
|
||||
status = "approved"
|
||||
+++
|
||||
<b>Bold</b> and <em>italic</em>.
|
||||
|
||||
<em>-- llywelwyn</em>: Thanks!"#;
|
||||
let entry = Entry::parse("2026-04-09-11223344", contents).unwrap();
|
||||
assert!(entry.body.contains("<b>Bold</b>"));
|
||||
assert!(entry.body.contains("<em>-- llywelwyn</em>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip() {
|
||||
let contents = r#"+++
|
||||
name = "alice"
|
||||
date = "2026-04-09"
|
||||
website = "https://example.com"
|
||||
status = "approved"
|
||||
+++
|
||||
Hello world!"#;
|
||||
let entry = Entry::parse("test", contents).unwrap();
|
||||
let serialized = entry.to_file_contents();
|
||||
let reparsed = Entry::parse("test", &serialized).unwrap();
|
||||
assert_eq!(entry.meta.name, reparsed.meta.name);
|
||||
assert_eq!(entry.body, reparsed.body);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_missing_frontmatter() {
|
||||
let result = Entry::parse("x", "no frontmatter here");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue