sync from monorepo @ 2452e92e

This commit is contained in:
2026-05-08 01:59:04 +02:00
commit b03dc15371
459 changed files with 129586 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
//! JSON read/write helpers with atomic writes.
//!
//! Follows the archivist pattern: write to .tmp, then rename.
use serde::{Deserialize, Serialize};
use std::path::Path;
use tokio::io::AsyncWriteExt;
/// Write a value to a JSON file atomically.
///
/// 1. Serializes to pretty-printed JSON
/// 2. Writes to `{path}.tmp`
/// 3. Renames temp file to target (atomic on most filesystems)
pub async fn write_json<T: Serialize>(path: &Path, value: &T) -> std::io::Result<()> {
let json = serde_json::to_string_pretty(value)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let temp_path = path.with_extension("tmp");
let mut file = tokio::fs::File::create(&temp_path).await?;
file.write_all(json.as_bytes()).await?;
file.sync_all().await?;
drop(file);
tokio::fs::rename(&temp_path, path).await?;
Ok(())
}
/// Read a value from a JSON file.
///
/// Returns `NotFound` if the file doesn't exist.
pub async fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> std::io::Result<T> {
let content = tokio::fs::read_to_string(path).await?;
let value: T = serde_json::from_str(&content)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(value)
}
/// Read a value from a JSON file, returning a default if the file doesn't exist.
pub async fn read_json_or_default<T: for<'de> Deserialize<'de> + Default>(
path: &Path,
) -> std::io::Result<T> {
match read_json(path).await {
Ok(value) => Ok(value),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(T::default()),
Err(e) => Err(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct TestData {
id: String,
value: i32,
}
#[tokio::test]
async fn test_write_and_read_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.json");
let data = TestData {
id: "test".to_string(),
value: 42,
};
write_json(&path, &data).await.unwrap();
let read: TestData = read_json(&path).await.unwrap();
assert_eq!(read, data);
}
#[tokio::test]
async fn test_read_missing_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("missing.json");
let result: std::io::Result<TestData> = read_json(&path).await;
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
}
#[tokio::test]
async fn test_read_json_or_default() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("missing.json");
let result: Vec<String> = read_json_or_default(&path).await.unwrap();
assert!(result.is_empty());
}
#[tokio::test]
async fn test_atomic_overwrite() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.json");
let data1 = TestData {
id: "first".to_string(),
value: 1,
};
let data2 = TestData {
id: "second".to_string(),
value: 2,
};
write_json(&path, &data1).await.unwrap();
write_json(&path, &data2).await.unwrap();
let read: TestData = read_json(&path).await.unwrap();
assert_eq!(read, data2);
// Temp file should not remain
assert!(!path.with_extension("tmp").exists());
}
}