1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10use validator::Validate;
11
12use crate::models::domains::{
13 ActuatorsDomain, AiDomain, AudioDomain, CommunicationDomain, ComplianceDomain, ComputeDomain,
14 ContextDomain, CoordinationDomain, EnvironmentInteractionDomain, HriDomain, IdentityDomain,
15 LocationDomain, MaintenanceDomain, ManipulationDomain, MotionDomain, NavigationDomain,
16 OperationalDomain, PayloadDomain, PerceptionDomain, PowerDomain, SafetyDomain,
17 SimulationDomain, ThermalDomain,
18};
19use crate::models::enums::{EventType, SourceType};
20
21#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
52pub struct UdmEvent {
53 pub event_id: String,
58
59 #[serde(alias = "timestamp")]
62 pub captured_at: DateTime<Utc>,
63
64 #[serde(default)]
66 pub event_type: EventType,
67
68 #[serde(default)]
71 pub source_id: String,
72
73 pub source_type: SourceType,
75
76 #[serde(alias = "schema_version")]
78 pub udm_version: String,
79
80 pub sdk_version: String,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
88 pub identity: Option<IdentityDomain>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub location: Option<LocationDomain>,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub motion: Option<MotionDomain>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub power: Option<PowerDomain>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub operational: Option<OperationalDomain>,
105
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub navigation: Option<NavigationDomain>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub perception: Option<PerceptionDomain>,
113
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub safety: Option<SafetyDomain>,
117
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub actuators: Option<ActuatorsDomain>,
121
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub communication: Option<CommunicationDomain>,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub compute: Option<ComputeDomain>,
129
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub ai: Option<AiDomain>,
133
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub maintenance: Option<MaintenanceDomain>,
137
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub context: Option<ContextDomain>,
141
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub payload: Option<PayloadDomain>,
145
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub manipulation: Option<ManipulationDomain>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub hri: Option<HriDomain>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub coordination: Option<CoordinationDomain>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub simulation: Option<SimulationDomain>,
161
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub thermal: Option<ThermalDomain>,
165
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub audio: Option<AudioDomain>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub environment_interaction: Option<EnvironmentInteractionDomain>,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub compliance: Option<ComplianceDomain>,
177
178 #[serde(skip_serializing_if = "Option::is_none")]
183 pub provenance: Option<Provenance>,
184
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub extensions: Option<serde_json::Value>,
188}
189
190#[derive(Debug, Clone, Default, Serialize, Deserialize)]
192pub struct Provenance {
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub signature: Option<String>,
196
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub key_id: Option<String>,
200
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub algorithm: Option<String>,
204
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub signed_fields: Option<Vec<String>>,
208
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub signed_at: Option<DateTime<Utc>>,
212}
213
214impl UdmEvent {
215 pub fn new(source_type: SourceType) -> Self {
219 Self {
220 event_id: Uuid::now_v7().to_string(),
221 captured_at: Utc::now(),
222 event_type: EventType::default(),
223 source_type,
224 udm_version: crate::UDM_VERSION.to_string(),
225 sdk_version: crate::SDK_VERSION.to_string(),
226 ..Default::default()
227 }
228 }
229
230 pub fn with_event_type(mut self, event_type: EventType) -> Self {
232 self.event_type = event_type;
233 self
234 }
235
236 pub fn with_identity(mut self, identity: IdentityDomain) -> Self {
238 self.identity = Some(identity);
239 self
240 }
241
242 pub fn with_location(mut self, location: LocationDomain) -> Self {
244 self.location = Some(location);
245 self
246 }
247
248 pub fn with_motion(mut self, motion: MotionDomain) -> Self {
250 self.motion = Some(motion);
251 self
252 }
253
254 pub fn with_power(mut self, power: PowerDomain) -> Self {
256 self.power = Some(power);
257 self
258 }
259
260 pub fn with_operational(mut self, operational: OperationalDomain) -> Self {
262 self.operational = Some(operational);
263 self
264 }
265
266 pub fn with_navigation(mut self, navigation: NavigationDomain) -> Self {
268 self.navigation = Some(navigation);
269 self
270 }
271
272 pub fn with_perception(mut self, perception: PerceptionDomain) -> Self {
274 self.perception = Some(perception);
275 self
276 }
277
278 pub fn with_safety(mut self, safety: SafetyDomain) -> Self {
280 self.safety = Some(safety);
281 self
282 }
283
284 pub fn with_actuators(mut self, actuators: ActuatorsDomain) -> Self {
286 self.actuators = Some(actuators);
287 self
288 }
289
290 pub fn with_communication(mut self, communication: CommunicationDomain) -> Self {
292 self.communication = Some(communication);
293 self
294 }
295
296 pub fn with_compute(mut self, compute: ComputeDomain) -> Self {
298 self.compute = Some(compute);
299 self
300 }
301
302 pub fn with_ai(mut self, ai: AiDomain) -> Self {
304 self.ai = Some(ai);
305 self
306 }
307
308 pub fn with_maintenance(mut self, maintenance: MaintenanceDomain) -> Self {
310 self.maintenance = Some(maintenance);
311 self
312 }
313
314 pub fn with_context(mut self, context: ContextDomain) -> Self {
316 self.context = Some(context);
317 self
318 }
319
320 pub fn with_payload(mut self, payload: PayloadDomain) -> Self {
322 self.payload = Some(payload);
323 self
324 }
325
326 pub fn with_manipulation(mut self, manipulation: ManipulationDomain) -> Self {
328 self.manipulation = Some(manipulation);
329 self
330 }
331
332 pub fn with_hri(mut self, hri: HriDomain) -> Self {
334 self.hri = Some(hri);
335 self
336 }
337
338 pub fn with_coordination(mut self, coordination: CoordinationDomain) -> Self {
340 self.coordination = Some(coordination);
341 self
342 }
343
344 pub fn with_simulation(mut self, simulation: SimulationDomain) -> Self {
346 self.simulation = Some(simulation);
347 self
348 }
349
350 pub fn with_thermal(mut self, thermal: ThermalDomain) -> Self {
352 self.thermal = Some(thermal);
353 self
354 }
355
356 pub fn with_audio(mut self, audio: AudioDomain) -> Self {
358 self.audio = Some(audio);
359 self
360 }
361
362 pub fn with_environment_interaction(
364 mut self,
365 environment_interaction: EnvironmentInteractionDomain,
366 ) -> Self {
367 self.environment_interaction = Some(environment_interaction);
368 self
369 }
370
371 pub fn with_compliance(mut self, compliance: ComplianceDomain) -> Self {
373 self.compliance = Some(compliance);
374 self
375 }
376
377 pub fn with_extensions(mut self, extensions: serde_json::Value) -> Self {
379 self.extensions = Some(extensions);
380 self
381 }
382
383 pub fn inject_license_metadata(&mut self, metadata: &crate::core::license::LicenseMetadata) {
387 let license_data = serde_json::json!({
388 "_license_status": metadata.status,
389 "_license_tenant_id": metadata.tenant_id,
390 "_license_plan": metadata.plan,
391 "_quarantine": metadata.quarantine,
392 "_grace_period_start": metadata.grace_started_at,
393 "_grace_period_expires": metadata.grace_expires_at,
394 "_last_online_check": metadata.last_online_check,
395 });
396
397 if let Some(ref mut extensions) = self.extensions {
398 if let Some(obj) = extensions.as_object_mut() {
399 if let Some(license_obj) = license_data.as_object() {
400 for (key, value) in license_obj {
401 obj.insert(key.clone(), value.clone());
402 }
403 }
404 }
405 } else {
406 self.extensions = Some(license_data);
407 }
408 }
409
410 pub fn with_provenance(mut self, provenance: Provenance) -> Self {
412 self.provenance = Some(provenance);
413 self
414 }
415
416 pub fn has_domain_data(&self) -> bool {
418 self.identity.is_some()
419 || self.location.is_some()
420 || self.motion.is_some()
421 || self.power.is_some()
422 || self.operational.is_some()
423 || self.navigation.is_some()
424 || self.perception.is_some()
425 || self.safety.is_some()
426 || self.actuators.is_some()
427 || self.communication.is_some()
428 || self.compute.is_some()
429 || self.ai.is_some()
430 || self.maintenance.is_some()
431 || self.context.is_some()
432 || self.payload.is_some()
433 || self.manipulation.is_some()
434 || self.hri.is_some()
435 || self.coordination.is_some()
436 || self.simulation.is_some()
437 || self.thermal.is_some()
438 || self.audio.is_some()
439 || self.environment_interaction.is_some()
440 || self.compliance.is_some()
441 }
442
443 pub fn estimated_size(&self) -> usize {
445 serde_json::to_string(self).map(|s| s.len()).unwrap_or(0)
446 }
447
448 pub fn to_json(&self) -> Result<String, serde_json::Error> {
450 serde_json::to_string(self)
451 }
452
453 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
455 serde_json::to_string_pretty(self)
456 }
457
458 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
460 serde_json::from_str(json)
461 }
462}
463
464impl Provenance {
465 pub fn new(algorithm: impl Into<String>) -> Self {
467 Self {
468 algorithm: Some(algorithm.into()),
469 signed_at: Some(Utc::now()),
470 ..Default::default()
471 }
472 }
473
474 pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
476 self.signature = Some(signature.into());
477 self
478 }
479
480 pub fn with_key_id(mut self, key_id: impl Into<String>) -> Self {
482 self.key_id = Some(key_id.into());
483 self
484 }
485
486 pub fn with_signed_fields(mut self, fields: Vec<String>) -> Self {
488 self.signed_fields = Some(fields);
489 self
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496
497 #[test]
498 fn test_event_creation() {
499 let event = UdmEvent::new(SourceType::Amr);
500
501 assert!(!event.event_id.is_empty());
502 assert_eq!(event.source_type, SourceType::Amr);
503 assert_eq!(event.event_type, EventType::TelemetryPeriodic);
504 assert!(!event.has_domain_data());
505 }
506
507 #[test]
508 fn test_event_with_domains() {
509 use crate::models::domains::location::LocalCoordinates;
510
511 let event = UdmEvent::new(SourceType::Amr)
512 .with_identity(IdentityDomain {
513 source_id: Some("robot-001".to_string()),
514 ..Default::default()
515 })
516 .with_location(LocationDomain {
517 local: Some(LocalCoordinates {
518 x_m: Some(10.0),
519 y_m: Some(20.0),
520 ..Default::default()
521 }),
522 ..Default::default()
523 });
524
525 assert!(event.has_domain_data());
526 assert_eq!(
527 event.identity.as_ref().unwrap().source_id,
528 Some("robot-001".to_string())
529 );
530 }
531
532 #[test]
533 fn test_event_serialization() {
534 let event = UdmEvent::new(SourceType::Amr).with_event_type(EventType::StateTransition);
535
536 let json = event.to_json().unwrap();
537 assert!(json.contains("\"event_id\""));
538 assert!(json.contains("\"state_transition\""));
539
540 let deserialized = UdmEvent::from_json(&json).unwrap();
541 assert_eq!(deserialized.event_type, EventType::StateTransition);
542 }
543
544 #[test]
545 fn test_provenance() {
546 let provenance = Provenance::new("hmac-sha256")
547 .with_signature("abc123")
548 .with_key_id("key-001");
549
550 assert_eq!(provenance.algorithm, Some("hmac-sha256".to_string()));
551 assert_eq!(provenance.signature, Some("abc123".to_string()));
552 }
553
554 #[test]
555 fn test_event_with_extensions() {
556 let extensions = serde_json::json!({
557 "custom_field": "custom_value",
558 "nested": { "key": 42 }
559 });
560
561 let event = UdmEvent::new(SourceType::Custom).with_extensions(extensions.clone());
562
563 assert_eq!(event.extensions, Some(extensions));
564 }
565
566 #[test]
571 fn test_event_default() {
572 let event = UdmEvent::default();
573 assert!(event.event_id.is_empty());
574 assert!(event.identity.is_none());
575 assert!(event.location.is_none());
576 }
577
578 #[test]
579 fn test_event_with_event_type() {
580 let event = UdmEvent::new(SourceType::Amr).with_event_type(EventType::SafetyViolation);
581 assert_eq!(event.event_type, EventType::SafetyViolation);
582 }
583
584 #[test]
585 fn test_event_with_all_domains() {
586 let event = UdmEvent::new(SourceType::Amr)
587 .with_identity(IdentityDomain::default())
588 .with_location(LocationDomain::default())
589 .with_motion(MotionDomain::default())
590 .with_power(PowerDomain::default())
591 .with_operational(OperationalDomain::default())
592 .with_navigation(NavigationDomain::default())
593 .with_perception(PerceptionDomain::default())
594 .with_safety(SafetyDomain::default())
595 .with_actuators(ActuatorsDomain::default())
596 .with_communication(CommunicationDomain::default())
597 .with_compute(ComputeDomain::default())
598 .with_ai(AiDomain::default())
599 .with_maintenance(MaintenanceDomain::default())
600 .with_context(ContextDomain::default())
601 .with_payload(PayloadDomain::default())
602 .with_manipulation(ManipulationDomain::default())
603 .with_hri(HriDomain::default())
604 .with_coordination(CoordinationDomain::default())
605 .with_simulation(SimulationDomain::default())
606 .with_thermal(ThermalDomain::default())
607 .with_audio(AudioDomain::default())
608 .with_environment_interaction(EnvironmentInteractionDomain::default())
609 .with_compliance(ComplianceDomain::default());
610
611 assert!(event.has_domain_data());
612 assert!(event.identity.is_some());
613 assert!(event.location.is_some());
614 assert!(event.motion.is_some());
615 assert!(event.power.is_some());
616 assert!(event.operational.is_some());
617 assert!(event.navigation.is_some());
618 assert!(event.perception.is_some());
619 assert!(event.safety.is_some());
620 assert!(event.actuators.is_some());
621 assert!(event.communication.is_some());
622 assert!(event.compute.is_some());
623 assert!(event.ai.is_some());
624 assert!(event.maintenance.is_some());
625 assert!(event.context.is_some());
626 assert!(event.payload.is_some());
627 assert!(event.manipulation.is_some());
628 assert!(event.hri.is_some());
629 assert!(event.coordination.is_some());
630 assert!(event.simulation.is_some());
631 assert!(event.thermal.is_some());
632 assert!(event.audio.is_some());
633 assert!(event.environment_interaction.is_some());
634 assert!(event.compliance.is_some());
635 }
636
637 #[test]
638 fn test_event_with_provenance() {
639 let prov = Provenance::new("hmac-sha256")
640 .with_signature("sig123")
641 .with_key_id("key-001");
642
643 let event = UdmEvent::new(SourceType::Amr).with_provenance(prov);
644
645 assert!(event.provenance.is_some());
646 assert_eq!(
647 event.provenance.as_ref().unwrap().signature,
648 Some("sig123".to_string())
649 );
650 }
651
652 #[test]
653 fn test_event_has_domain_data_single() {
654 let event_identity =
656 UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain::default());
657 assert!(event_identity.has_domain_data());
658
659 let event_motion = UdmEvent::new(SourceType::Amr).with_motion(MotionDomain::default());
660 assert!(event_motion.has_domain_data());
661
662 let event_ai = UdmEvent::new(SourceType::Amr).with_ai(AiDomain::default());
663 assert!(event_ai.has_domain_data());
664 }
665
666 #[test]
667 fn test_event_estimated_size() {
668 let event = UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain {
669 source_id: Some("robot-001".to_string()),
670 ..Default::default()
671 });
672
673 let size = event.estimated_size();
674 assert!(size > 0);
675 }
676
677 #[test]
678 fn test_event_to_json() {
679 let event = UdmEvent::new(SourceType::Amr);
680 let json = event.to_json().unwrap();
681
682 assert!(json.contains("event_id"));
683 assert!(json.contains("captured_at"));
684 assert!(json.contains("source_type"));
685 }
686
687 #[test]
688 fn test_event_to_json_pretty() {
689 let event = UdmEvent::new(SourceType::Amr);
690 let json = event.to_json_pretty().unwrap();
691
692 assert!(json.contains("\n"));
694 }
695
696 #[test]
697 fn test_event_from_json() {
698 let event = UdmEvent::new(SourceType::Drone);
699 let json = event.to_json().unwrap();
700
701 let deserialized = UdmEvent::from_json(&json).unwrap();
702 assert_eq!(deserialized.source_type, SourceType::Drone);
703 }
704
705 #[test]
706 fn test_event_from_json_invalid() {
707 let result = UdmEvent::from_json("invalid json");
708 result.unwrap_err();
709 }
710
711 #[test]
716 fn test_provenance_default() {
717 let prov = Provenance::default();
718 assert!(prov.signature.is_none());
719 assert!(prov.key_id.is_none());
720 assert!(prov.algorithm.is_none());
721 assert!(prov.signed_fields.is_none());
722 assert!(prov.signed_at.is_none());
723 }
724
725 #[test]
726 fn test_provenance_new() {
727 let prov = Provenance::new("hmac-sha256");
728 assert_eq!(prov.algorithm, Some("hmac-sha256".to_string()));
729 assert!(prov.signed_at.is_some());
730 }
731
732 #[test]
733 fn test_provenance_with_signature() {
734 let prov = Provenance::new("hmac-sha256").with_signature("abc123def456");
735 assert_eq!(prov.signature, Some("abc123def456".to_string()));
736 }
737
738 #[test]
739 fn test_provenance_with_key_id() {
740 let prov = Provenance::new("hmac-sha256").with_key_id("my-key-001");
741 assert_eq!(prov.key_id, Some("my-key-001".to_string()));
742 }
743
744 #[test]
745 fn test_provenance_with_signed_fields() {
746 let fields = vec![
747 "event_id".to_string(),
748 "timestamp".to_string(),
749 "identity".to_string(),
750 ];
751 let prov = Provenance::new("hmac-sha256").with_signed_fields(fields.clone());
752 assert_eq!(prov.signed_fields, Some(fields));
753 }
754
755 #[test]
756 fn test_provenance_chained_builders() {
757 let prov = Provenance::new("hmac-sha256")
758 .with_signature("sig")
759 .with_key_id("key")
760 .with_signed_fields(vec!["event_id".to_string()]);
761
762 assert!(prov.algorithm.is_some());
763 assert!(prov.signature.is_some());
764 assert!(prov.key_id.is_some());
765 assert!(prov.signed_fields.is_some());
766 }
767
768 #[test]
773 fn test_event_full_serialization_roundtrip() {
774 let event = UdmEvent::new(SourceType::Amr)
775 .with_event_type(EventType::TelemetryPeriodic)
776 .with_identity(IdentityDomain {
777 source_id: Some("robot-001".to_string()),
778 ..Default::default()
779 })
780 .with_location(LocationDomain::default())
781 .with_provenance(Provenance::new("hmac-sha256").with_signature("test"));
782
783 let json = event.to_json().unwrap();
784 let deserialized = UdmEvent::from_json(&json).unwrap();
785
786 assert_eq!(deserialized.source_type, SourceType::Amr);
787 assert!(deserialized.identity.is_some());
788 assert!(deserialized.location.is_some());
789 assert!(deserialized.provenance.is_some());
790 }
791
792 #[test]
793 fn test_event_clone() {
794 let event = UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain {
795 source_id: Some("robot-001".to_string()),
796 ..Default::default()
797 });
798
799 let cloned = event.clone();
800 assert_eq!(cloned.event_id, event.event_id);
801 assert_eq!(cloned.source_type, event.source_type);
802 }
803}