1use crate::models::{Resource, Task};
14use std::collections::{HashMap, HashSet};
15
16pub type ValidationResult = Result<(), Vec<ValidationError>>;
18
19#[derive(Debug, Clone, PartialEq)]
21pub struct ValidationError {
22 pub kind: ValidationErrorKind,
24 pub message: String,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum ValidationErrorKind {
31 DuplicateId,
33 InvalidResourceReference,
35 CyclicDependency,
37 EmptyTask,
39 InvalidPredecessor,
41}
42
43impl ValidationError {
44 fn new(kind: ValidationErrorKind, message: impl Into<String>) -> Self {
45 Self {
46 kind,
47 message: message.into(),
48 }
49 }
50}
51
52pub fn validate_input(tasks: &[Task], resources: &[Resource]) -> ValidationResult {
66 let mut errors = Vec::new();
67
68 let mut resource_ids = HashSet::new();
70 for r in resources {
71 if !resource_ids.insert(r.id.as_str()) {
72 errors.push(ValidationError::new(
73 ValidationErrorKind::DuplicateId,
74 format!("Duplicate resource ID: {}", r.id),
75 ));
76 }
77 }
78
79 let mut task_ids = HashSet::new();
81 let mut activity_ids = HashSet::new();
82
83 for task in tasks {
84 if !task_ids.insert(task.id.as_str()) {
85 errors.push(ValidationError::new(
86 ValidationErrorKind::DuplicateId,
87 format!("Duplicate task ID: {}", task.id),
88 ));
89 }
90
91 if task.activities.is_empty() {
92 errors.push(ValidationError::new(
93 ValidationErrorKind::EmptyTask,
94 format!("Task '{}' has no activities", task.id),
95 ));
96 }
97
98 for act in &task.activities {
99 if !activity_ids.insert(act.id.as_str()) {
100 errors.push(ValidationError::new(
101 ValidationErrorKind::DuplicateId,
102 format!("Duplicate activity ID: {}", act.id),
103 ));
104 }
105 }
106 }
107
108 for task in tasks {
110 for act in &task.activities {
111 for req in &act.resource_requirements {
112 for cand in &req.candidates {
113 if !resource_ids.contains(cand.as_str()) {
114 errors.push(ValidationError::new(
115 ValidationErrorKind::InvalidResourceReference,
116 format!(
117 "Activity '{}' references unknown resource '{}'",
118 act.id, cand
119 ),
120 ));
121 }
122 }
123 }
124 }
125 }
126
127 for task in tasks {
129 for act in &task.activities {
130 for pred in &act.predecessors {
131 if !activity_ids.contains(pred.as_str()) {
132 errors.push(ValidationError::new(
133 ValidationErrorKind::InvalidPredecessor,
134 format!(
135 "Activity '{}' references unknown predecessor '{}'",
136 act.id, pred
137 ),
138 ));
139 }
140 }
141 }
142 }
143
144 if let Some(cycle_err) = detect_cycles(tasks) {
146 errors.push(cycle_err);
147 }
148
149 if errors.is_empty() {
150 Ok(())
151 } else {
152 Err(errors)
153 }
154}
155
156fn detect_cycles(tasks: &[Task]) -> Option<ValidationError> {
165 let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
167 let mut all_ids: HashSet<&str> = HashSet::new();
168
169 for task in tasks {
170 for act in &task.activities {
171 all_ids.insert(&act.id);
172 for pred in &act.predecessors {
173 adj.entry(pred.as_str()).or_default().push(act.id.as_str());
174 }
175 }
176 }
177
178 let mut visited = HashSet::new();
180 let mut in_stack = HashSet::new();
181
182 for &node in &all_ids {
183 if !visited.contains(node) && has_cycle_dfs(node, &adj, &mut visited, &mut in_stack) {
184 return Some(ValidationError::new(
185 ValidationErrorKind::CyclicDependency,
186 format!("Circular dependency detected involving activity '{node}'"),
187 ));
188 }
189 }
190
191 None
192}
193
194fn has_cycle_dfs<'a>(
195 node: &'a str,
196 adj: &HashMap<&'a str, Vec<&'a str>>,
197 visited: &mut HashSet<&'a str>,
198 in_stack: &mut HashSet<&'a str>,
199) -> bool {
200 visited.insert(node);
201 in_stack.insert(node);
202
203 if let Some(neighbors) = adj.get(node) {
204 for &next in neighbors {
205 if in_stack.contains(next) {
206 return true; }
208 if !visited.contains(next) && has_cycle_dfs(next, adj, visited, in_stack) {
209 return true;
210 }
211 }
212 }
213
214 in_stack.remove(node);
215 false
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use crate::models::{Activity, ActivityDuration, Resource, ResourceRequirement, Task};
222
223 fn sample_resources() -> Vec<Resource> {
224 vec![
225 Resource::primary("M1").with_name("Machine 1"),
226 Resource::primary("M2").with_name("Machine 2"),
227 Resource::human("W1").with_name("Worker 1"),
228 ]
229 }
230
231 fn sample_tasks() -> Vec<Task> {
232 vec![
233 Task::new("J1")
234 .with_activity(
235 Activity::new("O1", "J1", 0)
236 .with_duration(ActivityDuration::fixed(1000))
237 .with_requirement(
238 ResourceRequirement::new("Machine").with_candidates(vec!["M1".into()]),
239 ),
240 )
241 .with_activity(
242 Activity::new("O2", "J1", 1)
243 .with_duration(ActivityDuration::fixed(2000))
244 .with_predecessor("O1")
245 .with_requirement(
246 ResourceRequirement::new("Machine").with_candidates(vec!["M2".into()]),
247 ),
248 ),
249 Task::new("J2").with_activity(
250 Activity::new("O3", "J2", 0)
251 .with_duration(ActivityDuration::fixed(1500))
252 .with_requirement(
253 ResourceRequirement::new("Machine").with_candidates(vec!["M1".into()]),
254 ),
255 ),
256 ]
257 }
258
259 #[test]
260 fn test_valid_input() {
261 let tasks = sample_tasks();
262 let resources = sample_resources();
263 assert!(validate_input(&tasks, &resources).is_ok());
264 }
265
266 #[test]
267 fn test_duplicate_task_id() {
268 let tasks = vec![
269 Task::new("J1").with_activity(Activity::new("O1", "J1", 0).with_process_time(100)),
270 Task::new("J1").with_activity(Activity::new("O2", "J1", 0).with_process_time(100)),
271 ];
272 let resources = sample_resources();
273
274 let errors = validate_input(&tasks, &resources).unwrap_err();
275 assert!(errors
276 .iter()
277 .any(|e| e.kind == ValidationErrorKind::DuplicateId));
278 }
279
280 #[test]
281 fn test_duplicate_resource_id() {
282 let tasks = sample_tasks();
283 let resources = vec![Resource::primary("M1"), Resource::primary("M1")];
284
285 let errors = validate_input(&tasks, &resources).unwrap_err();
286 assert!(errors
287 .iter()
288 .any(|e| e.kind == ValidationErrorKind::DuplicateId && e.message.contains("resource")));
289 }
290
291 #[test]
292 fn test_empty_task() {
293 let tasks = vec![Task::new("empty")]; let resources = sample_resources();
295
296 let errors = validate_input(&tasks, &resources).unwrap_err();
297 assert!(errors
298 .iter()
299 .any(|e| e.kind == ValidationErrorKind::EmptyTask));
300 }
301
302 #[test]
303 fn test_invalid_resource_reference() {
304 let tasks = vec![Task::new("J1").with_activity(
305 Activity::new("O1", "J1", 0)
306 .with_process_time(100)
307 .with_requirement(
308 ResourceRequirement::new("Machine").with_candidates(vec!["NONEXISTENT".into()]),
309 ),
310 )];
311 let resources = sample_resources();
312
313 let errors = validate_input(&tasks, &resources).unwrap_err();
314 assert!(errors
315 .iter()
316 .any(|e| e.kind == ValidationErrorKind::InvalidResourceReference));
317 }
318
319 #[test]
320 fn test_invalid_predecessor() {
321 let tasks = vec![Task::new("J1").with_activity(
322 Activity::new("O1", "J1", 0)
323 .with_process_time(100)
324 .with_predecessor("NONEXISTENT"),
325 )];
326 let resources = sample_resources();
327
328 let errors = validate_input(&tasks, &resources).unwrap_err();
329 assert!(errors
330 .iter()
331 .any(|e| e.kind == ValidationErrorKind::InvalidPredecessor));
332 }
333
334 #[test]
335 fn test_cyclic_dependency() {
336 let tasks = vec![Task::new("J1")
338 .with_activity(
339 Activity::new("O1", "J1", 0)
340 .with_process_time(100)
341 .with_predecessor("O3"),
342 )
343 .with_activity(
344 Activity::new("O2", "J1", 1)
345 .with_process_time(100)
346 .with_predecessor("O1"),
347 )
348 .with_activity(
349 Activity::new("O3", "J1", 2)
350 .with_process_time(100)
351 .with_predecessor("O2"),
352 )];
353 let resources = sample_resources();
354
355 let errors = validate_input(&tasks, &resources).unwrap_err();
356 assert!(errors
357 .iter()
358 .any(|e| e.kind == ValidationErrorKind::CyclicDependency));
359 }
360
361 #[test]
362 fn test_no_cycle_in_chain() {
363 let tasks = vec![Task::new("J1")
365 .with_activity(Activity::new("O1", "J1", 0).with_process_time(100))
366 .with_activity(
367 Activity::new("O2", "J1", 1)
368 .with_process_time(100)
369 .with_predecessor("O1"),
370 )
371 .with_activity(
372 Activity::new("O3", "J1", 2)
373 .with_process_time(100)
374 .with_predecessor("O2"),
375 )];
376 let resources = sample_resources();
377
378 assert!(validate_input(&tasks, &resources).is_ok());
379 }
380
381 #[test]
382 fn test_multiple_errors() {
383 let tasks = vec![
385 Task::new("empty"), Task::new("J1").with_activity(
387 Activity::new("O1", "J1", 0)
388 .with_process_time(100)
389 .with_requirement(
390 ResourceRequirement::new("M").with_candidates(vec!["UNKNOWN".into()]),
391 ),
392 ),
393 ];
394 let resources = vec![];
395
396 let errors = validate_input(&tasks, &resources).unwrap_err();
397 assert!(errors.len() >= 2);
398 }
399}