1use validator::Validate;
6
7use crate::error::{PhyTraceError, PhyTraceResult};
8use crate::models::event::UdmEvent;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ValidationLevel {
13 None,
15 Basic,
17 Full,
19}
20
21#[derive(Debug, Clone)]
23pub struct Validator {
24 level: ValidationLevel,
25}
26
27impl Default for Validator {
28 fn default() -> Self {
29 Self {
30 level: ValidationLevel::Basic,
31 }
32 }
33}
34
35impl Validator {
36 pub fn new(level: ValidationLevel) -> Self {
38 Self { level }
39 }
40
41 pub fn validate(&self, event: &UdmEvent) -> PhyTraceResult<()> {
43 match self.level {
44 ValidationLevel::None => Ok(()),
45 ValidationLevel::Basic => self.validate_basic(event),
46 ValidationLevel::Full => self.validate_full(event),
47 }
48 }
49
50 fn validate_basic(&self, event: &UdmEvent) -> PhyTraceResult<()> {
52 if event.event_id.is_empty() {
54 return Err(PhyTraceError::Validation(
55 "event_id is required".to_string(),
56 ));
57 }
58
59 if event.udm_version.is_empty() {
60 return Err(PhyTraceError::Validation(
61 "udm_version is required".to_string(),
62 ));
63 }
64
65 if event.sdk_version.is_empty() {
66 return Err(PhyTraceError::Validation(
67 "sdk_version is required".to_string(),
68 ));
69 }
70
71 Ok(())
72 }
73
74 fn validate_full(&self, event: &UdmEvent) -> PhyTraceResult<()> {
76 self.validate_basic(event)?;
78
79 event
81 .validate()
82 .map_err(|e| PhyTraceError::Validation(format!("Event validation failed: {}", e)))?;
83
84 if let Some(ref identity) = event.identity {
86 self.validate_identity(identity)?;
87 }
88
89 if let Some(ref location) = event.location {
90 self.validate_location(location)?;
91 }
92
93 if let Some(ref power) = event.power {
94 self.validate_power(power)?;
95 }
96
97 if let Some(ref safety) = event.safety {
98 self.validate_safety(safety)?;
99 }
100
101 Ok(())
102 }
103
104 fn validate_identity(
106 &self,
107 identity: &crate::models::domains::IdentityDomain,
108 ) -> PhyTraceResult<()> {
109 if let Some(ref source_id) = identity.source_id {
111 if source_id.is_empty() {
112 return Err(PhyTraceError::Validation(
113 "identity.source_id cannot be empty".to_string(),
114 ));
115 }
116 }
117 Ok(())
118 }
119
120 fn validate_location(
122 &self,
123 location: &crate::models::domains::LocationDomain,
124 ) -> PhyTraceResult<()> {
125 if let Some(lat) = location.latitude {
127 if !(-90.0..=90.0).contains(&lat) {
128 return Err(PhyTraceError::Validation(format!(
129 "latitude must be between -90 and 90, got {}",
130 lat
131 )));
132 }
133 }
134
135 if let Some(lon) = location.longitude {
136 if !(-180.0..=180.0).contains(&lon) {
137 return Err(PhyTraceError::Validation(format!(
138 "longitude must be between -180 and 180, got {}",
139 lon
140 )));
141 }
142 }
143
144 if let Some(heading) = location.heading_deg {
146 if !(0.0..=360.0).contains(&heading) && !(-180.0..=180.0).contains(&heading) {
147 return Err(PhyTraceError::Validation(format!(
148 "heading_deg should be in range [0, 360] or [-180, 180], got {}",
149 heading
150 )));
151 }
152 }
153
154 Ok(())
155 }
156
157 fn validate_power(&self, power: &crate::models::domains::PowerDomain) -> PhyTraceResult<()> {
159 if let Some(ref battery) = power.battery {
160 if let Some(soc) = battery.state_of_charge_pct {
162 if !(0.0..=100.0).contains(&soc) {
163 return Err(PhyTraceError::Validation(format!(
164 "battery.state_of_charge_pct must be between 0 and 100, got {}",
165 soc
166 )));
167 }
168 }
169
170 if let Some(health) = battery.state_of_health_pct {
172 if !(0.0..=100.0).contains(&health) {
173 return Err(PhyTraceError::Validation(format!(
174 "battery.state_of_health_pct must be between 0 and 100, got {}",
175 health
176 )));
177 }
178 }
179
180 if let Some(voltage) = battery.voltage_v {
182 if voltage < 0.0 {
183 return Err(PhyTraceError::Validation(format!(
184 "battery.voltage_v must be non-negative, got {}",
185 voltage
186 )));
187 }
188 }
189 }
190
191 Ok(())
192 }
193
194 fn validate_safety(&self, safety: &crate::models::domains::SafetyDomain) -> PhyTraceResult<()> {
196 if let Some(ref proximity) = safety.proximity {
198 if let Some(closest) = proximity.closest_distance_m {
199 if closest < 0.0 {
200 return Err(PhyTraceError::Validation(format!(
201 "safety.proximity.closest_distance_m must be non-negative, got {}",
202 closest
203 )));
204 }
205 }
206 }
207
208 Ok(())
209 }
210}
211
212pub fn validate_event(event: &UdmEvent) -> PhyTraceResult<()> {
214 Validator::default().validate(event)
215}
216
217pub fn validate_event_full(event: &UdmEvent) -> PhyTraceResult<()> {
219 Validator::new(ValidationLevel::Full).validate(event)
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::models::domains::power::Battery;
226 use crate::models::domains::safety::ProximityInfo;
227 use crate::models::domains::{IdentityDomain, LocationDomain, PowerDomain, SafetyDomain};
228 use crate::models::enums::SourceType;
229
230 fn valid_event() -> UdmEvent {
231 UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain {
232 source_id: Some("robot-001".to_string()),
233 ..Default::default()
234 })
235 }
236
237 #[test]
238 fn test_basic_validation_passes() {
239 let event = valid_event();
240 let validator = Validator::new(ValidationLevel::Basic);
241 assert!(validator.validate(&event).is_ok());
242 }
243
244 #[test]
245 fn test_full_validation_passes() {
246 let event = valid_event();
247 let validator = Validator::new(ValidationLevel::Full);
248 assert!(validator.validate(&event).is_ok());
249 }
250
251 #[test]
252 fn test_empty_event_id_fails() {
253 let mut event = valid_event();
254 event.event_id = String::new();
255
256 let validator = Validator::new(ValidationLevel::Basic);
257 assert!(validator.validate(&event).is_err());
258 }
259
260 #[test]
261 fn test_invalid_latitude_fails() {
262 let event = valid_event().with_location(LocationDomain {
263 latitude: Some(100.0), ..Default::default()
265 });
266
267 let validator = Validator::new(ValidationLevel::Full);
268 assert!(validator.validate(&event).is_err());
269 }
270
271 #[test]
272 fn test_invalid_soc_fails() {
273 let event = valid_event().with_power(PowerDomain {
274 battery: Some(Battery {
275 state_of_charge_pct: Some(150.0), ..Default::default()
277 }),
278 ..Default::default()
279 });
280
281 let validator = Validator::new(ValidationLevel::Full);
282 assert!(validator.validate(&event).is_err());
283 }
284
285 #[test]
286 fn test_validation_none_always_passes() {
287 let mut event = valid_event();
288 event.event_id = String::new(); let validator = Validator::new(ValidationLevel::None);
291 assert!(validator.validate(&event).is_ok());
292 }
293
294 #[test]
297 fn test_default_validator() {
298 let validator = Validator::default();
299 let event = valid_event();
300 assert!(validator.validate(&event).is_ok());
301 }
302
303 #[test]
304 fn test_convenience_validate_event() {
305 let event = valid_event();
306 assert!(validate_event(&event).is_ok());
307 }
308
309 #[test]
310 fn test_convenience_validate_event_full() {
311 let event = valid_event();
312 assert!(validate_event_full(&event).is_ok());
313 }
314
315 #[test]
316 fn test_empty_udm_version_fails() {
317 let mut event = valid_event();
318 event.udm_version = String::new();
319
320 let validator = Validator::new(ValidationLevel::Basic);
321 let result = validator.validate(&event);
322 assert!(result.is_err());
323 assert!(result.unwrap_err().to_string().contains("udm_version"));
324 }
325
326 #[test]
327 fn test_empty_sdk_version_fails() {
328 let mut event = valid_event();
329 event.sdk_version = String::new();
330
331 let validator = Validator::new(ValidationLevel::Basic);
332 let result = validator.validate(&event);
333 assert!(result.is_err());
334 assert!(result.unwrap_err().to_string().contains("sdk_version"));
335 }
336
337 #[test]
338 fn test_empty_identity_source_id_fails() {
339 let event = valid_event().with_identity(IdentityDomain {
340 source_id: Some(String::new()), ..Default::default()
342 });
343
344 let validator = Validator::new(ValidationLevel::Full);
345 let result = validator.validate(&event);
346 assert!(result.is_err());
347 assert!(result
348 .unwrap_err()
349 .to_string()
350 .contains("identity.source_id"));
351 }
352
353 #[test]
354 fn test_identity_without_source_id_passes() {
355 let event = valid_event().with_identity(IdentityDomain {
357 source_id: None,
358 ..Default::default()
359 });
360
361 let validator = Validator::new(ValidationLevel::Full);
362 assert!(validator.validate(&event).is_ok());
363 }
364
365 #[test]
366 fn test_invalid_negative_latitude_fails() {
367 let event = valid_event().with_location(LocationDomain {
368 latitude: Some(-100.0), ..Default::default()
370 });
371
372 let validator = Validator::new(ValidationLevel::Full);
373 let result = validator.validate(&event);
374 assert!(result.is_err());
375 assert!(result.unwrap_err().to_string().contains("latitude"));
376 }
377
378 #[test]
379 fn test_invalid_longitude_too_high_fails() {
380 let event = valid_event().with_location(LocationDomain {
381 longitude: Some(200.0), ..Default::default()
383 });
384
385 let validator = Validator::new(ValidationLevel::Full);
386 let result = validator.validate(&event);
387 assert!(result.is_err());
388 assert!(result.unwrap_err().to_string().contains("longitude"));
389 }
390
391 #[test]
392 fn test_invalid_longitude_too_low_fails() {
393 let event = valid_event().with_location(LocationDomain {
394 longitude: Some(-200.0), ..Default::default()
396 });
397
398 let validator = Validator::new(ValidationLevel::Full);
399 let result = validator.validate(&event);
400 assert!(result.is_err());
401 assert!(result.unwrap_err().to_string().contains("longitude"));
402 }
403
404 #[test]
405 fn test_invalid_heading_fails() {
406 let event = valid_event().with_location(LocationDomain {
407 heading_deg: Some(400.0), ..Default::default()
409 });
410
411 let validator = Validator::new(ValidationLevel::Full);
412 let result = validator.validate(&event);
413 assert!(result.is_err());
414 assert!(result.unwrap_err().to_string().contains("heading_deg"));
415 }
416
417 #[test]
418 fn test_valid_heading_0_360_passes() {
419 let event = valid_event().with_location(LocationDomain {
420 heading_deg: Some(270.0), ..Default::default()
422 });
423
424 let validator = Validator::new(ValidationLevel::Full);
425 assert!(validator.validate(&event).is_ok());
426 }
427
428 #[test]
429 fn test_valid_heading_negative_passes() {
430 let event = valid_event().with_location(LocationDomain {
431 heading_deg: Some(-90.0), ..Default::default()
433 });
434
435 let validator = Validator::new(ValidationLevel::Full);
436 assert!(validator.validate(&event).is_ok());
437 }
438
439 #[test]
440 fn test_valid_location_with_all_fields() {
441 let event = valid_event().with_location(LocationDomain {
442 latitude: Some(45.0),
443 longitude: Some(-122.0),
444 heading_deg: Some(180.0),
445 ..Default::default()
446 });
447
448 let validator = Validator::new(ValidationLevel::Full);
449 assert!(validator.validate(&event).is_ok());
450 }
451
452 #[test]
453 fn test_invalid_soc_negative_fails() {
454 let event = valid_event().with_power(PowerDomain {
455 battery: Some(Battery {
456 state_of_charge_pct: Some(-10.0), ..Default::default()
458 }),
459 ..Default::default()
460 });
461
462 let validator = Validator::new(ValidationLevel::Full);
463 let result = validator.validate(&event);
464 assert!(result.is_err());
465 assert!(result
466 .unwrap_err()
467 .to_string()
468 .contains("state_of_charge_pct"));
469 }
470
471 #[test]
472 fn test_invalid_soh_too_high_fails() {
473 let event = valid_event().with_power(PowerDomain {
474 battery: Some(Battery {
475 state_of_health_pct: Some(110.0), ..Default::default()
477 }),
478 ..Default::default()
479 });
480
481 let validator = Validator::new(ValidationLevel::Full);
482 let result = validator.validate(&event);
483 assert!(result.is_err());
484 assert!(result
485 .unwrap_err()
486 .to_string()
487 .contains("state_of_health_pct"));
488 }
489
490 #[test]
491 fn test_invalid_soh_negative_fails() {
492 let event = valid_event().with_power(PowerDomain {
493 battery: Some(Battery {
494 state_of_health_pct: Some(-5.0), ..Default::default()
496 }),
497 ..Default::default()
498 });
499
500 let validator = Validator::new(ValidationLevel::Full);
501 let result = validator.validate(&event);
502 assert!(result.is_err());
503 assert!(result
504 .unwrap_err()
505 .to_string()
506 .contains("state_of_health_pct"));
507 }
508
509 #[test]
510 fn test_invalid_voltage_negative_fails() {
511 let event = valid_event().with_power(PowerDomain {
512 battery: Some(Battery {
513 voltage_v: Some(-12.0), ..Default::default()
515 }),
516 ..Default::default()
517 });
518
519 let validator = Validator::new(ValidationLevel::Full);
520 let result = validator.validate(&event);
521 assert!(result.is_err());
522 assert!(result.unwrap_err().to_string().contains("voltage_v"));
523 }
524
525 #[test]
526 fn test_valid_power_with_all_fields() {
527 let event = valid_event().with_power(PowerDomain {
528 battery: Some(Battery {
529 state_of_charge_pct: Some(75.0),
530 state_of_health_pct: Some(95.0),
531 voltage_v: Some(48.0),
532 ..Default::default()
533 }),
534 ..Default::default()
535 });
536
537 let validator = Validator::new(ValidationLevel::Full);
538 assert!(validator.validate(&event).is_ok());
539 }
540
541 #[test]
542 fn test_power_without_battery_passes() {
543 let event = valid_event().with_power(PowerDomain {
544 battery: None,
545 ..Default::default()
546 });
547
548 let validator = Validator::new(ValidationLevel::Full);
549 assert!(validator.validate(&event).is_ok());
550 }
551
552 #[test]
553 fn test_invalid_proximity_distance_negative_fails() {
554 let event = valid_event().with_safety(SafetyDomain {
555 proximity: Some(ProximityInfo {
556 closest_distance_m: Some(-1.0), ..Default::default()
558 }),
559 ..Default::default()
560 });
561
562 let validator = Validator::new(ValidationLevel::Full);
563 let result = validator.validate(&event);
564 assert!(result.is_err());
565 assert!(result
566 .unwrap_err()
567 .to_string()
568 .contains("closest_distance_m"));
569 }
570
571 #[test]
572 fn test_valid_proximity_distance_passes() {
573 let event = valid_event().with_safety(SafetyDomain {
574 proximity: Some(ProximityInfo {
575 closest_distance_m: Some(2.5),
576 ..Default::default()
577 }),
578 ..Default::default()
579 });
580
581 let validator = Validator::new(ValidationLevel::Full);
582 assert!(validator.validate(&event).is_ok());
583 }
584
585 #[test]
586 fn test_safety_without_proximity_passes() {
587 let event = valid_event().with_safety(SafetyDomain {
588 proximity: None,
589 ..Default::default()
590 });
591
592 let validator = Validator::new(ValidationLevel::Full);
593 assert!(validator.validate(&event).is_ok());
594 }
595
596 #[test]
597 fn test_proximity_without_closest_distance_passes() {
598 let event = valid_event().with_safety(SafetyDomain {
599 proximity: Some(ProximityInfo {
600 closest_distance_m: None,
601 humans_nearby: Some(2),
602 ..Default::default()
603 }),
604 ..Default::default()
605 });
606
607 let validator = Validator::new(ValidationLevel::Full);
608 assert!(validator.validate(&event).is_ok());
609 }
610
611 #[test]
612 fn test_full_validation_with_all_domains() {
613 let event = valid_event()
614 .with_identity(IdentityDomain {
615 source_id: Some("robot-001".to_string()),
616 ..Default::default()
617 })
618 .with_location(LocationDomain {
619 latitude: Some(37.7749),
620 longitude: Some(-122.4194),
621 heading_deg: Some(45.0),
622 ..Default::default()
623 })
624 .with_power(PowerDomain {
625 battery: Some(Battery {
626 state_of_charge_pct: Some(80.0),
627 state_of_health_pct: Some(98.0),
628 voltage_v: Some(24.0),
629 ..Default::default()
630 }),
631 ..Default::default()
632 })
633 .with_safety(SafetyDomain {
634 proximity: Some(ProximityInfo {
635 closest_distance_m: Some(3.0),
636 ..Default::default()
637 }),
638 ..Default::default()
639 });
640
641 let validator = Validator::new(ValidationLevel::Full);
642 assert!(validator.validate(&event).is_ok());
643 }
644
645 #[test]
646 fn test_edge_case_latitude_boundaries() {
647 let event_min = valid_event().with_location(LocationDomain {
649 latitude: Some(-90.0),
650 ..Default::default()
651 });
652 let event_max = valid_event().with_location(LocationDomain {
653 latitude: Some(90.0),
654 ..Default::default()
655 });
656
657 let validator = Validator::new(ValidationLevel::Full);
658 assert!(validator.validate(&event_min).is_ok());
659 assert!(validator.validate(&event_max).is_ok());
660 }
661
662 #[test]
663 fn test_edge_case_longitude_boundaries() {
664 let event_min = valid_event().with_location(LocationDomain {
666 longitude: Some(-180.0),
667 ..Default::default()
668 });
669 let event_max = valid_event().with_location(LocationDomain {
670 longitude: Some(180.0),
671 ..Default::default()
672 });
673
674 let validator = Validator::new(ValidationLevel::Full);
675 assert!(validator.validate(&event_min).is_ok());
676 assert!(validator.validate(&event_max).is_ok());
677 }
678
679 #[test]
680 fn test_edge_case_soc_boundaries() {
681 let event_min = valid_event().with_power(PowerDomain {
683 battery: Some(Battery {
684 state_of_charge_pct: Some(0.0),
685 ..Default::default()
686 }),
687 ..Default::default()
688 });
689 let event_max = valid_event().with_power(PowerDomain {
690 battery: Some(Battery {
691 state_of_charge_pct: Some(100.0),
692 ..Default::default()
693 }),
694 ..Default::default()
695 });
696
697 let validator = Validator::new(ValidationLevel::Full);
698 assert!(validator.validate(&event_min).is_ok());
699 assert!(validator.validate(&event_max).is_ok());
700 }
701
702 #[test]
703 fn test_edge_case_zero_voltage_passes() {
704 let event = valid_event().with_power(PowerDomain {
705 battery: Some(Battery {
706 voltage_v: Some(0.0), ..Default::default()
708 }),
709 ..Default::default()
710 });
711
712 let validator = Validator::new(ValidationLevel::Full);
713 assert!(validator.validate(&event).is_ok());
714 }
715
716 #[test]
717 fn test_edge_case_zero_proximity_passes() {
718 let event = valid_event().with_safety(SafetyDomain {
719 proximity: Some(ProximityInfo {
720 closest_distance_m: Some(0.0), ..Default::default()
722 }),
723 ..Default::default()
724 });
725
726 let validator = Validator::new(ValidationLevel::Full);
727 assert!(validator.validate(&event).is_ok());
728 }
729}