1use crate::error::{CleanroomError, Result};
9use crate::validation::span_validator::SpanData;
10use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, HashSet};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct GraphExpectation {
16 pub must_include: Vec<(String, String)>,
18
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub must_not_cross: Option<Vec<(String, String)>>,
22
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub acyclic: Option<bool>,
26}
27
28impl GraphExpectation {
29 pub fn new(must_include: Vec<(String, String)>) -> Self {
31 Self {
32 must_include,
33 must_not_cross: None,
34 acyclic: None,
35 }
36 }
37
38 pub fn with_must_not_cross(mut self, must_not_cross: Vec<(String, String)>) -> Self {
40 self.must_not_cross = Some(must_not_cross);
41 self
42 }
43
44 pub fn with_acyclic(mut self, acyclic: bool) -> Self {
46 self.acyclic = Some(acyclic);
47 self
48 }
49
50 pub fn validate(&self, spans: &[SpanData]) -> Result<()> {
63 let validator = GraphValidator::new(spans);
64
65 for (parent_name, child_name) in &self.must_include {
67 validator.validate_edge_exists(parent_name, child_name)?;
68 }
69
70 if let Some(ref forbidden_edges) = self.must_not_cross {
72 for (parent_name, child_name) in forbidden_edges {
73 validator.validate_edge_not_exists(parent_name, child_name)?;
74 }
75 }
76
77 if let Some(true) = self.acyclic {
79 validator.validate_acyclic()?;
80 }
81
82 Ok(())
83 }
84}
85
86pub struct GraphValidator<'a> {
88 spans: &'a [SpanData],
90
91 span_by_id: HashMap<String, &'a SpanData>,
93
94 spans_by_name: HashMap<String, Vec<&'a SpanData>>,
96}
97
98impl<'a> GraphValidator<'a> {
99 pub fn new(spans: &'a [SpanData]) -> Self {
101 let mut span_by_id = HashMap::new();
102 let mut spans_by_name: HashMap<String, Vec<&SpanData>> = HashMap::new();
103
104 for span in spans {
105 span_by_id.insert(span.span_id.clone(), span);
106 spans_by_name
107 .entry(span.name.clone())
108 .or_default()
109 .push(span);
110 }
111
112 Self {
113 spans,
114 span_by_id,
115 spans_by_name,
116 }
117 }
118
119 pub fn validate_edge_exists(&self, parent_name: &str, child_name: &str) -> Result<()> {
133 let parent_spans = self.spans_by_name.get(parent_name).ok_or_else(|| {
134 CleanroomError::validation_error(format!(
135 "Graph validation failed: parent span '{}' not found",
136 parent_name
137 ))
138 })?;
139
140 let child_spans = self.spans_by_name.get(child_name).ok_or_else(|| {
141 CleanroomError::validation_error(format!(
142 "Graph validation failed: child span '{}' not found",
143 child_name
144 ))
145 })?;
146
147 let edge_exists = child_spans.iter().any(|child| {
149 if let Some(ref parent_id) = child.parent_span_id {
150 parent_spans
151 .iter()
152 .any(|parent| &parent.span_id == parent_id)
153 } else {
154 false
155 }
156 });
157
158 if !edge_exists {
159 return Err(CleanroomError::validation_error(format!(
160 "Graph validation failed: required edge '{}' -> '{}' not found",
161 parent_name, child_name
162 )));
163 }
164
165 Ok(())
166 }
167
168 pub fn validate_edge_not_exists(&self, parent_name: &str, child_name: &str) -> Result<()> {
180 let Some(parent_spans) = self.spans_by_name.get(parent_name) else {
182 return Ok(());
183 };
184
185 let Some(child_spans) = self.spans_by_name.get(child_name) else {
186 return Ok(());
187 };
188
189 let edge_exists = child_spans.iter().any(|child| {
191 if let Some(ref parent_id) = child.parent_span_id {
192 parent_spans
193 .iter()
194 .any(|parent| &parent.span_id == parent_id)
195 } else {
196 false
197 }
198 });
199
200 if edge_exists {
201 return Err(CleanroomError::validation_error(format!(
202 "Graph validation failed: forbidden edge '{}' -> '{}' found",
203 parent_name, child_name
204 )));
205 }
206
207 Ok(())
208 }
209
210 pub fn validate_acyclic(&self) -> Result<()> {
220 let mut visited = HashSet::new();
222 let mut in_path = HashSet::new();
223
224 for span in self.spans {
226 if !visited.contains(&span.span_id) {
227 if let Some(cycle_path) =
228 self.detect_cycle_dfs(span, &mut visited, &mut in_path, &mut Vec::new())
229 {
230 return Err(CleanroomError::validation_error(format!(
231 "Graph validation failed: cycle detected in span graph: {}",
232 cycle_path.join(" -> ")
233 )));
234 }
235 }
236 }
237
238 Ok(())
239 }
240
241 fn detect_cycle_dfs(
243 &self,
244 span: &SpanData,
245 visited: &mut HashSet<String>,
246 in_path: &mut HashSet<String>,
247 path: &mut Vec<String>,
248 ) -> Option<Vec<String>> {
249 visited.insert(span.span_id.clone());
250 in_path.insert(span.span_id.clone());
251 path.push(span.name.clone());
252
253 if let Some(ref parent_id) = span.parent_span_id {
255 if let Some(parent) = self.span_by_id.get(parent_id) {
256 if in_path.contains(parent_id) {
257 path.push(parent.name.clone());
259 return Some(path.clone());
260 }
261
262 if !visited.contains(parent_id) {
263 if let Some(cycle) = self.detect_cycle_dfs(parent, visited, in_path, path) {
264 return Some(cycle);
265 }
266 }
267 }
268 }
269
270 in_path.remove(&span.span_id);
271 path.pop();
272 None
273 }
274
275 pub fn get_all_edges(&self) -> Vec<(String, String)> {
277 let mut edges = Vec::new();
278
279 for child in self.spans {
280 if let Some(ref parent_id) = child.parent_span_id {
281 if let Some(parent) = self.span_by_id.get(parent_id) {
282 edges.push((parent.name.clone(), child.name.clone()));
283 }
284 }
285 }
286
287 edges
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use std::collections::HashMap;
295
296 fn create_span(name: &str, span_id: &str, parent_id: Option<&str>) -> SpanData {
297 SpanData {
298 name: name.to_string(),
299 span_id: span_id.to_string(),
300 parent_span_id: parent_id.map(|s| s.to_string()),
301 trace_id: "trace123".to_string(),
302 attributes: HashMap::new(),
303 start_time_unix_nano: Some(1000000),
304 end_time_unix_nano: Some(2000000),
305 kind: None,
306 events: None,
307 resource_attributes: HashMap::new(),
308 }
309 }
310
311 #[test]
312 fn test_graph_validator_edge_exists_valid() {
313 let spans = vec![
315 create_span("parent", "span1", None),
316 create_span("child", "span2", Some("span1")),
317 ];
318 let validator = GraphValidator::new(&spans);
319
320 let result = validator.validate_edge_exists("parent", "child");
322
323 assert!(result.is_ok());
325 }
326
327 #[test]
328 fn test_graph_validator_edge_exists_missing() {
329 let spans = vec![
331 create_span("parent", "span1", None),
332 create_span("child", "span2", None),
333 ];
334 let validator = GraphValidator::new(&spans);
335
336 let result = validator.validate_edge_exists("parent", "child");
338
339 assert!(result.is_err());
341 let err_msg = result.unwrap_err().to_string();
342 assert!(err_msg.contains("required edge"));
343 assert!(err_msg.contains("parent"));
344 assert!(err_msg.contains("child"));
345 }
346
347 #[test]
348 fn test_graph_validator_edge_exists_parent_not_found() {
349 let spans = vec![create_span("child", "span2", None)];
351 let validator = GraphValidator::new(&spans);
352
353 let result = validator.validate_edge_exists("parent", "child");
355
356 assert!(result.is_err());
358 let err_msg = result.unwrap_err().to_string();
359 assert!(err_msg.contains("parent span"));
360 assert!(err_msg.contains("not found"));
361 }
362
363 #[test]
364 fn test_graph_validator_edge_exists_child_not_found() {
365 let spans = vec![create_span("parent", "span1", None)];
367 let validator = GraphValidator::new(&spans);
368
369 let result = validator.validate_edge_exists("parent", "child");
371
372 assert!(result.is_err());
374 let err_msg = result.unwrap_err().to_string();
375 assert!(err_msg.contains("child span"));
376 assert!(err_msg.contains("not found"));
377 }
378
379 #[test]
380 fn test_graph_validator_edge_not_exists_valid() {
381 let spans = vec![
383 create_span("parent", "span1", None),
384 create_span("child", "span2", None),
385 ];
386 let validator = GraphValidator::new(&spans);
387
388 let result = validator.validate_edge_not_exists("parent", "child");
390
391 assert!(result.is_ok());
393 }
394
395 #[test]
396 fn test_graph_validator_edge_not_exists_fails_when_edge_present() {
397 let spans = vec![
399 create_span("parent", "span1", None),
400 create_span("child", "span2", Some("span1")),
401 ];
402 let validator = GraphValidator::new(&spans);
403
404 let result = validator.validate_edge_not_exists("parent", "child");
406
407 assert!(result.is_err());
409 let err_msg = result.unwrap_err().to_string();
410 assert!(err_msg.contains("forbidden edge"));
411 assert!(err_msg.contains("parent"));
412 assert!(err_msg.contains("child"));
413 }
414
415 #[test]
416 fn test_graph_validator_edge_not_exists_valid_when_parent_missing() {
417 let spans = vec![create_span("child", "span2", None)];
419 let validator = GraphValidator::new(&spans);
420
421 let result = validator.validate_edge_not_exists("parent", "child");
423
424 assert!(result.is_ok());
426 }
427
428 #[test]
429 fn test_graph_validator_acyclic_valid_linear_chain() {
430 let spans = vec![
432 create_span("root", "span1", None),
433 create_span("a", "span2", Some("span1")),
434 create_span("b", "span3", Some("span2")),
435 ];
436 let validator = GraphValidator::new(&spans);
437
438 let result = validator.validate_acyclic();
440
441 assert!(result.is_ok());
443 }
444
445 #[test]
446 fn test_graph_validator_acyclic_valid_tree() {
447 let spans = vec![
449 create_span("root", "span1", None),
450 create_span("a", "span2", Some("span1")),
451 create_span("b", "span3", Some("span1")),
452 create_span("c", "span4", Some("span2")),
453 ];
454 let validator = GraphValidator::new(&spans);
455
456 let result = validator.validate_acyclic();
458
459 assert!(result.is_ok());
461 }
462
463 #[test]
464 fn test_graph_validator_acyclic_detects_self_loop() {
465 let spans = vec![create_span("a", "span1", Some("span1"))];
467 let validator = GraphValidator::new(&spans);
468
469 let result = validator.validate_acyclic();
471
472 assert!(result.is_err());
474 let err_msg = result.unwrap_err().to_string();
475 assert!(err_msg.contains("cycle detected"));
476 }
477
478 #[test]
479 fn test_graph_validator_acyclic_valid_multiple_roots() {
480 let spans = vec![
482 create_span("root1", "span1", None),
483 create_span("a", "span2", Some("span1")),
484 create_span("root2", "span3", None),
485 create_span("b", "span4", Some("span3")),
486 ];
487 let validator = GraphValidator::new(&spans);
488
489 let result = validator.validate_acyclic();
491
492 assert!(result.is_ok());
494 }
495
496 #[test]
497 fn test_graph_expectation_validate_must_include() {
498 let spans = vec![
500 create_span("parent", "span1", None),
501 create_span("child", "span2", Some("span1")),
502 ];
503 let expectation = GraphExpectation::new(vec![("parent".to_string(), "child".to_string())]);
504
505 let result = expectation.validate(&spans);
507
508 assert!(result.is_ok());
510 }
511
512 #[test]
513 fn test_graph_expectation_validate_must_include_fails() {
514 let spans = vec![
516 create_span("parent", "span1", None),
517 create_span("child", "span2", None),
518 ];
519 let expectation = GraphExpectation::new(vec![("parent".to_string(), "child".to_string())]);
520
521 let result = expectation.validate(&spans);
523
524 assert!(result.is_err());
526 }
527
528 #[test]
529 fn test_graph_expectation_validate_must_not_cross() {
530 let spans = vec![
532 create_span("a", "span1", None),
533 create_span("b", "span2", None),
534 ];
535 let expectation = GraphExpectation::new(vec![])
536 .with_must_not_cross(vec![("a".to_string(), "b".to_string())]);
537
538 let result = expectation.validate(&spans);
540
541 assert!(result.is_ok());
543 }
544
545 #[test]
546 fn test_graph_expectation_validate_must_not_cross_fails() {
547 let spans = vec![
549 create_span("a", "span1", None),
550 create_span("b", "span2", Some("span1")),
551 ];
552 let expectation = GraphExpectation::new(vec![])
553 .with_must_not_cross(vec![("a".to_string(), "b".to_string())]);
554
555 let result = expectation.validate(&spans);
557
558 assert!(result.is_err());
560 }
561
562 #[test]
563 fn test_graph_expectation_validate_acyclic() {
564 let spans = vec![
566 create_span("root", "span1", None),
567 create_span("a", "span2", Some("span1")),
568 create_span("b", "span3", Some("span2")),
569 ];
570 let expectation = GraphExpectation::new(vec![]).with_acyclic(true);
571
572 let result = expectation.validate(&spans);
574
575 assert!(result.is_ok());
577 }
578
579 #[test]
580 fn test_graph_expectation_validate_combined_requirements() {
581 let spans = vec![
583 create_span("parent", "span1", None),
584 create_span("child1", "span2", Some("span1")),
585 create_span("child2", "span3", Some("span1")),
586 create_span("grandchild", "span4", Some("span2")),
587 ];
588 let expectation = GraphExpectation::new(vec![
589 ("parent".to_string(), "child1".to_string()),
590 ("parent".to_string(), "child2".to_string()),
591 ])
592 .with_must_not_cross(vec![("child1".to_string(), "child2".to_string())])
593 .with_acyclic(true);
594
595 let result = expectation.validate(&spans);
597
598 assert!(result.is_ok());
600 }
601
602 #[test]
603 fn test_graph_expectation_multiple_spans_same_name() {
604 let spans = vec![
606 create_span("http.request", "span1", None),
607 create_span("http.request", "span2", None),
608 create_span("db.query", "span3", Some("span1")),
609 create_span("db.query", "span4", Some("span2")),
610 ];
611 let expectation =
612 GraphExpectation::new(vec![("http.request".to_string(), "db.query".to_string())]);
613
614 let result = expectation.validate(&spans);
616
617 assert!(result.is_ok());
619 }
620
621 #[test]
622 fn test_graph_validator_get_all_edges() {
623 let spans = vec![
625 create_span("root", "span1", None),
626 create_span("a", "span2", Some("span1")),
627 create_span("b", "span3", Some("span1")),
628 create_span("c", "span4", Some("span2")),
629 ];
630 let validator = GraphValidator::new(&spans);
631
632 let edges = validator.get_all_edges();
634
635 assert_eq!(edges.len(), 3);
637 assert!(edges.contains(&("root".to_string(), "a".to_string())));
638 assert!(edges.contains(&("root".to_string(), "b".to_string())));
639 assert!(edges.contains(&("a".to_string(), "c".to_string())));
640 }
641}