phytrace_sdk/core/
builder.rs

1//! Event builder for constructing UDM events fluently.
2//!
3//! The EventBuilder provides a fluent interface for constructing events
4//! with automatic configuration injection and validation.
5
6use crate::core::config::PhyTraceConfig;
7use crate::error::{BuilderError, PhyTraceResult};
8use crate::models::domains::location::LocalCoordinates;
9use crate::models::domains::*;
10use crate::models::enums::{EventType, SourceType};
11use crate::models::event::UdmEvent;
12
13/// Fluent builder for UDM events.
14///
15/// # Example
16///
17/// ```rust
18/// use phytrace_sdk::core::{EventBuilder, PhyTraceConfig};
19/// use phytrace_sdk::{EventType, SourceType};
20///
21/// let config = PhyTraceConfig::new("robot-001")
22///     .with_source_type(SourceType::Amr);
23///
24/// let event = EventBuilder::new(&config)
25///     .event_type(EventType::TelemetryPeriodic)
26///     .position_local(10.0, 20.0, 0.0)
27///     .heading_deg(90.0)
28///     .battery_soc(85.0)
29///     .build()
30///     .unwrap();
31/// ```
32pub struct EventBuilder {
33    event: UdmEvent,
34    config: PhyTraceConfig,
35}
36
37impl Clone for EventBuilder {
38    fn clone(&self) -> Self {
39        Self {
40            event: self.event.clone(),
41            config: self.config.clone(),
42        }
43    }
44}
45
46impl EventBuilder {
47    /// Create a new event builder with configuration.
48    pub fn new(config: &PhyTraceConfig) -> Self {
49        let mut event = UdmEvent::new(config.source.source_type);
50
51        // Set top-level source_id for PhyCloud compatibility
52        event.source_id = config.source.source_id.clone();
53
54        // Auto-populate identity from config
55        event.identity = Some(IdentityDomain {
56            source_id: Some(config.source.source_id.clone()),
57            fleet_id: config.source.fleet_id.clone(),
58            site_id: config.source.site_id.clone(),
59            organization_id: config.source.organization_id.clone(),
60            tags: if config.source.tags.is_empty() {
61                None
62            } else {
63                Some(config.source.tags.clone())
64            },
65            ..Default::default()
66        });
67
68        Self {
69            event,
70            config: config.clone(),
71        }
72    }
73
74    /// Set the event type.
75    pub fn event_type(mut self, event_type: EventType) -> Self {
76        self.event.event_type = event_type;
77        self
78    }
79
80    /// Override the source type.
81    pub fn source_type(mut self, source_type: SourceType) -> Self {
82        self.event.source_type = source_type;
83        self
84    }
85
86    // =========================================================================
87    // Identity Helpers
88    // =========================================================================
89
90    /// Set the source ID (robot/system identifier).
91    /// This sets both the top-level source_id (for PhyCloud) and the identity domain.
92    pub fn source_id(mut self, source_id: impl Into<String>) -> Self {
93        let sid = source_id.into();
94        // Set top-level source_id for PhyCloud compatibility
95        self.event.source_id = sid.clone();
96        // Also set in identity domain for UDM completeness
97        let identity = self.event.identity.get_or_insert_with(Default::default);
98        identity.source_id = Some(sid);
99        self
100    }
101
102    /// Set model/firmware information.
103    pub fn model_info(
104        mut self,
105        _manufacturer: impl Into<String>,
106        model: impl Into<String>,
107        firmware: impl Into<String>,
108    ) -> Self {
109        let identity = self.event.identity.get_or_insert_with(Default::default);
110        identity.model = Some(model.into());
111        identity.firmware_version = Some(firmware.into());
112        self
113    }
114
115    /// Add a tag.
116    pub fn tag(mut self, tag: impl Into<String>) -> Self {
117        let identity = self.event.identity.get_or_insert_with(Default::default);
118        let tags = identity.tags.get_or_insert_with(Vec::new);
119        tags.push(tag.into());
120        self
121    }
122
123    // =========================================================================
124    // Location Helpers
125    // =========================================================================
126
127    /// Set local position (x, y, z in meters).
128    pub fn position_local(mut self, x: f64, y: f64, z: f64) -> Self {
129        let location = self.event.location.get_or_insert_with(Default::default);
130        location.local = Some(LocalCoordinates {
131            x_m: Some(x),
132            y_m: Some(y),
133            z_m: Some(z),
134            ..Default::default()
135        });
136        self
137    }
138
139    /// Set 2D position (convenience for x, y only).
140    pub fn position_2d(mut self, x: f64, y: f64) -> Self {
141        let location = self.event.location.get_or_insert_with(Default::default);
142        location.local = Some(LocalCoordinates {
143            x_m: Some(x),
144            y_m: Some(y),
145            z_m: Some(0.0),
146            ..Default::default()
147        });
148        self
149    }
150
151    /// Set 3D position (convenience alias).
152    pub fn position_3d(self, x: f64, y: f64, z: f64) -> Self {
153        self.position_local(x, y, z)
154    }
155
156    /// Set heading in degrees (0-360).
157    pub fn heading_deg(mut self, heading: f64) -> Self {
158        let location = self.event.location.get_or_insert_with(Default::default);
159        location.heading_deg = Some(heading);
160        self
161    }
162
163    /// Set heading in radians (converted to degrees).
164    pub fn heading(self, heading_rad: f64) -> Self {
165        let heading_deg = heading_rad.to_degrees();
166        self.heading_deg(heading_deg)
167    }
168
169    /// Set GPS coordinates.
170    pub fn gps(mut self, latitude: f64, longitude: f64) -> Self {
171        let location = self.event.location.get_or_insert_with(Default::default);
172        location.latitude = Some(latitude);
173        location.longitude = Some(longitude);
174        self
175    }
176
177    /// Set floor number.
178    pub fn floor(mut self, floor: i32) -> Self {
179        let location = self.event.location.get_or_insert_with(Default::default);
180        location.floor = Some(floor);
181        self
182    }
183
184    /// Set map ID and frame.
185    pub fn map(mut self, map_id: impl Into<String>, frame_id: impl Into<String>) -> Self {
186        let location = self.event.location.get_or_insert_with(Default::default);
187        location.map_id = Some(map_id.into());
188        location.frame_id = Some(frame_id.into());
189        self
190    }
191
192    // =========================================================================
193    // Motion Helpers
194    // =========================================================================
195
196    /// Set linear velocity (m/s).
197    pub fn linear_velocity(mut self, vx: f64, vy: f64, vz: f64) -> Self {
198        let motion = self.event.motion.get_or_insert_with(Default::default);
199        motion.linear_velocity = Some(motion::LinearVelocity {
200            x_mps: Some(vx),
201            y_mps: Some(vy),
202            z_mps: Some(vz),
203        });
204        self
205    }
206
207    /// Set angular velocity (rad/s) - converts to degrees/s internally.
208    pub fn angular_velocity(mut self, roll_rps: f64, pitch_rps: f64, yaw_rps: f64) -> Self {
209        let motion = self.event.motion.get_or_insert_with(Default::default);
210        motion.angular_velocity = Some(motion::AngularVelocity {
211            roll_dps: Some(roll_rps.to_degrees()),
212            pitch_dps: Some(pitch_rps.to_degrees()),
213            yaw_dps: Some(yaw_rps.to_degrees()),
214        });
215        self
216    }
217
218    /// Set speed (m/s) - for 2D motion.
219    pub fn speed(mut self, speed_mps: f64) -> Self {
220        let motion = self.event.motion.get_or_insert_with(Default::default);
221        motion.speed_mps = Some(speed_mps);
222        self
223    }
224
225    // =========================================================================
226    // Power Helpers
227    // =========================================================================
228
229    /// Set battery state of charge (0-100%).
230    pub fn battery_soc(mut self, soc_percent: f64) -> Self {
231        let power = self.event.power.get_or_insert_with(Default::default);
232        let battery = power.battery.get_or_insert_with(Default::default);
233        battery.state_of_charge_pct = Some(soc_percent);
234        self
235    }
236
237    /// Set battery voltage.
238    pub fn battery_voltage(mut self, voltage_v: f64) -> Self {
239        let power = self.event.power.get_or_insert_with(Default::default);
240        let battery = power.battery.get_or_insert_with(Default::default);
241        battery.voltage_v = Some(voltage_v);
242        self
243    }
244
245    /// Set battery health (0-100%).
246    pub fn battery_health(mut self, health_percent: f64) -> Self {
247        let power = self.event.power.get_or_insert_with(Default::default);
248        let battery = power.battery.get_or_insert_with(Default::default);
249        battery.state_of_health_pct = Some(health_percent);
250        self
251    }
252
253    /// Set charging status.
254    pub fn charging(mut self, is_charging: bool) -> Self {
255        use crate::models::enums::ChargingState;
256        let power = self.event.power.get_or_insert_with(Default::default);
257        let charging = power.charging.get_or_insert_with(Default::default);
258        charging.state = Some(if is_charging {
259            ChargingState::Charging
260        } else {
261            ChargingState::NotCharging
262        });
263        self
264    }
265
266    // =========================================================================
267    // Operational Helpers
268    // =========================================================================
269
270    /// Set operational mode.
271    pub fn operational_mode(mut self, mode: crate::models::enums::OperationalMode) -> Self {
272        let operational = self.event.operational.get_or_insert_with(Default::default);
273        operational.mode = Some(mode);
274        self
275    }
276
277    /// Set operational state.
278    pub fn operational_state(mut self, state: crate::models::enums::OperationalState) -> Self {
279        let operational = self.event.operational.get_or_insert_with(Default::default);
280        operational.state = Some(state);
281        self
282    }
283
284    /// Set current task.
285    pub fn task(mut self, task_id: impl Into<String>, task_type: impl Into<String>) -> Self {
286        let operational = self.event.operational.get_or_insert_with(Default::default);
287        operational.task = Some(operational::Task {
288            task_id: Some(task_id.into()),
289            task_type: Some(task_type.into()),
290            ..Default::default()
291        });
292        self
293    }
294
295    // =========================================================================
296    // Safety Helpers
297    // =========================================================================
298
299    /// Set safety state.
300    pub fn safety_state(mut self, state: crate::models::enums::SafetyState) -> Self {
301        let safety = self.event.safety.get_or_insert_with(Default::default);
302        safety.safety_state = Some(state);
303        self
304    }
305
306    /// Set e-stop status.
307    pub fn estop_active(mut self, active: bool) -> Self {
308        let safety = self.event.safety.get_or_insert_with(Default::default);
309        let estop = safety.e_stop.get_or_insert_with(Default::default);
310        estop.is_active = Some(active);
311        self
312    }
313
314    // =========================================================================
315    // Navigation Helpers
316    // =========================================================================
317
318    /// Set localization quality.
319    pub fn localization_quality(
320        mut self,
321        quality: crate::models::enums::LocalizationQuality,
322    ) -> Self {
323        let nav = self.event.navigation.get_or_insert_with(Default::default);
324        let loc = nav.localization.get_or_insert_with(Default::default);
325        loc.quality = Some(quality);
326        self
327    }
328
329    /// Set path state.
330    pub fn path_state(mut self, state: crate::models::enums::PathState) -> Self {
331        let nav = self.event.navigation.get_or_insert_with(Default::default);
332        let path = nav.path.get_or_insert_with(Default::default);
333        path.state = Some(state);
334        self
335    }
336
337    // =========================================================================
338    // Domain Setters
339    // =========================================================================
340
341    /// Set the full identity domain.
342    pub fn identity(mut self, identity: IdentityDomain) -> Self {
343        self.event.identity = Some(identity);
344        self
345    }
346
347    /// Set the full location domain.
348    pub fn location(mut self, location: LocationDomain) -> Self {
349        self.event.location = Some(location);
350        self
351    }
352
353    /// Set the full motion domain.
354    pub fn motion(mut self, motion: MotionDomain) -> Self {
355        self.event.motion = Some(motion);
356        self
357    }
358
359    /// Set the full power domain.
360    pub fn power(mut self, power: PowerDomain) -> Self {
361        self.event.power = Some(power);
362        self
363    }
364
365    /// Set the full operational domain.
366    pub fn operational(mut self, operational: OperationalDomain) -> Self {
367        self.event.operational = Some(operational);
368        self
369    }
370
371    /// Set the full navigation domain.
372    pub fn navigation(mut self, navigation: NavigationDomain) -> Self {
373        self.event.navigation = Some(navigation);
374        self
375    }
376
377    /// Set the full perception domain.
378    pub fn perception(mut self, perception: PerceptionDomain) -> Self {
379        self.event.perception = Some(perception);
380        self
381    }
382
383    /// Set the full safety domain.
384    pub fn safety(mut self, safety: SafetyDomain) -> Self {
385        self.event.safety = Some(safety);
386        self
387    }
388
389    /// Set the full actuators domain.
390    pub fn actuators(mut self, actuators: ActuatorsDomain) -> Self {
391        self.event.actuators = Some(actuators);
392        self
393    }
394
395    /// Set the full communication domain.
396    pub fn communication(mut self, communication: CommunicationDomain) -> Self {
397        self.event.communication = Some(communication);
398        self
399    }
400
401    /// Set the full compute domain.
402    pub fn compute(mut self, compute: ComputeDomain) -> Self {
403        self.event.compute = Some(compute);
404        self
405    }
406
407    /// Set the full AI domain.
408    pub fn ai(mut self, ai: AiDomain) -> Self {
409        self.event.ai = Some(ai);
410        self
411    }
412
413    /// Set the full maintenance domain.
414    pub fn maintenance(mut self, maintenance: MaintenanceDomain) -> Self {
415        self.event.maintenance = Some(maintenance);
416        self
417    }
418
419    /// Set the full context domain.
420    pub fn context(mut self, context: ContextDomain) -> Self {
421        self.event.context = Some(context);
422        self
423    }
424
425    /// Set the full payload domain.
426    pub fn payload(mut self, payload: PayloadDomain) -> Self {
427        self.event.payload = Some(payload);
428        self
429    }
430
431    /// Set the full manipulation domain.
432    pub fn manipulation(mut self, manipulation: ManipulationDomain) -> Self {
433        self.event.manipulation = Some(manipulation);
434        self
435    }
436
437    /// Set the full HRI domain.
438    pub fn hri(mut self, hri: HriDomain) -> Self {
439        self.event.hri = Some(hri);
440        self
441    }
442
443    /// Set the full coordination domain.
444    pub fn coordination(mut self, coordination: CoordinationDomain) -> Self {
445        self.event.coordination = Some(coordination);
446        self
447    }
448
449    /// Set the full simulation domain.
450    pub fn simulation(mut self, simulation: SimulationDomain) -> Self {
451        self.event.simulation = Some(simulation);
452        self
453    }
454
455    /// Set the full thermal domain.
456    pub fn thermal(mut self, thermal: ThermalDomain) -> Self {
457        self.event.thermal = Some(thermal);
458        self
459    }
460
461    /// Set the full audio domain.
462    pub fn audio(mut self, audio: AudioDomain) -> Self {
463        self.event.audio = Some(audio);
464        self
465    }
466
467    /// Set the full environment interaction domain.
468    pub fn environment_interaction(
469        mut self,
470        environment_interaction: EnvironmentInteractionDomain,
471    ) -> Self {
472        self.event.environment_interaction = Some(environment_interaction);
473        self
474    }
475
476    /// Set the full compliance domain.
477    pub fn compliance(mut self, compliance: ComplianceDomain) -> Self {
478        self.event.compliance = Some(compliance);
479        self
480    }
481
482    /// Set custom extensions.
483    pub fn extensions(mut self, extensions: serde_json::Value) -> Self {
484        self.event.extensions = Some(extensions);
485        self
486    }
487
488    // =========================================================================
489    // Build
490    // =========================================================================
491
492    /// Build the event, validating and optionally signing.
493    pub fn build(self) -> PhyTraceResult<UdmEvent> {
494        // Validate required fields
495        if !self.event.has_domain_data() {
496            return Err(BuilderError::MissingField("at least one domain".to_string()).into());
497        }
498
499        Ok(self.event)
500    }
501
502    /// Build without validation.
503    pub fn build_unchecked(self) -> UdmEvent {
504        self.event
505    }
506
507    /// Get a reference to the event being built.
508    pub fn peek(&self) -> &UdmEvent {
509        &self.event
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516    use crate::models::domains::{
517        ActuatorsDomain, AiDomain, AudioDomain, CommunicationDomain, ComplianceDomain,
518        ComputeDomain, ContextDomain, CoordinationDomain, EnvironmentInteractionDomain, HriDomain,
519        MaintenanceDomain, ManipulationDomain, MotionDomain, NavigationDomain, PayloadDomain,
520        PerceptionDomain, SafetyDomain, SimulationDomain, ThermalDomain,
521    };
522    use crate::models::enums::{
523        ChargingState, LocalizationQuality, OperationalMode, OperationalState, PathState,
524        SafetyState,
525    };
526
527    fn test_config() -> PhyTraceConfig {
528        PhyTraceConfig::new("robot-001").with_source_type(SourceType::Amr)
529    }
530
531    #[test]
532    fn test_builder_basic() {
533        let event = EventBuilder::new(&test_config())
534            .position_2d(10.0, 20.0)
535            .build()
536            .unwrap();
537
538        assert_eq!(
539            event.identity.as_ref().unwrap().source_id,
540            Some("robot-001".to_string())
541        );
542        assert!(event.location.is_some());
543    }
544
545    #[test]
546    fn test_builder_all_helpers() {
547        let event = EventBuilder::new(&test_config())
548            .event_type(EventType::TelemetryPeriodic)
549            .position_2d(10.0, 20.0)
550            .heading_deg(90.0)
551            .speed(2.5)
552            .battery_soc(85.0)
553            .operational_mode(OperationalMode::Autonomous)
554            .safety_state(SafetyState::Normal)
555            .build()
556            .unwrap();
557
558        assert!(event.location.is_some());
559        assert!(event.motion.is_some());
560        assert!(event.power.is_some());
561        assert!(event.operational.is_some());
562        assert!(event.safety.is_some());
563    }
564
565    #[test]
566    fn test_builder_with_full_domain() {
567        let location = LocationDomain {
568            heading_deg: Some(180.0),
569            ..Default::default()
570        };
571
572        let event = EventBuilder::new(&test_config())
573            .location(location)
574            .build()
575            .unwrap();
576
577        let loc = event.location.unwrap();
578        assert_eq!(loc.heading_deg, Some(180.0));
579    }
580
581    #[test]
582    fn test_builder_empty_fails() {
583        let config = PhyTraceConfig::new("robot-001");
584        let mut builder = EventBuilder::new(&config);
585        // Clear the auto-populated identity
586        builder.event.identity = None;
587
588        let result = builder.build();
589        assert!(result.is_err());
590    }
591
592    // ==========================================================================
593    // Additional Builder Tests for Coverage
594    // ==========================================================================
595
596    #[test]
597    fn test_builder_clone() {
598        let builder = EventBuilder::new(&test_config()).position_2d(10.0, 20.0);
599        let cloned = builder.clone();
600
601        let event1 = builder.build().unwrap();
602        let event2 = cloned.build().unwrap();
603
604        assert_eq!(
605            event1
606                .location
607                .as_ref()
608                .unwrap()
609                .local
610                .as_ref()
611                .unwrap()
612                .x_m,
613            event2
614                .location
615                .as_ref()
616                .unwrap()
617                .local
618                .as_ref()
619                .unwrap()
620                .x_m
621        );
622    }
623
624    #[test]
625    fn test_builder_source_type_override() {
626        let event = EventBuilder::new(&test_config())
627            .source_type(SourceType::Drone)
628            .position_2d(0.0, 0.0)
629            .build()
630            .unwrap();
631
632        assert_eq!(event.source_type, SourceType::Drone);
633    }
634
635    #[test]
636    fn test_builder_model_info() {
637        let event = EventBuilder::new(&test_config())
638            .model_info("Manufacturer", "Model X", "1.2.3")
639            .position_2d(0.0, 0.0)
640            .build()
641            .unwrap();
642
643        let identity = event.identity.unwrap();
644        assert_eq!(identity.model, Some("Model X".to_string()));
645        assert_eq!(identity.firmware_version, Some("1.2.3".to_string()));
646    }
647
648    #[test]
649    fn test_builder_tag() {
650        let event = EventBuilder::new(&test_config())
651            .tag("warehouse")
652            .tag("zone-a")
653            .position_2d(0.0, 0.0)
654            .build()
655            .unwrap();
656
657        let identity = event.identity.unwrap();
658        let tags = identity.tags.unwrap();
659        assert!(tags.contains(&"warehouse".to_string()));
660        assert!(tags.contains(&"zone-a".to_string()));
661    }
662
663    #[test]
664    fn test_builder_position_local() {
665        let event = EventBuilder::new(&test_config())
666            .position_local(1.0, 2.0, 3.0)
667            .build()
668            .unwrap();
669
670        let local = event.location.unwrap().local.unwrap();
671        assert_eq!(local.x_m, Some(1.0));
672        assert_eq!(local.y_m, Some(2.0));
673        assert_eq!(local.z_m, Some(3.0));
674    }
675
676    #[test]
677    fn test_builder_position_3d() {
678        let event = EventBuilder::new(&test_config())
679            .position_3d(5.0, 6.0, 7.0)
680            .build()
681            .unwrap();
682
683        let local = event.location.unwrap().local.unwrap();
684        assert_eq!(local.x_m, Some(5.0));
685        assert_eq!(local.y_m, Some(6.0));
686        assert_eq!(local.z_m, Some(7.0));
687    }
688
689    #[test]
690    fn test_builder_heading() {
691        // Test heading in radians (should convert to degrees)
692        let event = EventBuilder::new(&test_config())
693            .heading(std::f64::consts::PI) // 180 degrees
694            .position_2d(0.0, 0.0)
695            .build()
696            .unwrap();
697
698        let heading = event.location.unwrap().heading_deg.unwrap();
699        assert!((heading - 180.0).abs() < 0.001);
700    }
701
702    #[test]
703    fn test_builder_gps() {
704        let event = EventBuilder::new(&test_config())
705            .gps(41.8781, -87.6298)
706            .build()
707            .unwrap();
708
709        let location = event.location.unwrap();
710        assert_eq!(location.latitude, Some(41.8781));
711        assert_eq!(location.longitude, Some(-87.6298));
712    }
713
714    #[test]
715    fn test_builder_floor() {
716        let event = EventBuilder::new(&test_config())
717            .floor(3)
718            .position_2d(0.0, 0.0)
719            .build()
720            .unwrap();
721
722        assert_eq!(event.location.unwrap().floor, Some(3));
723    }
724
725    #[test]
726    fn test_builder_map() {
727        let event = EventBuilder::new(&test_config())
728            .map("map_001", "base_link")
729            .position_2d(0.0, 0.0)
730            .build()
731            .unwrap();
732
733        let location = event.location.unwrap();
734        assert_eq!(location.map_id, Some("map_001".to_string()));
735        assert_eq!(location.frame_id, Some("base_link".to_string()));
736    }
737
738    #[test]
739    fn test_builder_linear_velocity() {
740        let event = EventBuilder::new(&test_config())
741            .linear_velocity(1.0, 0.5, 0.0)
742            .build()
743            .unwrap();
744
745        let lv = event.motion.unwrap().linear_velocity.unwrap();
746        assert_eq!(lv.x_mps, Some(1.0));
747        assert_eq!(lv.y_mps, Some(0.5));
748        assert_eq!(lv.z_mps, Some(0.0));
749    }
750
751    #[test]
752    fn test_builder_angular_velocity() {
753        let event = EventBuilder::new(&test_config())
754            .angular_velocity(0.0, 0.0, 1.0) // rad/s, should convert to deg/s
755            .build()
756            .unwrap();
757
758        let av = event.motion.unwrap().angular_velocity.unwrap();
759        assert!((av.yaw_dps.unwrap() - 57.2958).abs() < 0.1); // ~1 rad/s in deg/s
760    }
761
762    #[test]
763    fn test_builder_battery_voltage() {
764        let event = EventBuilder::new(&test_config())
765            .battery_voltage(24.5)
766            .build()
767            .unwrap();
768
769        let battery = event.power.unwrap().battery.unwrap();
770        assert_eq!(battery.voltage_v, Some(24.5));
771    }
772
773    #[test]
774    fn test_builder_battery_health() {
775        let event = EventBuilder::new(&test_config())
776            .battery_health(95.0)
777            .build()
778            .unwrap();
779
780        let battery = event.power.unwrap().battery.unwrap();
781        assert_eq!(battery.state_of_health_pct, Some(95.0));
782    }
783
784    #[test]
785    fn test_builder_charging() {
786        let event_charging = EventBuilder::new(&test_config())
787            .charging(true)
788            .build()
789            .unwrap();
790
791        let charging = event_charging.power.unwrap().charging.unwrap();
792        assert_eq!(charging.state, Some(ChargingState::Charging));
793
794        let event_not_charging = EventBuilder::new(&test_config())
795            .charging(false)
796            .build()
797            .unwrap();
798
799        let charging = event_not_charging.power.unwrap().charging.unwrap();
800        assert_eq!(charging.state, Some(ChargingState::NotCharging));
801    }
802
803    #[test]
804    fn test_builder_operational_state() {
805        let event = EventBuilder::new(&test_config())
806            .operational_state(OperationalState::ExecutingTask)
807            .build()
808            .unwrap();
809
810        assert_eq!(
811            event.operational.unwrap().state,
812            Some(OperationalState::ExecutingTask)
813        );
814    }
815
816    #[test]
817    fn test_builder_task() {
818        let event = EventBuilder::new(&test_config())
819            .task("task-001", "delivery")
820            .build()
821            .unwrap();
822
823        let task = event.operational.unwrap().task.unwrap();
824        assert_eq!(task.task_id, Some("task-001".to_string()));
825        assert_eq!(task.task_type, Some("delivery".to_string()));
826    }
827
828    #[test]
829    fn test_builder_estop_active() {
830        let event = EventBuilder::new(&test_config())
831            .estop_active(true)
832            .build()
833            .unwrap();
834
835        let estop = event.safety.unwrap().e_stop.unwrap();
836        assert_eq!(estop.is_active, Some(true));
837    }
838
839    #[test]
840    fn test_builder_localization_quality() {
841        let event = EventBuilder::new(&test_config())
842            .localization_quality(LocalizationQuality::Good)
843            .build()
844            .unwrap();
845
846        let loc = event.navigation.unwrap().localization.unwrap();
847        assert_eq!(loc.quality, Some(LocalizationQuality::Good));
848    }
849
850    #[test]
851    fn test_builder_path_state() {
852        let event = EventBuilder::new(&test_config())
853            .path_state(PathState::Executing)
854            .build()
855            .unwrap();
856
857        let path = event.navigation.unwrap().path.unwrap();
858        assert_eq!(path.state, Some(PathState::Executing));
859    }
860
861    #[test]
862    fn test_builder_all_domain_setters() {
863        let event = EventBuilder::new(&test_config())
864            .identity(IdentityDomain::default())
865            .location(LocationDomain::default())
866            .motion(MotionDomain::default())
867            .power(PowerDomain::default())
868            .operational(OperationalDomain::default())
869            .navigation(NavigationDomain::default())
870            .perception(PerceptionDomain::default())
871            .safety(SafetyDomain::default())
872            .actuators(ActuatorsDomain::default())
873            .communication(CommunicationDomain::default())
874            .compute(ComputeDomain::default())
875            .ai(AiDomain::default())
876            .maintenance(MaintenanceDomain::default())
877            .context(ContextDomain::default())
878            .payload(PayloadDomain::default())
879            .manipulation(ManipulationDomain::default())
880            .hri(HriDomain::default())
881            .coordination(CoordinationDomain::default())
882            .simulation(SimulationDomain::default())
883            .thermal(ThermalDomain::default())
884            .audio(AudioDomain::default())
885            .environment_interaction(EnvironmentInteractionDomain::default())
886            .compliance(ComplianceDomain::default())
887            .build()
888            .unwrap();
889
890        assert!(event.identity.is_some());
891        assert!(event.location.is_some());
892        assert!(event.motion.is_some());
893        assert!(event.power.is_some());
894        assert!(event.operational.is_some());
895        assert!(event.navigation.is_some());
896        assert!(event.perception.is_some());
897        assert!(event.safety.is_some());
898        assert!(event.actuators.is_some());
899        assert!(event.communication.is_some());
900        assert!(event.compute.is_some());
901        assert!(event.ai.is_some());
902        assert!(event.maintenance.is_some());
903        assert!(event.context.is_some());
904        assert!(event.payload.is_some());
905        assert!(event.manipulation.is_some());
906        assert!(event.hri.is_some());
907        assert!(event.coordination.is_some());
908        assert!(event.simulation.is_some());
909        assert!(event.thermal.is_some());
910        assert!(event.audio.is_some());
911        assert!(event.environment_interaction.is_some());
912        assert!(event.compliance.is_some());
913    }
914
915    #[test]
916    fn test_builder_extensions() {
917        let extensions = serde_json::json!({
918            "custom_field": "custom_value",
919            "nested": { "key": 123 }
920        });
921
922        let event = EventBuilder::new(&test_config())
923            .extensions(extensions.clone())
924            .position_2d(0.0, 0.0)
925            .build()
926            .unwrap();
927
928        assert_eq!(event.extensions, Some(extensions));
929    }
930
931    #[test]
932    fn test_builder_build_unchecked() {
933        let config = PhyTraceConfig::new("robot-001");
934        let mut builder = EventBuilder::new(&config);
935        builder.event.identity = None;
936
937        // build_unchecked should succeed even without domain data
938        let event = builder.build_unchecked();
939        assert!(event.identity.is_none());
940    }
941
942    #[test]
943    fn test_builder_peek() {
944        let builder = EventBuilder::new(&test_config()).position_2d(10.0, 20.0);
945
946        let event = builder.peek();
947        assert!(event.location.is_some());
948    }
949
950    #[test]
951    fn test_builder_config_with_tags() {
952        // Test that tags from config are passed through
953        let config = PhyTraceConfig::new("robot-001").with_source_type(SourceType::Amr);
954
955        let event = EventBuilder::new(&config)
956            .tag("test-tag")
957            .position_2d(0.0, 0.0)
958            .build()
959            .unwrap();
960
961        let identity = event.identity.unwrap();
962        assert!(identity.tags.is_some());
963        assert!(identity.tags.unwrap().contains(&"test-tag".to_string()));
964    }
965}