use super::loader::{ConfigLoader, ConfigTier};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileSource {
User,
Project,
ProjectDeprecated,
Embedded,
}
impl std::fmt::Display for FileSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FileSource::User => write!(f, "user"),
FileSource::Project => write!(f, "project"),
FileSource::ProjectDeprecated => write!(f, "project (deprecated)"),
FileSource::Embedded => write!(f, "embedded"),
}
}
}
#[derive(Debug, Clone)]
pub struct ResolvedFile {
pub content: String,
pub path: Option<PathBuf>,
pub source: FileSource,
}
impl ResolvedFile {
pub fn from_disk(content: String, path: PathBuf, source: FileSource) -> Self {
Self {
content,
path: Some(path),
source,
}
}
pub fn from_embedded(content: &'static str) -> Self {
Self {
content: content.to_string(),
path: None,
source: FileSource::Embedded,
}
}
}
impl ConfigLoader {
pub fn find_file(&self, relative_path: &str) -> Option<ResolvedFile> {
if let Some(ref user_dir) = self.paths.user_dir {
let path = user_dir.join(relative_path);
if path.exists()
&& let Ok(content) = std::fs::read_to_string(&path)
{
return Some(ResolvedFile::from_disk(content, path, FileSource::User));
}
}
if let Some(ref project_dir) = self.paths.project_dir
&& project_dir.exists()
{
let path = project_dir.join(relative_path);
if path.exists()
&& let Ok(content) = std::fs::read_to_string(&path)
{
return Some(ResolvedFile::from_disk(content, path, FileSource::Project));
}
}
if let Some(ref project_dir) = self.paths.project_dir_deprecated
&& project_dir.exists()
{
let path = project_dir.join(relative_path);
if path.exists()
&& let Ok(content) = std::fs::read_to_string(&path)
{
return Some(ResolvedFile::from_disk(
content,
path,
FileSource::ProjectDeprecated,
));
}
}
None
}
pub fn find_file_path(&self, relative_path: &str) -> Option<PathBuf> {
if let Some(ref user_dir) = self.paths.user_dir {
let path = user_dir.join(relative_path);
if path.exists() {
return Some(path);
}
}
if let Some(ref project_dir) = self.paths.project_dir
&& project_dir.exists()
{
let path = project_dir.join(relative_path);
if path.exists() {
return Some(path);
}
}
if let Some(ref project_dir) = self.paths.project_dir_deprecated
&& project_dir.exists()
{
let path = project_dir.join(relative_path);
if path.exists() {
return Some(path);
}
}
None
}
pub fn file_exists(&self, relative_path: &str) -> bool {
self.find_file_path(relative_path).is_some()
}
pub fn file_tier(&self, relative_path: &str) -> Option<ConfigTier> {
if let Some(ref user_dir) = self.paths.user_dir
&& user_dir.join(relative_path).exists()
{
return Some(ConfigTier::User);
}
if let Some(ref project_dir) = self.paths.project_dir
&& project_dir.exists()
&& project_dir.join(relative_path).exists()
{
return Some(ConfigTier::Project);
}
if let Some(ref project_dir) = self.paths.project_dir_deprecated
&& project_dir.exists()
&& project_dir.join(relative_path).exists()
{
return Some(ConfigTier::Project);
}
None
}
pub fn list_files(&self, relative_dir: &str) -> Vec<(String, FileSource)> {
use std::collections::HashMap;
let mut files: HashMap<String, FileSource> = HashMap::new();
if let Some(ref project_dir) = self.paths.project_dir_deprecated {
let dir = project_dir.join(relative_dir);
if dir.is_dir()
&& let Ok(entries) = std::fs::read_dir(&dir)
{
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
files.insert(name.to_string(), FileSource::ProjectDeprecated);
}
}
}
}
if let Some(ref project_dir) = self.paths.project_dir {
let dir = project_dir.join(relative_dir);
if dir.is_dir()
&& let Ok(entries) = std::fs::read_dir(&dir)
{
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
files.insert(name.to_string(), FileSource::Project);
}
}
}
}
if let Some(ref user_dir) = self.paths.user_dir {
let dir = user_dir.join(relative_dir);
if dir.is_dir()
&& let Ok(entries) = std::fs::read_dir(&dir)
{
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
files.insert(name.to_string(), FileSource::User);
}
}
}
}
let mut result: Vec<_> = files.into_iter().collect();
result.sort_by(|a, b| a.0.cmp(&b.0));
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::ConfigPaths;
use tempfile::TempDir;
fn create_test_loader(temp: &TempDir) -> ConfigLoader {
let project_dir = temp.path().join("task-graph");
let user_dir = temp.path().join("user");
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::create_dir_all(&user_dir).unwrap();
let paths = ConfigPaths::with_dirs(Some(project_dir), Some(user_dir));
ConfigLoader::load_with_paths(paths).unwrap()
}
#[test]
fn test_find_file_user_priority() {
let temp = TempDir::new().unwrap();
let project_dir = temp.path().join("task-graph");
let user_dir = temp.path().join("user");
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::create_dir_all(&user_dir).unwrap();
std::fs::write(project_dir.join("test.txt"), "project content").unwrap();
std::fs::write(user_dir.join("test.txt"), "user content").unwrap();
let paths = ConfigPaths::with_dirs(Some(project_dir), Some(user_dir));
let loader = ConfigLoader::load_with_paths(paths).unwrap();
let file = loader.find_file("test.txt").unwrap();
assert_eq!(file.content, "user content");
assert_eq!(file.source, FileSource::User);
}
#[test]
fn test_find_file_project_fallback() {
let temp = TempDir::new().unwrap();
let project_dir = temp.path().join("task-graph");
let user_dir = temp.path().join("user");
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::create_dir_all(&user_dir).unwrap();
std::fs::write(project_dir.join("test.txt"), "project content").unwrap();
let paths = ConfigPaths::with_dirs(Some(project_dir), Some(user_dir));
let loader = ConfigLoader::load_with_paths(paths).unwrap();
let file = loader.find_file("test.txt").unwrap();
assert_eq!(file.content, "project content");
assert_eq!(file.source, FileSource::Project);
}
#[test]
fn test_find_file_not_found() {
let temp = TempDir::new().unwrap();
let loader = create_test_loader(&temp);
assert!(loader.find_file("nonexistent.txt").is_none());
}
#[test]
fn test_list_files_deduplication() {
let temp = TempDir::new().unwrap();
let project_dir = temp.path().join("task-graph");
let user_dir = temp.path().join("user");
let project_skills = project_dir.join("skills");
let user_skills = user_dir.join("skills");
std::fs::create_dir_all(&project_skills).unwrap();
std::fs::create_dir_all(&user_skills).unwrap();
std::fs::write(project_skills.join("shared.md"), "project").unwrap();
std::fs::write(project_skills.join("project-only.md"), "project").unwrap();
std::fs::write(user_skills.join("shared.md"), "user").unwrap();
std::fs::write(user_skills.join("user-only.md"), "user").unwrap();
let paths = ConfigPaths::with_dirs(Some(project_dir), Some(user_dir));
let loader = ConfigLoader::load_with_paths(paths).unwrap();
let files = loader.list_files("skills");
assert_eq!(files.len(), 3);
let shared = files.iter().find(|(name, _)| name == "shared.md").unwrap();
assert_eq!(shared.1, FileSource::User);
}
}