guild_cli/config/
project.rs1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use serde::Deserialize;
5
6use crate::config::types::{DependsOn, ProjectName, TargetName};
7use crate::error::ConfigError;
8
9#[derive(Debug, Deserialize)]
11struct ProjectToml {
12 project: ProjectSection,
13 #[serde(default)]
14 targets: HashMap<TargetName, TargetSection>,
15}
16
17#[derive(Debug, Deserialize)]
18struct ProjectSection {
19 name: ProjectName,
20 #[serde(default)]
21 tags: Vec<String>,
22 #[serde(default)]
23 depends_on: Vec<ProjectName>,
24}
25
26#[derive(Debug, Deserialize)]
27struct TargetSection {
28 command: String,
29 #[serde(default)]
30 depends_on: Vec<DependsOn>,
31 #[serde(default)]
32 inputs: Vec<String>,
33 #[serde(default)]
34 outputs: Vec<String>,
35}
36
37#[derive(Debug, Clone)]
39pub struct ProjectConfig {
40 name: ProjectName,
41 tags: Vec<String>,
42 depends_on: Vec<ProjectName>,
43 targets: HashMap<TargetName, TargetConfig>,
44 root: PathBuf,
45}
46
47#[derive(Debug, Clone)]
49pub struct TargetConfig {
50 command: String,
51 depends_on: Vec<DependsOn>,
52 inputs: Vec<String>,
53 outputs: Vec<String>,
54}
55
56impl ProjectConfig {
57 pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
59 let content = std::fs::read_to_string(path).map_err(|e| ConfigError::ReadFile {
60 path: path.to_path_buf(),
61 source: e,
62 })?;
63 let root = path
64 .parent()
65 .expect("guild.toml must have a parent directory")
66 .to_path_buf();
67 Self::from_str(&content, root)
68 }
69
70 pub fn from_str(content: &str, root: PathBuf) -> Result<Self, ConfigError> {
72 let raw: ProjectToml = toml::from_str(content).map_err(|e| ConfigError::ParseToml {
73 path: root.join("guild.toml"),
74 source: e,
75 })?;
76 let targets = raw
77 .targets
78 .into_iter()
79 .map(|(name, section)| {
80 let config = TargetConfig {
81 command: section.command,
82 depends_on: section.depends_on,
83 inputs: section.inputs,
84 outputs: section.outputs,
85 };
86 (name, config)
87 })
88 .collect();
89 Ok(Self {
90 name: raw.project.name,
91 tags: raw.project.tags,
92 depends_on: raw.project.depends_on,
93 targets,
94 root,
95 })
96 }
97
98 pub fn name(&self) -> &ProjectName {
99 &self.name
100 }
101
102 pub fn tags(&self) -> &[String] {
103 &self.tags
104 }
105
106 pub fn depends_on(&self) -> &[ProjectName] {
107 &self.depends_on
108 }
109
110 pub fn targets(&self) -> &HashMap<TargetName, TargetConfig> {
111 &self.targets
112 }
113
114 pub fn root(&self) -> &Path {
115 &self.root
116 }
117}
118
119impl TargetConfig {
120 pub fn command(&self) -> &str {
121 &self.command
122 }
123
124 pub fn depends_on(&self) -> &[DependsOn] {
125 &self.depends_on
126 }
127
128 pub fn inputs(&self) -> &[String] {
129 &self.inputs
130 }
131
132 pub fn outputs(&self) -> &[String] {
133 &self.outputs
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn test_parse_project_config() {
143 let toml = r#"
144[project]
145name = "my-app"
146tags = ["app", "typescript"]
147depends_on = ["shared-lib"]
148
149[targets.build]
150command = "npm run build"
151depends_on = ["^build"]
152
153[targets.test]
154command = "npm test"
155depends_on = ["build"]
156
157[targets.lint]
158command = "npm run lint"
159"#;
160 let config = ProjectConfig::from_str(toml, PathBuf::from("/tmp/my-app")).unwrap();
161 assert_eq!(config.name().as_str(), "my-app");
162 assert_eq!(config.tags(), &["app", "typescript"]);
163 assert_eq!(config.depends_on().len(), 1);
164 assert_eq!(config.depends_on()[0].as_str(), "shared-lib");
165 assert_eq!(config.targets().len(), 3);
166
167 let build = &config.targets()[&"build".parse::<TargetName>().unwrap()];
168 assert_eq!(build.command(), "npm run build");
169 assert_eq!(build.depends_on().len(), 1);
170 assert!(build.depends_on()[0].is_upstream());
171 }
172
173 #[test]
174 fn test_parse_minimal_project() {
175 let toml = r#"
176[project]
177name = "minimal"
178"#;
179 let config = ProjectConfig::from_str(toml, PathBuf::from("/tmp/minimal")).unwrap();
180 assert_eq!(config.name().as_str(), "minimal");
181 assert!(config.tags().is_empty());
182 assert!(config.depends_on().is_empty());
183 assert!(config.targets().is_empty());
184 }
185
186 #[test]
187 fn test_parse_project_invalid_name() {
188 let toml = r#"
189[project]
190name = "My App"
191"#;
192 assert!(ProjectConfig::from_str(toml, PathBuf::from("/tmp")).is_err());
193 }
194
195 #[test]
196 fn test_target_with_inputs_outputs() {
197 let toml = r#"
198[project]
199name = "my-app"
200
201[targets.build]
202command = "cargo build"
203inputs = ["src/**/*.rs", "Cargo.toml"]
204outputs = ["target/release/my-app"]
205"#;
206 let config = ProjectConfig::from_str(toml, PathBuf::from("/tmp/my-app")).unwrap();
207 let build = &config.targets()[&"build".parse::<TargetName>().unwrap()];
208 assert_eq!(build.inputs(), &["src/**/*.rs", "Cargo.toml"]);
209 assert_eq!(build.outputs(), &["target/release/my-app"]);
210 }
211}