use chrono::{DateTime, Utc};
use serde::Deserialize;
#[derive(Debug, Clone)]
pub struct DrawerSummary {
pub title: String,
pub tags: Vec<String>,
pub created_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Deserialize)]
struct RawDrawer {
#[serde(default)]
title: String,
#[serde(default)]
tags: Vec<String>,
#[serde(default)]
created_at: Option<String>,
}
impl From<RawDrawer> for DrawerSummary {
fn from(r: RawDrawer) -> Self {
let created_at = r
.created_at
.as_deref()
.and_then(|s| s.parse::<DateTime<Utc>>().ok());
DrawerSummary {
title: r.title,
tags: r.tags,
created_at,
}
}
}
pub async fn fetch_recent_palace_drawers(
memory_url: &str,
palace_id: &str,
limit: usize,
since: Option<DateTime<Utc>>,
) -> Vec<DrawerSummary> {
let url = format!(
"{}/api/v1/palaces/{}/drawers?sort=created_desc&limit={}",
memory_url.trim_end_matches('/'),
palace_id,
limit
);
let client = match reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build()
{
Ok(c) => c,
Err(e) => {
eprintln!("catchup: could not build HTTP client: {e}");
return vec![];
}
};
let resp = match client.get(&url).send().await {
Ok(r) => r,
Err(e) => {
eprintln!("catchup: could not reach trusty-memory at {memory_url}: {e}");
return vec![];
}
};
if !resp.status().is_success() {
eprintln!(
"catchup: trusty-memory returned {} for palace drawer query",
resp.status()
);
return vec![];
}
let raw: Vec<RawDrawer> = match resp.json().await {
Ok(v) => v,
Err(e) => {
eprintln!("catchup: could not parse palace drawer response: {e}");
return vec![];
}
};
let mut drawers: Vec<DrawerSummary> = raw.into_iter().map(DrawerSummary::from).collect();
if let Some(since_ts) = since {
drawers.retain(|d| d.created_at.is_some_and(|ts| ts > since_ts));
}
drawers
}
#[cfg(test)]
mod tests {
use super::*;
fn make_raw(title: &str, tags: Vec<&str>, created_at: Option<&str>) -> RawDrawer {
RawDrawer {
title: title.to_string(),
tags: tags.into_iter().map(|s| s.to_string()).collect(),
created_at: created_at.map(|s| s.to_string()),
}
}
#[test]
fn drawer_response_parsing() {
let raw = make_raw(
"My Drawer",
vec!["rust", "mpm"],
Some("2026-06-27T10:00:00Z"),
);
let summary = DrawerSummary::from(raw);
assert_eq!(summary.title, "My Drawer");
assert_eq!(summary.tags, vec!["rust", "mpm"]);
assert!(summary.created_at.is_some());
}
#[test]
fn drawer_since_filter() {
let raw_drawers = vec![
make_raw("Old", vec![], Some("2026-06-25T00:00:00Z")),
make_raw("New", vec![], Some("2026-06-27T00:00:00Z")),
];
let mut summaries: Vec<DrawerSummary> =
raw_drawers.into_iter().map(DrawerSummary::from).collect();
let since: DateTime<Utc> = "2026-06-26T00:00:00Z".parse().unwrap();
summaries.retain(|d| d.created_at.is_some_and(|ts| ts > since));
assert_eq!(summaries.len(), 1);
assert_eq!(summaries[0].title, "New");
}
#[test]
fn drawer_empty_array() {
let raw: Vec<RawDrawer> = vec![];
let summaries: Vec<DrawerSummary> = raw.into_iter().map(DrawerSummary::from).collect();
assert!(summaries.is_empty());
}
#[test]
fn drawer_missing_created_at_is_none() {
let raw = make_raw("No Date", vec![], None);
let s = DrawerSummary::from(raw);
assert!(s.created_at.is_none());
}
#[tokio::test]
#[ignore]
async fn live_drawer_fetch() {
let drawers =
fetch_recent_palace_drawers("http://127.0.0.1:7990", "test-palace", 5, None).await;
let _ = drawers;
}
}