cairn_mod/cli/
retention.rs1use std::path::Path;
14use std::time::Duration;
15
16use reqwest::Client;
17use serde::{Deserialize, Serialize};
18
19use super::auth::acquire_service_auth;
20use super::error::CliError;
21use super::pds::PdsClient;
22use super::session::SessionFile;
23
24const RETENTION_SWEEP_LXM: &str = "tools.cairn.admin.retentionSweep";
25
26#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
29pub struct SweepResponse {
30 #[serde(rename = "rowsDeleted")]
32 pub rows_deleted: i64,
33 pub batches: u64,
35 #[serde(rename = "durationMs")]
37 pub duration_ms: u64,
38 #[serde(
42 rename = "retentionDaysApplied",
43 skip_serializing_if = "Option::is_none",
44 default
45 )]
46 pub retention_days_applied: Option<u32>,
47}
48
49#[derive(Debug, Clone, Default)]
54pub struct SweepInput {
55 pub cairn_server_override: Option<String>,
57}
58
59pub async fn sweep(
68 session: &mut SessionFile,
69 session_path: &Path,
70 input: SweepInput,
71) -> Result<SweepResponse, CliError> {
72 let cairn_server = input
73 .cairn_server_override
74 .as_deref()
75 .unwrap_or(&session.cairn_server_url)
76 .trim_end_matches('/')
77 .to_string();
78 let pds = PdsClient::new(&session.pds_url)?;
79 let token = acquire_service_auth(&pds, session, session_path, RETENTION_SWEEP_LXM).await?;
80
81 let url = format!("{cairn_server}/xrpc/{RETENTION_SWEEP_LXM}");
82 let client = Client::builder()
83 .timeout(Duration::from_secs(300))
84 .build()
85 .expect("reqwest build");
86 let resp = client
87 .post(&url)
88 .bearer_auth(&token)
89 .header("content-type", "application/json")
90 .body("{}")
91 .send()
92 .await
93 .map_err(|source| CliError::Http {
94 url: url.clone(),
95 source,
96 })?;
97 if !resp.status().is_success() {
98 let status = resp.status().as_u16();
99 let body = resp.text().await.unwrap_or_default();
100 return Err(CliError::CairnStatus { url, status, body });
101 }
102 let bytes = resp.bytes().await.map_err(|source| CliError::Http {
103 url: url.clone(),
104 source,
105 })?;
106 serde_json::from_slice::<SweepResponse>(&bytes)
107 .map_err(|source| CliError::MalformedResponse { url, source })
108}
109
110pub fn format_sweep_human(resp: &SweepResponse) -> String {
116 use std::fmt::Write;
117 let mut s = String::new();
118 let _ = write!(
119 s,
120 "rows_deleted={} batches={} duration_ms={}",
121 resp.rows_deleted, resp.batches, resp.duration_ms,
122 );
123 match resp.retention_days_applied {
124 Some(days) => {
125 let _ = write!(s, " retention_days={days}");
126 }
127 None => {
128 let _ = write!(s, " (no retention cutoff configured; sweep was a no-op)");
129 }
130 }
131 s
132}
133
134pub fn format_sweep_json(resp: &SweepResponse) -> String {
136 serde_json::to_string_pretty(resp).expect("SweepResponse serializes")
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn format_human_shows_retention_days_when_present() {
145 let resp = SweepResponse {
146 rows_deleted: 42,
147 batches: 3,
148 duration_ms: 87,
149 retention_days_applied: Some(180),
150 };
151 let s = format_sweep_human(&resp);
152 assert!(s.contains("rows_deleted=42"));
153 assert!(s.contains("batches=3"));
154 assert!(s.contains("retention_days=180"));
155 }
156
157 #[test]
158 fn format_human_notes_no_cutoff_when_field_absent() {
159 let resp = SweepResponse {
160 rows_deleted: 0,
161 batches: 1,
162 duration_ms: 0,
163 retention_days_applied: None,
164 };
165 let s = format_sweep_human(&resp);
166 assert!(s.contains("rows_deleted=0"));
167 assert!(s.contains("no retention cutoff configured"));
168 }
169
170 #[test]
171 fn format_json_omits_retention_days_when_none() {
172 let resp = SweepResponse {
173 rows_deleted: 0,
174 batches: 1,
175 duration_ms: 0,
176 retention_days_applied: None,
177 };
178 let json = format_sweep_json(&resp);
179 assert!(
180 !json.contains("retentionDaysApplied"),
181 "skip_serializing_if drops the field"
182 );
183 }
184
185 #[test]
186 fn deserializes_server_camelcase_shape() {
187 let body = r#"{"rowsDeleted":7,"batches":2,"durationMs":15,"retentionDaysApplied":30}"#;
188 let r: SweepResponse = serde_json::from_str(body).unwrap();
189 assert_eq!(r.rows_deleted, 7);
190 assert_eq!(r.batches, 2);
191 assert_eq!(r.duration_ms, 15);
192 assert_eq!(r.retention_days_applied, Some(30));
193 }
194}