problemreductions/
io.rs

1//! File I/O utilities for problem serialization.
2//!
3//! This module provides functions for reading and writing problems
4//! to various file formats using serde.
5
6use crate::error::{ProblemError, Result};
7use serde::{de::DeserializeOwned, Serialize};
8use std::fs::File;
9use std::io::{BufReader, BufWriter, Read, Write};
10use std::path::Path;
11
12/// Supported file formats.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum FileFormat {
15    /// JSON format (human-readable).
16    Json,
17    /// Compact JSON format (no pretty-printing).
18    JsonCompact,
19}
20
21impl FileFormat {
22    /// Detect file format from file extension.
23    pub fn from_extension(path: &Path) -> Option<Self> {
24        let ext = path.extension()?.to_str()?.to_lowercase();
25        match ext.as_str() {
26            "json" => Some(FileFormat::Json),
27            _ => None,
28        }
29    }
30}
31
32/// Write a problem to a file.
33///
34/// # Arguments
35///
36/// * `problem` - The problem to write
37/// * `path` - The file path to write to
38/// * `format` - The file format to use
39///
40/// # Example
41///
42/// ```no_run
43/// use problemreductions::io::{write_problem, FileFormat};
44/// use problemreductions::models::graph::IndependentSet;
45///
46/// let problem = IndependentSet::<i32>::new(3, vec![(0, 1), (1, 2)]);
47/// write_problem(&problem, "problem.json", FileFormat::Json).unwrap();
48/// ```
49pub fn write_problem<T: Serialize, P: AsRef<Path>>(
50    problem: &T,
51    path: P,
52    format: FileFormat,
53) -> Result<()> {
54    let file = File::create(path.as_ref())
55        .map_err(|e| ProblemError::IoError(format!("Failed to create file: {}", e)))?;
56    let writer = BufWriter::new(file);
57
58    match format {
59        FileFormat::Json => serde_json::to_writer_pretty(writer, problem)
60            .map_err(|e| ProblemError::SerializationError(format!("Failed to write JSON: {}", e))),
61        FileFormat::JsonCompact => serde_json::to_writer(writer, problem)
62            .map_err(|e| ProblemError::SerializationError(format!("Failed to write JSON: {}", e))),
63    }
64}
65
66/// Read a problem from a file.
67///
68/// # Arguments
69///
70/// * `path` - The file path to read from
71/// * `format` - The file format to use
72///
73/// # Example
74///
75/// ```no_run
76/// use problemreductions::io::{read_problem, FileFormat};
77/// use problemreductions::models::graph::IndependentSet;
78///
79/// let problem: IndependentSet<i32> = read_problem("problem.json", FileFormat::Json).unwrap();
80/// ```
81pub fn read_problem<T: DeserializeOwned, P: AsRef<Path>>(
82    path: P,
83    format: FileFormat,
84) -> Result<T> {
85    let file = File::open(path.as_ref())
86        .map_err(|e| ProblemError::IoError(format!("Failed to open file: {}", e)))?;
87    let reader = BufReader::new(file);
88
89    match format {
90        FileFormat::Json | FileFormat::JsonCompact => serde_json::from_reader(reader)
91            .map_err(|e| ProblemError::SerializationError(format!("Failed to parse JSON: {}", e))),
92    }
93}
94
95/// Serialize a problem to a JSON string.
96pub fn to_json<T: Serialize>(problem: &T) -> Result<String> {
97    serde_json::to_string_pretty(problem)
98        .map_err(|e| ProblemError::SerializationError(format!("Failed to serialize: {}", e)))
99}
100
101/// Serialize a problem to a compact JSON string.
102pub fn to_json_compact<T: Serialize>(problem: &T) -> Result<String> {
103    serde_json::to_string(problem)
104        .map_err(|e| ProblemError::SerializationError(format!("Failed to serialize: {}", e)))
105}
106
107/// Deserialize a problem from a JSON string.
108pub fn from_json<T: DeserializeOwned>(json: &str) -> Result<T> {
109    serde_json::from_str(json)
110        .map_err(|e| ProblemError::SerializationError(format!("Failed to parse JSON: {}", e)))
111}
112
113/// Read a file to a string.
114pub fn read_file<P: AsRef<Path>>(path: P) -> Result<String> {
115    let mut file = File::open(path.as_ref())
116        .map_err(|e| ProblemError::IoError(format!("Failed to open file: {}", e)))?;
117    let mut contents = String::new();
118    file.read_to_string(&mut contents)
119        .map_err(|e| ProblemError::IoError(format!("Failed to read file: {}", e)))?;
120    Ok(contents)
121}
122
123/// Write a string to a file.
124pub fn write_file<P: AsRef<Path>>(path: P, contents: &str) -> Result<()> {
125    let mut file = File::create(path.as_ref())
126        .map_err(|e| ProblemError::IoError(format!("Failed to create file: {}", e)))?;
127    file.write_all(contents.as_bytes())
128        .map_err(|e| ProblemError::IoError(format!("Failed to write file: {}", e)))?;
129    Ok(())
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::models::graph::IndependentSet;
136    use std::fs;
137
138    #[test]
139    fn test_to_json() {
140        let problem = IndependentSet::<i32>::new(3, vec![(0, 1), (1, 2)]);
141        let json = to_json(&problem);
142        assert!(json.is_ok());
143        let json = json.unwrap();
144        assert!(json.contains("graph"));
145    }
146
147    #[test]
148    fn test_from_json() {
149        let problem = IndependentSet::<i32>::new(3, vec![(0, 1), (1, 2)]);
150        let json = to_json(&problem).unwrap();
151        let restored: IndependentSet<i32> = from_json(&json).unwrap();
152        assert_eq!(restored.num_vertices(), 3);
153        assert_eq!(restored.num_edges(), 2);
154    }
155
156    #[test]
157    fn test_json_compact() {
158        let problem = IndependentSet::<i32>::new(3, vec![(0, 1)]);
159        let compact = to_json_compact(&problem).unwrap();
160        let pretty = to_json(&problem).unwrap();
161        // Compact should be shorter
162        assert!(compact.len() < pretty.len());
163    }
164
165    #[test]
166    fn test_file_roundtrip() {
167        let problem = IndependentSet::<i32>::new(4, vec![(0, 1), (1, 2), (2, 3)]);
168        let path = "/tmp/test_problem.json";
169
170        // Write
171        write_problem(&problem, path, FileFormat::Json).unwrap();
172
173        // Read back
174        let restored: IndependentSet<i32> = read_problem(path, FileFormat::Json).unwrap();
175        assert_eq!(restored.num_vertices(), 4);
176        assert_eq!(restored.num_edges(), 3);
177
178        // Cleanup
179        fs::remove_file(path).ok();
180    }
181
182    #[test]
183    fn test_file_format_from_extension() {
184        assert_eq!(
185            FileFormat::from_extension(Path::new("test.json")),
186            Some(FileFormat::Json)
187        );
188        assert_eq!(
189            FileFormat::from_extension(Path::new("test.JSON")),
190            Some(FileFormat::Json)
191        );
192        assert_eq!(FileFormat::from_extension(Path::new("test.txt")), None);
193        assert_eq!(FileFormat::from_extension(Path::new("noext")), None);
194    }
195
196    #[test]
197    fn test_read_write_file() {
198        let path = "/tmp/test_io.txt";
199        let contents = "Hello, World!";
200
201        write_file(path, contents).unwrap();
202        let read_back = read_file(path).unwrap();
203
204        assert_eq!(read_back, contents);
205
206        fs::remove_file(path).ok();
207    }
208
209    #[test]
210    fn test_invalid_json() {
211        let result: Result<IndependentSet<i32>> = from_json("not valid json");
212        assert!(result.is_err());
213    }
214}