use crate::effects::validation::{FileError, ValidatedFileSet};
use crate::effects::AnalysisEffect;
use crate::env::{AnalysisEnv, RealEnv};
use crate::errors::AnalysisError;
use std::path::PathBuf;
use stillwater::effect::prelude::*;
pub fn read_file_effect(path: PathBuf) -> AnalysisEffect<String> {
let path_display = path.display().to_string();
from_fn(move |env: &RealEnv| {
env.file_system().read_to_string(&path).map_err(|e| {
AnalysisError::io_with_path(format!("Failed to read file: {}", e.message()), &path)
})
})
.map_err(move |e| {
AnalysisError::io_with_path(
format!("Reading file '{}': {}", path_display, e.message()),
e.path().cloned().unwrap_or_default(),
)
})
.boxed()
}
pub fn read_file_bytes_effect(path: PathBuf) -> AnalysisEffect<Vec<u8>> {
let path_display = path.display().to_string();
from_fn(move |env: &RealEnv| {
env.file_system().read_bytes(&path).map_err(|e| {
AnalysisError::io_with_path(
format!(
"Failed to read bytes from '{}': {}",
path_display,
e.message()
),
&path,
)
})
})
.boxed()
}
pub fn write_file_effect(path: PathBuf, content: String) -> AnalysisEffect<()> {
let path_display = path.display().to_string();
from_fn(move |env: &RealEnv| {
env.file_system().write(&path, &content).map_err(|e| {
AnalysisError::io_with_path(
format!("Failed to write file '{}': {}", path_display, e.message()),
&path,
)
})
})
.boxed()
}
pub fn file_exists_effect(path: PathBuf) -> AnalysisEffect<bool> {
from_fn(move |env: &RealEnv| Ok(env.file_system().is_file(&path))).boxed()
}
pub fn path_exists_effect(path: PathBuf) -> AnalysisEffect<bool> {
from_fn(move |env: &RealEnv| Ok(env.file_system().exists(&path))).boxed()
}
pub fn is_directory_effect(path: PathBuf) -> AnalysisEffect<bool> {
from_fn(move |env: &RealEnv| Ok(env.file_system().is_dir(&path))).boxed()
}
#[derive(Clone, Debug)]
pub struct FileContent {
pub path: PathBuf,
pub content: String,
}
impl FileContent {
pub fn new(path: PathBuf, content: String) -> Self {
Self { path, content }
}
}
pub fn read_files_with_accumulation(
paths: Vec<PathBuf>,
) -> AnalysisEffect<ValidatedFileSet<FileContent>> {
from_fn(move |env: &RealEnv| {
let mut result = ValidatedFileSet::empty();
for path in &paths {
match env.file_system().read_to_string(path) {
Ok(content) => {
result.add_valid(FileContent::new(path.clone(), content));
}
Err(e) => {
result.add_error(
FileError::new(path.clone(), format!("Failed to read: {}", e.message()))
.with_code("E001"),
);
}
}
}
Ok(result)
})
.boxed()
}
pub fn read_files_parallel_with_accumulation(
paths: Vec<PathBuf>,
) -> AnalysisEffect<ValidatedFileSet<FileContent>> {
from_async(move |env: &RealEnv| {
let env = env.clone();
let paths = paths.clone();
async move {
use tokio::task::JoinSet;
let mut result = ValidatedFileSet::empty();
let mut join_set = JoinSet::new();
for path in paths {
let env_clone = env.clone();
let path_clone = path.clone();
join_set.spawn(async move {
let read_result = env_clone.file_system().read_to_string(&path_clone);
(path_clone, read_result)
});
}
while let Some(task_result) = join_set.join_next().await {
match task_result {
Ok((path, read_result)) => match read_result {
Ok(content) => {
result.add_valid(FileContent::new(path, content));
}
Err(e) => {
result.add_error(
FileError::new(path, format!("Failed to read: {e}"))
.with_code("E001"),
);
}
},
Err(e) => {
result.add_error(
FileError::new(
PathBuf::from("<unknown>"),
format!("Task error: {}", e),
)
.with_code("E900"),
);
}
}
}
Ok(result)
}
})
.boxed()
}
pub fn process_files_with_accumulation<T, F>(
paths: Vec<PathBuf>,
processor: F,
) -> AnalysisEffect<ValidatedFileSet<T>>
where
T: Send + 'static,
F: Fn(&PathBuf, &str) -> Result<T, String> + Send + Sync + 'static,
{
use std::sync::Arc;
let processor = Arc::new(processor);
from_fn(move |env: &RealEnv| {
let mut result = ValidatedFileSet::empty();
for path in &paths {
match env.file_system().read_to_string(path) {
Ok(content) => match processor(path, &content) {
Ok(processed) => {
result.add_valid(processed);
}
Err(e) => {
result.add_error(FileError::new(path.clone(), e).with_code("E010"));
}
},
Err(e) => {
result.add_error(
FileError::new(path.clone(), format!("Failed to read: {}", e.message()))
.with_code("E001"),
);
}
}
}
Ok(result)
})
.boxed()
}
pub fn validated_file_set_to_strict_effect<T: Send + 'static>(
file_set: ValidatedFileSet<T>,
) -> Result<Vec<T>, AnalysisError> {
file_set.into_strict_result().map_err(|errors| {
let messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
AnalysisError::multi_file(messages)
})
}
pub fn validated_file_set_to_lenient_effect<T: Send + 'static>(
file_set: ValidatedFileSet<T>,
) -> Result<Vec<T>, AnalysisError> {
file_set.into_lenient_result().map_err(|errors| {
let messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
AnalysisError::multi_file(messages)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::DebtmapConfig;
use crate::effects::run_effect;
use tempfile::TempDir;
fn create_test_env() -> (TempDir, DebtmapConfig) {
let temp_dir = TempDir::new().unwrap();
(temp_dir, DebtmapConfig::default())
}
#[test]
fn test_read_file_effect_success() {
let (temp_dir, config) = create_test_env();
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, "Hello, World!").unwrap();
let effect = read_file_effect(file_path);
let result = run_effect(effect, config);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "Hello, World!");
}
#[test]
fn test_read_file_effect_not_found() {
let config = DebtmapConfig::default();
let effect = read_file_effect("/nonexistent/path/file.txt".into());
let result = run_effect(effect, config);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Failed to read"));
}
#[test]
fn test_write_file_effect() {
let (temp_dir, config) = create_test_env();
let file_path = temp_dir.path().join("output.txt");
let effect = write_file_effect(file_path.clone(), "Test content".to_string());
let result = run_effect(effect, config);
assert!(result.is_ok());
assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "Test content");
}
#[test]
fn test_file_exists_effect() {
let (temp_dir, config) = create_test_env();
let file_path = temp_dir.path().join("exists.txt");
std::fs::write(&file_path, "").unwrap();
let effect = file_exists_effect(file_path);
assert!(run_effect(effect, config.clone()).unwrap());
let effect = file_exists_effect(temp_dir.path().join("nonexistent.txt"));
assert!(!run_effect(effect, config).unwrap());
}
#[test]
fn test_is_directory_effect() {
let (temp_dir, config) = create_test_env();
let dir_path = temp_dir.path().join("subdir");
std::fs::create_dir(&dir_path).unwrap();
let effect = is_directory_effect(dir_path);
assert!(run_effect(effect, config).unwrap());
}
#[test]
fn test_read_files_with_accumulation_all_success() {
let (temp_dir, config) = create_test_env();
std::fs::write(temp_dir.path().join("a.txt"), "content a").unwrap();
std::fs::write(temp_dir.path().join("b.txt"), "content b").unwrap();
std::fs::write(temp_dir.path().join("c.txt"), "content c").unwrap();
let paths = vec![
temp_dir.path().join("a.txt"),
temp_dir.path().join("b.txt"),
temp_dir.path().join("c.txt"),
];
let effect = read_files_with_accumulation(paths);
let result = run_effect(effect, config).unwrap();
assert!(result.is_all_success());
assert_eq!(result.valid.len(), 3);
assert_eq!(result.errors.len(), 0);
}
#[test]
fn test_read_files_with_accumulation_partial_success() {
let (temp_dir, config) = create_test_env();
std::fs::write(temp_dir.path().join("good1.txt"), "content 1").unwrap();
std::fs::write(temp_dir.path().join("good2.txt"), "content 2").unwrap();
let paths = vec![
temp_dir.path().join("good1.txt"),
temp_dir.path().join("missing.txt"), temp_dir.path().join("good2.txt"),
temp_dir.path().join("also_missing.txt"), ];
let effect = read_files_with_accumulation(paths);
let result = run_effect(effect, config).unwrap();
assert!(result.is_partial_success());
assert_eq!(result.valid.len(), 2);
assert_eq!(result.errors.len(), 2);
let error_paths: Vec<_> = result.errors.iter().map(|e| e.path.clone()).collect();
assert!(error_paths
.iter()
.any(|p| p.file_name().unwrap() == "missing.txt"));
assert!(error_paths
.iter()
.any(|p| p.file_name().unwrap() == "also_missing.txt"));
}
#[test]
fn test_read_files_with_accumulation_all_failed() {
let config = DebtmapConfig::default();
let paths = vec![
PathBuf::from("/nonexistent/a.txt"),
PathBuf::from("/nonexistent/b.txt"),
];
let effect = read_files_with_accumulation(paths);
let result = run_effect(effect, config).unwrap();
assert!(result.is_all_failed());
assert_eq!(result.valid.len(), 0);
assert_eq!(result.errors.len(), 2);
}
#[test]
fn test_read_files_with_accumulation_empty() {
let config = DebtmapConfig::default();
let paths: Vec<PathBuf> = vec![];
let effect = read_files_with_accumulation(paths);
let result = run_effect(effect, config).unwrap();
assert!(!result.is_partial_success());
assert!(!result.is_all_success()); assert!(!result.is_all_failed()); assert_eq!(result.valid.len(), 0);
assert_eq!(result.errors.len(), 0);
}
#[test]
fn test_process_files_with_accumulation() {
let (temp_dir, config) = create_test_env();
std::fs::write(temp_dir.path().join("num1.txt"), "42").unwrap();
std::fs::write(temp_dir.path().join("num2.txt"), "not a number").unwrap(); std::fs::write(temp_dir.path().join("num3.txt"), "100").unwrap();
let paths = vec![
temp_dir.path().join("num1.txt"),
temp_dir.path().join("num2.txt"),
temp_dir.path().join("num3.txt"),
];
let effect = process_files_with_accumulation(paths, |_path, content| {
content
.trim()
.parse::<i32>()
.map_err(|e| format!("Parse error: {}", e))
});
let result = run_effect(effect, config).unwrap();
assert!(result.is_partial_success());
assert_eq!(result.valid.len(), 2);
assert_eq!(result.errors.len(), 1);
assert!(result.valid.contains(&42));
assert!(result.valid.contains(&100));
}
#[test]
fn test_validated_file_set_to_strict_effect() {
let mut file_set: ValidatedFileSet<String> = ValidatedFileSet::empty();
file_set.add_valid("good".to_string());
file_set.add_error(FileError::new("bad.txt", "error"));
let result = validated_file_set_to_strict_effect(file_set);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("bad.txt"));
}
#[test]
fn test_validated_file_set_to_lenient_effect() {
let mut file_set: ValidatedFileSet<String> = ValidatedFileSet::empty();
file_set.add_valid("good".to_string());
file_set.add_error(FileError::new("bad.txt", "error"));
let result = validated_file_set_to_lenient_effect(file_set);
assert!(result.is_ok());
assert_eq!(result.unwrap(), vec!["good".to_string()]);
}
#[test]
fn test_file_content_struct() {
let fc = FileContent::new(PathBuf::from("test.txt"), "content".to_string());
assert_eq!(fc.path, PathBuf::from("test.txt"));
assert_eq!(fc.content, "content");
}
}