use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
const PAIRING_FILE: &str = "pairing.json";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PairingRecord {
pub chat_id: i64,
pub paired_at: String,
}
impl PairingRecord {
pub fn new(chat_id: i64) -> Self {
Self {
chat_id,
paired_at: chrono::Utc::now().to_rfc3339(),
}
}
}
pub fn pairing_path(root: &Path) -> PathBuf {
root.join(PAIRING_FILE)
}
pub fn load(root: &Path) -> Option<PairingRecord> {
let path = pairing_path(root);
let contents = std::fs::read_to_string(&path).ok()?;
match serde_json::from_str::<PairingRecord>(&contents) {
Ok(record) => Some(record),
Err(e) => {
tracing::warn!("ignoring malformed {}: {e}", path.display());
None
}
}
}
pub fn save(root: &Path, record: &PairingRecord) -> std::io::Result<()> {
std::fs::create_dir_all(root)?;
let path = pairing_path(root);
let tmp = path.with_extension("json.tmp");
let json = serde_json::to_string_pretty(record)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
std::fs::write(&tmp, json)?;
std::fs::rename(&tmp, &path)?;
Ok(())
}
pub fn clear(root: &Path) -> std::io::Result<()> {
let path = pairing_path(root);
match std::fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_stamps_current_time() {
let record = PairingRecord::new(42);
assert_eq!(record.chat_id, 42);
assert!(record.paired_at.contains('T'));
}
#[test]
fn load_missing_is_none() {
let dir = tempfile::tempdir().expect("temp dir");
assert!(load(dir.path()).is_none());
}
#[test]
fn save_then_load_round_trips() {
let dir = tempfile::tempdir().expect("temp dir");
let record = PairingRecord::new(123_456_789);
save(dir.path(), &record).expect("save succeeds");
let loaded = load(dir.path()).expect("record loads");
assert_eq!(loaded, record);
}
#[test]
fn save_creates_missing_root() {
let dir = tempfile::tempdir().expect("temp dir");
let nested = dir.path().join("does/not/exist");
let record = PairingRecord::new(7);
save(&nested, &record).expect("save creates the directory");
assert_eq!(load(&nested), Some(record));
}
#[test]
fn clear_removes_file() {
let dir = tempfile::tempdir().expect("temp dir");
save(dir.path(), &PairingRecord::new(1)).expect("save");
assert!(load(dir.path()).is_some());
clear(dir.path()).expect("clear succeeds");
assert!(load(dir.path()).is_none());
clear(dir.path()).expect("idempotent clear");
}
#[test]
fn load_malformed_is_none() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::write(pairing_path(dir.path()), "{ not json").expect("write garbage");
assert!(load(dir.path()).is_none());
}
}