dada_lang/main_lib/test/
spec_validation.rs1use std::collections::HashSet;
2use std::fs;
3use std::path::Path;
4
5use dada_util::{Fallible, bail};
6use regex::Regex;
7
8pub struct SpecValidator {
10 valid_spec_ids: HashSet<String>,
12}
13
14impl SpecValidator {
15 pub fn new() -> Fallible<Self> {
17 let mut validator = SpecValidator {
18 valid_spec_ids: HashSet::new(),
19 };
20 validator.load_spec_ids()?;
21 Ok(validator)
22 }
23
24 fn load_spec_ids(&mut self) -> Fallible<()> {
26 let spec_src_path = Path::new("spec/src");
27
28 if !spec_src_path.exists() {
29 bail!(
30 "Spec source directory not found at {}. Make sure you're running from the dada project root.",
31 spec_src_path.display()
32 );
33 }
34
35 self.scan_directory(spec_src_path)?;
36 Ok(())
37 }
38
39 fn scan_directory(&mut self, dir: &Path) -> Fallible<()> {
41 for entry in fs::read_dir(dir)? {
42 let entry = entry?;
43 let path = entry.path();
44
45 if path.is_dir() {
46 self.scan_directory(&path)?;
47 } else if let Some(extension) = path.extension()
48 && extension == "md"
49 {
50 self.extract_spec_ids_from_file(&path)?;
51 }
52 }
53 Ok(())
54 }
55
56 fn extract_spec_ids_from_file(&mut self, file_path: &Path) -> Fallible<()> {
62 let content = fs::read_to_string(file_path)?;
63
64 let spec_src = Path::new("spec/src");
65 let relative_path = file_path.strip_prefix(spec_src).unwrap_or(file_path);
66 let file_prefix = dada_spec_common::file_path_to_prefix(relative_path);
67
68 let directive_start = Regex::new(r"^:::\{spec\}(.*)$")?;
69 let directive_end = Regex::new(r"^:::$")?;
70 let inline_re = Regex::new(r"\{spec\}`([^`]+)`")?;
71
72 let mut heading_tracker = dada_spec_common::HeadingTracker::new();
73 let mut in_directive = false;
74 let mut current_parent_id = String::new();
75
76 for line in content.lines() {
77 let trimmed = line.trim();
78
79 if !in_directive {
80 heading_tracker.process_line(trimmed);
81
82 if let Some(captures) = directive_start.captures(trimmed) {
83 let rest = captures.get(1).map(|m| m.as_str()).unwrap_or("");
84 let (local_name, _tags) = dada_spec_common::parse_spec_tokens(rest);
85
86 let full_id = dada_spec_common::resolve_spec_id(
87 &file_prefix,
88 &heading_tracker.current_segments(),
89 local_name.as_deref().unwrap_or(""),
90 );
91 self.valid_spec_ids.insert(full_id.clone());
92 current_parent_id = full_id;
93 in_directive = true;
94 }
95 } else if directive_end.is_match(trimmed) {
96 in_directive = false;
97 current_parent_id.clear();
98 } else {
99 for cap in inline_re.captures_iter(trimmed) {
103 if let Some(content) = cap.get(1) {
104 let (name, _tags) = dada_spec_common::parse_spec_tokens(content.as_str());
105 let name = name.unwrap_or_else(|| content.as_str().to_string());
107 let sub_id = format!("{}.{}", current_parent_id, name);
108 self.valid_spec_ids.insert(sub_id);
109 }
110 }
111 }
112 }
113
114 Ok(())
115 }
116
117 pub fn validate_spec_refs(&self, spec_refs: &[String]) -> Vec<String> {
119 spec_refs
120 .iter()
121 .filter(|spec_ref| !self.valid_spec_ids.contains(*spec_ref))
122 .cloned()
123 .collect()
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn test_spec_validation() {
133 let mut validator = SpecValidator {
135 valid_spec_ids: HashSet::new(),
136 };
137 validator
138 .valid_spec_ids
139 .insert("syntax.string-literals.delimiters.quoted".to_string());
140 validator
141 .valid_spec_ids
142 .insert("permissions.lease.transfer".to_string());
143
144 let refs = vec![
146 "syntax.string-literals.delimiters.quoted".to_string(),
147 "invalid.spec.ref".to_string(),
148 "permissions.lease.transfer".to_string(),
149 ];
150 let invalid_refs = validator.validate_spec_refs(&refs);
151 assert_eq!(invalid_refs, vec!["invalid.spec.ref"]);
152 }
153}