1use rustc_hash::FxHashMap;
4use serde::{Deserialize, Deserializer, Serialize};
5
6use crate::package::{Language, Task};
7use crate::remote_cache::RemoteCacheConfig;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(untagged)]
11pub enum TaskValue {
12 Simple(String),
13 Complex {
14 command: String,
15 #[serde(default)]
16 depends_on: Vec<String>,
17 },
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Config {
23 pub name: String,
24 pub language: String,
25 pub public: bool,
26 #[serde(default)]
27 pub deps: Deps,
28 #[serde(deserialize_with = "deserialize_tasks")]
29 #[serde(default)]
30 pub tasks: FxHashMap<String, TaskValue>,
31}
32
33pub(crate) fn deserialize_tasks<'de, D>(
34 deserializer: D,
35) -> Result<FxHashMap<String, TaskValue>, D::Error>
36where
37 D: Deserializer<'de>,
38{
39 let map: FxHashMap<String, toml::Value> = FxHashMap::deserialize(deserializer)?;
40 let mut result = FxHashMap::default();
41 let mut dotted_deps: FxHashMap<String, Vec<String>> = FxHashMap::default();
42
43 for (key, value) in map {
45 if let Some((task_name, dep_key)) = key.split_once('.') {
47 if dep_key == "depends_on" {
48 if let toml::Value::Array(arr) = value {
49 let deps: Vec<String> = arr
50 .iter()
51 .filter_map(|v| v.as_str().map(|s| s.to_string()))
52 .collect();
53 dotted_deps.insert(task_name.to_string(), deps);
54 } else {
55 return Err(serde::de::Error::custom(format!(
56 "Task dependency '{}' must be an array",
57 key
58 )));
59 }
60 continue;
61 }
62 }
63
64 match value {
65 toml::Value::String(s) => {
66 result.insert(key, TaskValue::Simple(s));
67 }
68 toml::Value::Table(t) => {
69 let command = t
70 .get("command")
71 .and_then(|v| v.as_str())
72 .ok_or_else(|| {
73 serde::de::Error::custom("Task table must have 'command' field")
74 })?
75 .to_string();
76 let depends_on = t
77 .get("depends_on")
78 .and_then(|v| v.as_array())
79 .map(|arr| {
80 arr.iter()
81 .filter_map(|v| v.as_str().map(|s| s.to_string()))
82 .collect()
83 })
84 .unwrap_or_default();
85 result.insert(
86 key,
87 TaskValue::Complex {
88 command,
89 depends_on,
90 },
91 );
92 }
93 _ => {
94 return Err(serde::de::Error::custom(
95 "Task value must be a string or a table",
96 ));
97 }
98 }
99 }
100
101 for (task_name, deps) in dotted_deps {
103 let task_value = result.remove(&task_name).ok_or_else(|| {
104 serde::de::Error::custom(format!(
105 "Task '{}' referenced in dotted-key dependency does not exist",
106 task_name
107 ))
108 })?;
109
110 match task_value {
111 TaskValue::Simple(command) => {
112 result.insert(
113 task_name,
114 TaskValue::Complex {
115 command,
116 depends_on: deps,
117 },
118 );
119 }
120 TaskValue::Complex { command, .. } => {
121 result.insert(
123 task_name,
124 TaskValue::Complex {
125 command,
126 depends_on: deps,
127 },
128 );
129 }
130 }
131 }
132
133 Ok(result)
134}
135
136pub(crate) fn parse_tasks_from_toml_map(
137 map: &toml::map::Map<String, toml::Value>,
138) -> FxHashMap<String, TaskValue> {
139 let mut result = FxHashMap::default();
140 let mut dotted_deps: FxHashMap<String, Vec<String>> = FxHashMap::default();
141
142 for (key, value) in map {
143 if let Some((task_name, dep_key)) = key.split_once('.') {
144 if dep_key == "depends_on" {
145 if let toml::Value::Array(arr) = value {
146 let deps: Vec<String> = arr
147 .iter()
148 .filter_map(|v| v.as_str().map(|s| s.to_string()))
149 .collect();
150 dotted_deps.insert(task_name.to_string(), deps);
151 }
152 continue;
153 }
154 }
155
156 match value {
157 toml::Value::String(s) => {
158 result.insert(key.clone(), TaskValue::Simple(s.clone()));
159 }
160 toml::Value::Table(t) => {
161 if let Some(command_val) = t.get("command").and_then(|v| v.as_str()) {
162 let depends_on = t
163 .get("depends_on")
164 .and_then(|v| v.as_array())
165 .map(|arr| {
166 arr.iter()
167 .filter_map(|v| v.as_str().map(|s| s.to_string()))
168 .collect()
169 })
170 .unwrap_or_default();
171 result.insert(
172 key.clone(),
173 TaskValue::Complex {
174 command: command_val.to_string(),
175 depends_on,
176 },
177 );
178 }
179 }
180 _ => {}
181 }
182 }
183
184 for (task_name, deps) in dotted_deps {
185 if let Some(task_value) = result.remove(&task_name) {
186 match task_value {
187 TaskValue::Simple(command) => {
188 result.insert(
189 task_name,
190 TaskValue::Complex {
191 command,
192 depends_on: deps,
193 },
194 );
195 }
196 TaskValue::Complex { command, .. } => {
197 result.insert(
198 task_name,
199 TaskValue::Complex {
200 command,
201 depends_on: deps,
202 },
203 );
204 }
205 }
206 }
207 }
208
209 result
210}
211
212#[derive(Debug, Clone, Default, Serialize, Deserialize)]
214pub struct Deps {
215 #[serde(default)]
217 pub internal: Vec<String>,
218}
219
220#[derive(Debug, Clone, Default, Serialize, Deserialize)]
222pub struct WorkspaceConfig {
223 pub cache_dir: Option<String>,
225 pub default_parallel: Option<usize>,
227 #[serde(skip)]
229 pub workspace_config_path: Option<std::path::PathBuf>,
230 #[serde(default, deserialize_with = "deserialize_tasks")]
232 pub tasks: FxHashMap<String, TaskValue>,
233 #[serde(default)]
235 pub remote_cache: Option<RemoteCacheConfig>,
236}
237
238impl Config {
239 pub fn parse_language(&self) -> Result<Language, crate::Error> {
240 Language::from_str(&self.language).ok_or_else(|| crate::Error::InvalidLanguage {
241 lang: self.language.clone(),
242 })
243 }
244
245 pub fn to_tasks(&self) -> Vec<Task> {
246 self.tasks
247 .iter()
248 .map(|(name, task_value)| match task_value {
249 TaskValue::Simple(command) => Task {
250 name: name.clone(),
251 command: command.clone(),
252 depends_on: Vec::new(),
253 },
254 TaskValue::Complex {
255 command,
256 depends_on,
257 } => Task {
258 name: name.clone(),
259 command: command.clone(),
260 depends_on: depends_on.clone(),
261 },
262 })
263 .collect()
264 }
265}
266
267impl WorkspaceConfig {
268 pub fn to_tasks(&self) -> Vec<Task> {
269 self.tasks
270 .iter()
271 .map(|(name, task_value)| match task_value {
272 TaskValue::Simple(command) => Task {
273 name: name.clone(),
274 command: command.clone(),
275 depends_on: Vec::new(),
276 },
277 TaskValue::Complex {
278 command,
279 depends_on,
280 } => Task {
281 name: name.clone(),
282 command: command.clone(),
283 depends_on: depends_on.clone(),
284 },
285 })
286 .collect()
287 }
288}