dove_core/
request_ledger.rs1use anyhow::{Context, Result};
12use serde::{Deserialize, Serialize};
13use std::path::{Path, PathBuf};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct RequestRecord {
17 pub id: String,
18 pub fragment: String,
21 pub description: String,
22 pub created_at: u64,
23}
24
25pub fn record(rec: RequestRecord) -> Result<()> {
27 record_at(&requests_path()?, rec)
28}
29
30pub fn all() -> Vec<RequestRecord> {
32 requests_path()
33 .ok()
34 .map(|p| all_from(&p))
35 .unwrap_or_default()
36}
37
38pub fn get(id: &str) -> Option<RequestRecord> {
40 requests_path().ok().and_then(|p| get_from(&p, id))
41}
42
43pub fn remove(id: &str) -> Result<()> {
45 remove_at(&requests_path()?, id)
46}
47
48fn record_at(path: &Path, rec: RequestRecord) -> Result<()> {
52 let mut all = load_from(path).unwrap_or_default();
53 all.retain(|r| r.id != rec.id);
54 all.push(rec);
55 save_to(path, &all)
56}
57
58fn all_from(path: &Path) -> Vec<RequestRecord> {
60 load_from(path).unwrap_or_default()
61}
62
63fn get_from(path: &Path, id: &str) -> Option<RequestRecord> {
66 all_from(path).into_iter().find(|r| r.id == id)
67}
68
69fn remove_at(path: &Path, id: &str) -> Result<()> {
72 let mut all = load_from(path).unwrap_or_default();
73 all.retain(|r| r.id != id);
74 save_to(path, &all)
75}
76
77fn load_from(path: &Path) -> Result<Vec<RequestRecord>> {
78 match std::fs::read_to_string(path) {
79 Ok(text) => serde_json::from_str(&text).context("parsing the dove requests ledger"),
80 Err(_) => Ok(Vec::new()), }
82}
83
84fn save_to(path: &Path, all: &[RequestRecord]) -> Result<()> {
85 std::fs::create_dir_all(path.parent().unwrap())?;
86 let text = serde_json::to_string_pretty(all).context("serializing the dove requests ledger")?;
87 std::fs::write(path, text).with_context(|| format!("writing {}", path.display()))?;
88 set_private(path)
89}
90
91#[cfg(unix)]
94fn set_private(path: &Path) -> Result<()> {
95 use std::os::unix::fs::PermissionsExt;
96 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
97 .with_context(|| format!("chmod 600 {}", path.display()))
98}
99#[cfg(not(unix))]
100fn set_private(_path: &Path) -> Result<()> {
101 Ok(())
102}
103
104fn requests_path() -> Result<PathBuf> {
105 if let Ok(x) = std::env::var("XDG_CONFIG_HOME") {
106 if !x.is_empty() {
107 return Ok(PathBuf::from(x).join("dove/requests.json"));
108 }
109 }
110 let home = std::env::var("HOME").map_err(|_| anyhow::anyhow!("HOME is not set"))?;
111 Ok(PathBuf::from(home).join(".config/dove/requests.json"))
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
125 fn round_trips_records_through_the_public_api_paths() {
126 let dir = std::env::temp_dir().join(format!("dove-req-test-{}", std::process::id()));
127 let _ = std::fs::remove_dir_all(&dir);
128 let path = dir.join("requests.json");
129
130 let a = RequestRecord {
131 id: "aaa".into(),
132 fragment: "deadbeef".into(),
133 description: "invoice".into(),
134 created_at: 1_000,
135 };
136 let b = RequestRecord {
137 id: "bbb".into(),
138 fragment: "cafebabe".into(),
139 description: "vacation photo".into(),
140 created_at: 2_000,
141 };
142
143 assert!(all_from(&path).is_empty(), "fresh ledger starts empty");
144
145 record_at(&path, a.clone()).unwrap();
147 record_at(&path, b.clone()).unwrap();
148
149 let everything = all_from(&path);
151 assert_eq!(everything.len(), 2);
152
153 let got_a = get_from(&path, &a.id).unwrap();
155 assert_eq!(got_a.fragment, "deadbeef");
156 assert_eq!(got_a.description, "invoice");
157 assert_eq!(got_a.created_at, 1_000);
158 let got_b = get_from(&path, &b.id).unwrap();
159 assert_eq!(got_b.fragment, "cafebabe");
160 assert!(get_from(&path, "no-such-id").is_none());
161
162 let a_revised = RequestRecord {
164 description: "invoice (revised)".into(),
165 ..a.clone()
166 };
167 record_at(&path, a_revised).unwrap();
168 assert_eq!(all_from(&path).len(), 2, "same id replaces, not appends");
169 assert_eq!(
170 get_from(&path, &a.id).unwrap().description,
171 "invoice (revised)"
172 );
173
174 remove_at(&path, &a.id).unwrap();
176 let remaining = all_from(&path);
177 assert_eq!(remaining.len(), 1);
178 assert_eq!(remaining[0].id, b.id);
179
180 #[cfg(unix)]
182 {
183 use std::os::unix::fs::PermissionsExt;
184 let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
185 assert_eq!(mode, 0o600);
186 }
187
188 let _ = std::fs::remove_dir_all(&dir);
189 }
190}