1use serde::{Deserialize, Serialize};
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Default, Serialize, Deserialize)]
8pub struct Config {
9 #[serde(default)]
11 pub project: ProjectConfig,
12}
13
14#[derive(Debug, Serialize, Deserialize)]
15pub struct ProjectConfig {
16 #[serde(default = "default_tasks_dir")]
18 pub tasks_dir: String,
19}
20
21impl Default for ProjectConfig {
22 fn default() -> Self {
23 Self {
24 tasks_dir: default_tasks_dir(),
25 }
26 }
27}
28
29fn default_tasks_dir() -> String {
30 "tasks".to_string()
31}
32
33impl Config {
34 pub fn from_file(path: &Path) -> crate::Result<Self> {
36 let content = std::fs::read_to_string(path)?;
37 let config: Config = toml::from_str(&content)
38 .map_err(|e| crate::Error::Graph(format!("Config parse error: {}", e)))?;
39 Ok(config)
40 }
41
42 pub fn find_and_load() -> Option<Self> {
44 let mut current_dir = std::env::current_dir().ok()?;
45
46 loop {
47 let config_path = current_dir.join(".taskgraph.toml");
48 if config_path.exists() {
49 return Self::from_file(&config_path).ok();
50 }
51
52 match current_dir.parent() {
54 Some(parent) => current_dir = parent.to_path_buf(),
55 None => break,
56 }
57 }
58
59 None
60 }
61
62 pub fn tasks_path(&self) -> PathBuf {
64 PathBuf::from(&self.project.tasks_dir)
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use std::io::Write;
72 use tempfile::NamedTempFile;
73
74 #[test]
75 fn test_default_config() {
76 let config = Config::default();
77 assert_eq!(config.project.tasks_dir, "tasks");
78 }
79
80 #[test]
81 fn test_load_config() {
82 let mut file = NamedTempFile::new().unwrap();
83 writeln!(
84 file,
85 r#"
86[project]
87tasks_dir = "my-tasks"
88"#
89 )
90 .unwrap();
91
92 let config = Config::from_file(file.path()).unwrap();
93 assert_eq!(config.project.tasks_dir, "my-tasks");
94 }
95}