roder_roadmap/
validator.rs1use std::collections::HashSet;
2
3use crate::parser::path_diagnostic;
4use crate::{Diagnostic, DiagnosticSeverity, Document, ValidationResult};
5
6pub fn validate_document(document: &Document) -> ValidationResult {
7 let mut diagnostics = Vec::new();
8 if let Some(diagnostic) = path_diagnostic(&document.path) {
9 diagnostics.push(diagnostic);
10 }
11 require(
12 &mut diagnostics,
13 document,
14 !document.title.trim().is_empty(),
15 None,
16 "missing title heading",
17 );
18 require(
19 &mut diagnostics,
20 document,
21 !document.goal.trim().is_empty(),
22 None,
23 "missing **Goal:** field",
24 );
25 require(
26 &mut diagnostics,
27 document,
28 !document.architecture.trim().is_empty(),
29 None,
30 "missing **Architecture:** field",
31 );
32 require(
33 &mut diagnostics,
34 document,
35 !document.owned_paths.is_empty(),
36 None,
37 "missing owned paths",
38 );
39 require(
40 &mut diagnostics,
41 document,
42 !document.tasks.is_empty(),
43 None,
44 "missing task checklist items",
45 );
46 require(
47 &mut diagnostics,
48 document,
49 document
50 .tasks
51 .iter()
52 .any(|task| !task.run_blocks.is_empty()),
53 None,
54 "missing Run block",
55 );
56 require(
57 &mut diagnostics,
58 document,
59 !document.acceptance.is_empty(),
60 None,
61 "missing acceptance checklist",
62 );
63
64 let mut seen = HashSet::new();
65 for task in &document.tasks {
66 if !seen.insert(task.id.clone()) {
67 diagnostics.push(Diagnostic {
68 path: document.path.clone(),
69 line: Some(task.line),
70 severity: DiagnosticSeverity::Error,
71 message: format!("duplicate task id: {}", task.id),
72 });
73 }
74 }
75
76 ValidationResult {
77 document_id: document.id.clone(),
78 diagnostics,
79 }
80}
81
82fn require(
83 diagnostics: &mut Vec<Diagnostic>,
84 document: &Document,
85 ok: bool,
86 line: Option<usize>,
87 message: &str,
88) {
89 if !ok {
90 diagnostics.push(Diagnostic {
91 path: document.path.clone(),
92 line,
93 severity: DiagnosticSeverity::Error,
94 message: message.to_string(),
95 });
96 }
97}