Skip to main content

perfgate_types/
io.rs

1//! File I/O helpers for reading JSON files.
2
3use serde::de::DeserializeOwned;
4use std::path::Path;
5
6/// Error returned by [`read_json_file`].
7#[derive(Debug, thiserror::Error)]
8pub enum ReadJsonError {
9    /// Failed to read the file from disk.
10    #[error("failed to read {path}: {source}")]
11    Read {
12        path: String,
13        source: std::io::Error,
14    },
15
16    /// Failed to parse the file contents as JSON.
17    #[error("failed to parse JSON from {path}: {source}")]
18    Parse {
19        path: String,
20        source: serde_json::Error,
21    },
22}
23
24/// Reads a JSON file from disk and deserializes it into `T`.
25///
26/// This replaces the common boilerplate of `fs::read_to_string` followed
27/// by `serde_json::from_str`, with error messages that include the file path.
28///
29/// # Errors
30///
31/// Returns [`ReadJsonError::Read`] if the file cannot be read, or
32/// [`ReadJsonError::Parse`] if the contents are not valid JSON for `T`.
33///
34/// # Example
35///
36/// ```no_run
37/// use perfgate_types::read_json_file;
38/// use perfgate_types::RunReceipt;
39/// use std::path::Path;
40///
41/// let receipt: RunReceipt = read_json_file(Path::new("run.json")).unwrap();
42/// ```
43pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T, ReadJsonError> {
44    let contents = std::fs::read_to_string(path).map_err(|source| ReadJsonError::Read {
45        path: path.display().to_string(),
46        source,
47    })?;
48    serde_json::from_str(&contents).map_err(|source| ReadJsonError::Parse {
49        path: path.display().to_string(),
50        source,
51    })
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn read_json_file_nonexistent_returns_read_error() {
60        let result = read_json_file::<serde_json::Value>(Path::new("does_not_exist.json"));
61        let err = result.unwrap_err();
62        assert!(matches!(err, ReadJsonError::Read { .. }));
63        assert!(err.to_string().contains("does_not_exist.json"));
64    }
65
66    #[test]
67    fn read_json_file_invalid_json_returns_parse_error() {
68        let dir = tempfile::tempdir().unwrap();
69        let path = dir.path().join("bad.json");
70        std::fs::write(&path, "not valid json {{{").unwrap();
71
72        let result = read_json_file::<serde_json::Value>(&path);
73        let err = result.unwrap_err();
74        assert!(matches!(err, ReadJsonError::Parse { .. }));
75        assert!(err.to_string().contains("bad.json"));
76    }
77
78    #[test]
79    fn read_json_file_valid_json_roundtrip() {
80        let dir = tempfile::tempdir().unwrap();
81        let path = dir.path().join("good.json");
82        std::fs::write(&path, r#"{"key": "value"}"#).unwrap();
83
84        let result: serde_json::Value = read_json_file(&path).unwrap();
85        assert_eq!(result["key"], "value");
86    }
87
88    #[test]
89    fn read_json_error_display_includes_path() {
90        let err = ReadJsonError::Read {
91            path: "/tmp/test.json".to_string(),
92            source: std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"),
93        };
94        let msg = err.to_string();
95        assert!(msg.contains("/tmp/test.json"));
96        assert!(msg.contains("failed to read"));
97    }
98}