use std::{error::Error, path::Path};
use thiserror::Error;
use crate::Forge;
pub type BoxedError = Box<dyn Error + Send + Sync>;
pub struct Transform {
transformer: Box<dyn Fn(String) -> Result<String, BoxedError>>,
}
impl Transform {
pub fn new<F>(transformer: F) -> Self
where
F: Fn(String) -> Result<String, BoxedError> + 'static,
{
Self {
transformer: Box::new(transformer),
}
}
pub fn apply(&self, input: &str) -> Result<String, BoxedError> {
(self.transformer)(input.to_string())
}
}
#[derive(Error, Debug)]
pub enum TransformError {
#[error("failed to perform file I/O while transforming file: {0}")]
StdIo(#[from] std::io::Error),
#[error("failed to apply transformation to file content: {0}")]
Transform(#[from] BoxedError),
}
impl Forge for Transform {
type Error = TransformError;
fn forge(&self, into: impl AsRef<Path>) -> Result<(), Self::Error> {
let path = into.as_ref();
let content = std::fs::read_to_string(path).map_err(TransformError::StdIo)?;
let transformed = self.apply(&content).map_err(TransformError::Transform)?;
std::fs::write(path, transformed).map_err(TransformError::StdIo)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{io::Write, sync::Arc};
use tempfile::{tempdir, NamedTempFile};
#[test]
fn test_transform_applies_function() {
let transformer =
|input: String| -> Result<String, BoxedError> { Ok(input.to_uppercase()) };
let transform = Transform::new(transformer);
let input = "hello world";
let result = transform.apply(input).unwrap();
assert_eq!(result, "HELLO WORLD");
}
#[test]
fn test_transform_file_content() {
let mut temp_file = NamedTempFile::new().unwrap();
let original_content = "hello world";
temp_file.write_all(original_content.as_bytes()).unwrap();
let transformer =
|input: String| -> Result<String, BoxedError> { Ok(input.to_uppercase()) };
let transform = Transform::new(transformer);
let result = transform.forge(temp_file.path());
assert!(result.is_ok());
let content = std::fs::read_to_string(temp_file.path()).unwrap();
assert_eq!(content, "HELLO WORLD");
}
#[test]
fn test_transform_handles_error() {
let transformer =
|_: String| -> Result<String, BoxedError> { Err("transform failed".into()) };
let transform = Transform::new(transformer);
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(b"content").unwrap();
let result = transform.forge(temp_file.path());
assert!(result.is_err());
match result {
Err(TransformError::Transform(err)) => assert_eq!(err.to_string(), "transform failed"),
other => unreachable!("Expected Transform error but got: {:?}", other),
}
}
#[test]
fn test_transform_handles_file_not_found() {
let nonexistent_path = tempdir().unwrap().path().join("nonexistent_file.txt");
let transform = Transform::new(|s| Ok(s));
let result = transform.forge(nonexistent_path);
assert!(result.is_err());
match result {
Err(TransformError::StdIo(err)) => assert_eq!(err.kind(), std::io::ErrorKind::NotFound),
other => unreachable!("Expected StdIo error but got: {:?}", other),
}
}
#[test]
fn test_transform_can_use_captured_data() {
let counter = Arc::new(std::sync::Mutex::new(0));
let counter_clone = counter.clone();
let transformer = move |input: String| -> Result<String, BoxedError> {
let mut count = counter_clone.lock().unwrap();
*count += 1;
Ok(format!("{} (transformed {} times)", input, *count))
};
let transform = Transform::new(transformer);
let input = "hello";
let result1 = transform.apply(input).unwrap();
let result2 = transform.apply(input).unwrap();
assert_eq!(result1, "hello (transformed 1 times)");
assert_eq!(result2, "hello (transformed 2 times)");
assert_eq!(*counter.lock().unwrap(), 2);
}
}