1use std::path::Path;
2
3use clayers_xml::query::{QueryMode as XmlQueryMode, QueryResult as XmlQueryResult};
4
5#[derive(Debug)]
7pub enum QueryResult {
8 Count(usize),
10 Text(Vec<String>),
12 Xml(Vec<String>),
14}
15
16pub fn execute_query(
25 spec_dir: &Path,
26 xpath_expr: &str,
27 mode: QueryMode,
28) -> Result<QueryResult, crate::Error> {
29 let index_files = crate::discovery::find_index_files(spec_dir)?;
30 if index_files.is_empty() {
31 return Err(crate::Error::Discovery("no specs found".into()));
32 }
33
34 let mut all_file_paths = Vec::new();
35 for index_path in &index_files {
36 let file_paths = crate::discovery::discover_spec_files(index_path)?;
37 all_file_paths.extend(file_paths);
38 }
39
40 let combined_xml = crate::assembly::assemble_combined_string(&all_file_paths)?;
41
42 let xml_mode = match mode {
43 QueryMode::Count => XmlQueryMode::Count,
44 QueryMode::Text => XmlQueryMode::Text,
45 QueryMode::Xml => XmlQueryMode::Xml,
46 };
47
48 let result = clayers_xml::query::evaluate_xpath(
49 &combined_xml,
50 xpath_expr,
51 xml_mode,
52 &[],
53 )?;
54
55 Ok(match result {
56 XmlQueryResult::Count(n) => QueryResult::Count(n),
57 XmlQueryResult::Text(t) => QueryResult::Text(t),
58 XmlQueryResult::Xml(x) => QueryResult::Xml(x),
59 })
60}
61
62#[derive(Debug, Clone, Copy)]
64pub enum QueryMode {
65 Count,
66 Text,
67 Xml,
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use crate::namespace;
74 use std::path::PathBuf;
75
76 fn spec_dir() -> PathBuf {
77 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
78 .join("../../clayers/clayers")
79 .canonicalize()
80 .expect("clayers/clayers/ not found")
81 }
82
83 #[test]
84 fn query_count_terms() {
85 let result =
86 execute_query(&spec_dir(), "//trm:term", QueryMode::Count).expect("query failed");
87 if let QueryResult::Count(count) = result {
88 assert!(count >= 15, "expected 15+ terms, got {count}");
89 } else {
90 panic!("expected Count result");
91 }
92 }
93
94 #[test]
95 fn query_count_depends_on_relations() {
96 let result = execute_query(
97 &spec_dir(),
98 "//rel:relation[@type=\"depends-on\"]",
99 QueryMode::Count,
100 )
101 .expect("query failed");
102 if let QueryResult::Count(count) = result {
103 assert!(count >= 20, "expected 20+ depends-on, got {count}");
104 } else {
105 panic!("expected Count result");
106 }
107 }
108
109 #[test]
110 fn query_text_term_definition() {
111 let result = execute_query(
112 &spec_dir(),
113 "//trm:term[@id=\"term-layer\"]/trm:definition",
114 QueryMode::Text,
115 )
116 .expect("query failed");
117 if let QueryResult::Text(texts) = result {
118 assert!(!texts.is_empty(), "should find term-layer definition");
119 let text = &texts[0];
120 assert!(
121 text.len() > 10,
122 "definition should have meaningful text: {text}"
123 );
124 assert!(!text.contains("<trm:"), "text should not contain XML tags");
125 } else {
126 panic!("expected Text result");
127 }
128 }
129
130 #[test]
131 fn query_xml_output() {
132 let result = execute_query(
133 &spec_dir(),
134 "//trm:term[@id=\"term-layer\"]",
135 QueryMode::Xml,
136 )
137 .expect("query failed");
138 if let QueryResult::Xml(xmls) = result {
139 assert!(!xmls.is_empty(), "should find term-layer");
140 let xml = &xmls[0];
141 assert!(xml.contains('<'), "should contain XML");
142 let old_urn = ["living", "spec"].concat();
143 assert!(!xml.contains(&old_urn), "should not contain old URN");
144 } else {
145 panic!("expected Xml result");
146 }
147 }
148
149 #[test]
150 fn all_namespace_prefixes_resolve() {
151 let prefixes = [
152 "pr", "trm", "org", "rel", "art", "llm", "rev", "spec", "cmb", "idx", "dec", "src",
153 "pln",
154 ];
155 for prefix in prefixes {
156 assert!(
157 namespace::uri_for(prefix).is_some(),
158 "prefix {prefix} should resolve to a URI"
159 );
160 }
161 }
162
163 #[test]
166 fn query_absolute_path_terms() {
167 let result = execute_query(
169 &spec_dir(),
170 "/cmb:spec/trm:term/trm:name",
171 QueryMode::Text,
172 )
173 .expect("absolute path query failed");
174 if let QueryResult::Text(texts) = result {
175 assert!(!texts.is_empty(), "should find term names via absolute path");
176 } else {
177 panic!("expected Text result");
178 }
179 }
180
181 #[test]
182 fn query_count_function() {
183 let result = execute_query(
184 &spec_dir(),
185 "count(//trm:term)",
186 QueryMode::Text,
187 )
188 .expect("count() query failed");
189 if let QueryResult::Text(texts) = result {
190 assert_eq!(texts.len(), 1, "count() should return one value");
191 let count: f64 = texts[0].parse().expect("count should be numeric");
192 assert!(count >= 15.0, "expected 15+ terms, got {count}");
193 } else {
194 panic!("expected Text result");
195 }
196 }
197
198 #[test]
199 fn query_starts_with_predicate() {
200 let result = execute_query(
201 &spec_dir(),
202 "//trm:term[starts-with(@id, 'term-')]",
203 QueryMode::Count,
204 )
205 .expect("starts-with query failed");
206 if let QueryResult::Count(count) = result {
207 assert!(count >= 15, "expected 15+ terms with 'term-' prefix, got {count}");
208 } else {
209 panic!("expected Count result");
210 }
211 }
212
213 #[test]
214 fn query_string_length_function() {
215 let result = execute_query(
216 &spec_dir(),
217 "string-length(//trm:term[@id='term-layer']/trm:name)",
218 QueryMode::Text,
219 )
220 .expect("string-length query failed");
221 if let QueryResult::Text(texts) = result {
222 assert_eq!(texts.len(), 1, "should return one value");
223 let len: f64 = texts[0].parse().expect("should be numeric");
224 assert!(len > 0.0, "term name should have non-zero length");
225 } else {
226 panic!("expected Text result");
227 }
228 }
229
230 #[test]
231 fn query_positional_predicate() {
232 let result = execute_query(
233 &spec_dir(),
234 "(//trm:term)[1]/trm:name",
235 QueryMode::Text,
236 )
237 .expect("positional predicate query failed");
238 if let QueryResult::Text(texts) = result {
239 assert_eq!(texts.len(), 1, "should return exactly 1 term name");
240 } else {
241 panic!("expected Text result");
242 }
243 }
244}