qualia_client_core/wellfair/
personal_profile.rs1use std::fs::{self, OpenOptions};
4use std::io::{BufRead, BufReader, Write};
5use std::path::{Path, PathBuf};
6
7use serde::{Deserialize, Serialize};
8
9pub const CONTACTS_FILE: &str = "wellfair/emergency_contacts.jsonl";
10pub const MAX_CONTACTS: usize = 32;
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13pub struct EmergencyContact {
14 pub id: String,
15 pub display_name: String,
16 pub relationship: String,
17 pub phone: Option<String>,
18 pub email: Option<String>,
19 pub notes: Option<String>,
20 pub created_at_unix: u32,
21}
22
23pub struct EmergencyContactStore {
24 path: PathBuf,
25}
26
27impl EmergencyContactStore {
28 pub fn open(storage_root: impl AsRef<Path>) -> std::io::Result<Self> {
29 let path = storage_root.as_ref().join(CONTACTS_FILE);
30 if let Some(parent) = path.parent() {
31 fs::create_dir_all(parent)?;
32 }
33 if !path.exists() {
34 OpenOptions::new().create(true).write(true).open(&path)?;
35 }
36 Ok(Self { path })
37 }
38
39 pub fn append(&self, contact: &EmergencyContact) -> std::io::Result<()> {
40 let line =
41 serde_json::to_string(contact).map_err(|e| std::io::Error::other(e.to_string()))?;
42 let mut file = OpenOptions::new().append(true).open(&self.path)?;
43 writeln!(file, "{line}")?;
44 file.sync_all()?;
45 Ok(())
46 }
47
48 pub fn list(&self) -> std::io::Result<Vec<EmergencyContact>> {
49 let file = fs::File::open(&self.path)?;
50 let reader = BufReader::new(file);
51 let mut out = Vec::new();
52 for line in reader.lines() {
53 let line = line?;
54 if line.trim().is_empty() {
55 continue;
56 }
57 if let Ok(c) = serde_json::from_str::<EmergencyContact>(&line) {
58 out.push(c);
59 }
60 }
61 if out.len() > MAX_CONTACTS {
62 out.drain(0..out.len() - MAX_CONTACTS);
63 }
64 Ok(out)
65 }
66}
67
68pub fn new_contact_id(name: &str, unix: u32) -> String {
69 use sha2::{Digest, Sha256};
70 let digest = Sha256::digest(format!("{name}:{unix}").as_bytes());
71 format!("ec-{}", hex::encode(&digest[..4]))
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn emergency_contact_round_trip() {
80 let dir = tempfile::tempdir().unwrap();
81 let store = EmergencyContactStore::open(dir.path()).unwrap();
82 let c = EmergencyContact {
83 id: new_contact_id("Alex", 100),
84 display_name: "Alex".into(),
85 relationship: "sibling".into(),
86 phone: Some("+1-555-0100".into()),
87 email: None,
88 notes: None,
89 created_at_unix: 100,
90 };
91 store.append(&c).unwrap();
92 let listed = store.list().unwrap();
93 assert_eq!(listed.len(), 1);
94 assert_eq!(listed[0].display_name, "Alex");
95 }
96}