1use serde::de::DeserializeOwned;
4use std::path::Path;
5
6#[derive(Debug, thiserror::Error)]
8pub enum ReadJsonError {
9 #[error("failed to read {path}: {source}")]
11 Read {
12 path: String,
13 source: std::io::Error,
14 },
15
16 #[error("failed to parse JSON from {path}: {source}")]
18 Parse {
19 path: String,
20 source: serde_json::Error,
21 },
22}
23
24pub 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}