Skip to main content

rh_foundation/
io.rs

1//! File I/O utilities.
2//!
3//! This module provides common file operations used across the workspace.
4
5use crate::config::Config;
6use crate::error::{io_error_with_path, Result};
7use std::path::Path;
8
9/// Load configuration from a JSON file.
10///
11/// # Example
12/// ```no_run
13/// use rh_foundation::{Config, io};
14/// use serde::{Deserialize, Serialize};
15///
16/// #[derive(Debug, Serialize, Deserialize)]
17/// struct MyConfig {
18///     name: String,
19/// }
20///
21/// impl Config for MyConfig {}
22///
23/// let config: MyConfig = io::load_config_from_file("config.json").unwrap();
24/// ```
25pub fn load_config_from_file<T: Config>(path: impl AsRef<Path>) -> Result<T> {
26    let path = path.as_ref();
27    let content = std::fs::read_to_string(path)
28        .map_err(|e| io_error_with_path(e, path, "read config from"))?;
29    let config: T = serde_json::from_str(&content)?;
30    config.validate()?;
31    Ok(config)
32}
33
34/// Save configuration to a JSON file.
35///
36/// # Example
37/// ```no_run
38/// use rh_foundation::{Config, io};
39/// use serde::{Deserialize, Serialize};
40///
41/// #[derive(Debug, Serialize, Deserialize)]
42/// struct MyConfig {
43///     name: String,
44/// }
45///
46/// impl Config for MyConfig {}
47///
48/// let config = MyConfig { name: "test".to_string() };
49/// io::save_config_to_file(&config, "config.json").unwrap();
50/// ```
51pub fn save_config_to_file<T: Config>(config: &T, path: impl AsRef<Path>) -> Result<()> {
52    config.validate()?;
53    let content = serde_json::to_string_pretty(config)?;
54    let path = path.as_ref();
55    std::fs::write(path, content).map_err(|e| io_error_with_path(e, path, "write config to"))?;
56    Ok(())
57}
58
59/// Read a JSON file and deserialize it.
60pub fn read_json<T>(path: impl AsRef<Path>) -> Result<T>
61where
62    T: serde::de::DeserializeOwned,
63{
64    let path = path.as_ref();
65    let content =
66        std::fs::read_to_string(path).map_err(|e| io_error_with_path(e, path, "read JSON from"))?;
67    serde_json::from_str(&content).map_err(Into::into)
68}
69
70/// Write a value as JSON to a file.
71pub fn write_json<T>(path: impl AsRef<Path>, value: &T, pretty: bool) -> Result<()>
72where
73    T: serde::Serialize,
74{
75    let path = path.as_ref();
76    let content = if pretty {
77        serde_json::to_string_pretty(value)?
78    } else {
79        serde_json::to_string(value)?
80    };
81    std::fs::write(path, content).map_err(|e| io_error_with_path(e, path, "write JSON to"))?;
82    Ok(())
83}
84
85/// Ensure a directory exists, creating it if necessary.
86pub fn ensure_dir(path: impl AsRef<Path>) -> Result<()> {
87    let path = path.as_ref();
88    if !path.exists() {
89        std::fs::create_dir_all(path)?;
90    }
91    Ok(())
92}
93
94/// Get the canonical (absolute) path.
95pub fn canonical_path(path: impl AsRef<Path>) -> Result<std::path::PathBuf> {
96    path.as_ref().canonicalize().map_err(Into::into)
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use serde::{Deserialize, Serialize};
103
104    #[derive(Debug, Serialize, Deserialize, PartialEq)]
105    struct TestConfig {
106        name: String,
107        value: u32,
108    }
109
110    impl Config for TestConfig {}
111
112    #[test]
113    fn test_config_roundtrip() {
114        let config = TestConfig {
115            name: "test".to_string(),
116            value: 42,
117        };
118
119        let json = serde_json::to_string(&config).unwrap();
120        let deserialized: TestConfig = serde_json::from_str(&json).unwrap();
121
122        assert_eq!(config, deserialized);
123    }
124}