phytrace_sdk/models/domains/
context.rs

1//! Context Domain - Time, facility, weather, and traffic context.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// Context domain containing environmental and temporal context.
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct ContextDomain {
9    /// Time context
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub time: Option<TimeContext>,
12
13    /// Facility context
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub facility: Option<FacilityContext>,
16
17    /// Weather context
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub weather: Option<WeatherContext>,
20
21    /// Traffic context
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub traffic: Option<TrafficContext>,
24}
25
26/// Time-related context.
27#[derive(Debug, Clone, Default, Serialize, Deserialize)]
28pub struct TimeContext {
29    /// Local timezone
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub timezone: Option<String>,
32
33    /// Local time
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub local_time: Option<DateTime<Utc>>,
36
37    /// Whether it's a work day
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub is_work_day: Option<bool>,
40
41    /// Shift name (e.g., "day", "night", "swing")
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub shift: Option<String>,
44
45    /// Whether facility is in peak hours
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub is_peak_hours: Option<bool>,
48
49    /// Time until shift change (minutes)
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub minutes_until_shift_change: Option<i32>,
52}
53
54/// Facility-related context.
55#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct FacilityContext {
57    /// Facility ID
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub facility_id: Option<String>,
60
61    /// Facility name
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub name: Option<String>,
64
65    /// Facility type
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub facility_type: Option<String>,
68
69    /// Operating status
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub operating_status: Option<String>,
72
73    /// Occupancy level (percentage)
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub occupancy_pct: Option<f64>,
76
77    /// Number of humans in facility
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub human_count: Option<u32>,
80
81    /// Number of active robots
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub robot_count: Option<u32>,
84
85    /// Current production rate
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub production_rate: Option<f64>,
88}
89
90/// Weather-related context.
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct WeatherContext {
93    /// Temperature (Celsius)
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub temperature_c: Option<f64>,
96
97    /// Humidity (percentage)
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub humidity_pct: Option<f64>,
100
101    /// Weather condition
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub condition: Option<String>,
104
105    /// Wind speed (m/s)
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub wind_speed_mps: Option<f64>,
108
109    /// Visibility (meters)
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub visibility_m: Option<f64>,
112
113    /// Whether conditions affect outdoor operation
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub affects_operation: Option<bool>,
116}
117
118/// Traffic-related context (for facility/warehouse traffic).
119#[derive(Debug, Clone, Default, Serialize, Deserialize)]
120pub struct TrafficContext {
121    /// Current traffic level (low, medium, high)
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub level: Option<String>,
124
125    /// Congestion score (0-100)
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub congestion_score: Option<f64>,
128
129    /// Active vehicles/robots in area
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub active_vehicles: Option<u32>,
132
133    /// Blocked paths count
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub blocked_paths: Option<u32>,
136
137    /// Average speed in area (m/s)
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub avg_speed_mps: Option<f64>,
140}
141
142impl ContextDomain {
143    /// Create with facility context.
144    pub fn with_facility(facility: FacilityContext) -> Self {
145        Self {
146            facility: Some(facility),
147            ..Default::default()
148        }
149    }
150
151    /// Builder to add time context.
152    pub fn with_time(mut self, time: TimeContext) -> Self {
153        self.time = Some(time);
154        self
155    }
156
157    /// Builder to add weather context.
158    pub fn with_weather(mut self, weather: WeatherContext) -> Self {
159        self.weather = Some(weather);
160        self
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_context_domain() {
170        let ctx = ContextDomain::with_facility(FacilityContext {
171            name: Some("Warehouse East".into()),
172            ..Default::default()
173        });
174        assert!(ctx.facility.is_some());
175    }
176
177    // ==========================================================================
178    // ContextDomain Tests
179    // ==========================================================================
180
181    #[test]
182    fn test_context_domain_default() {
183        let ctx = ContextDomain::default();
184        assert!(ctx.time.is_none());
185        assert!(ctx.facility.is_none());
186        assert!(ctx.weather.is_none());
187        assert!(ctx.traffic.is_none());
188    }
189
190    #[test]
191    fn test_context_domain_with_facility() {
192        let facility = FacilityContext {
193            facility_id: Some("fac-001".to_string()),
194            name: Some("Main Warehouse".to_string()),
195            facility_type: Some("warehouse".to_string()),
196            ..Default::default()
197        };
198
199        let ctx = ContextDomain::with_facility(facility);
200        assert!(ctx.facility.is_some());
201        assert_eq!(
202            ctx.facility.as_ref().unwrap().name,
203            Some("Main Warehouse".to_string())
204        );
205    }
206
207    #[test]
208    fn test_context_domain_with_time() {
209        let time = TimeContext {
210            timezone: Some("America/New_York".to_string()),
211            is_work_day: Some(true),
212            shift: Some("day".to_string()),
213            ..Default::default()
214        };
215
216        let ctx = ContextDomain::default().with_time(time);
217        assert!(ctx.time.is_some());
218        assert_eq!(ctx.time.as_ref().unwrap().shift, Some("day".to_string()));
219    }
220
221    #[test]
222    fn test_context_domain_with_weather() {
223        let weather = WeatherContext {
224            temperature_c: Some(22.5),
225            humidity_pct: Some(65.0),
226            condition: Some("Partly Cloudy".to_string()),
227            ..Default::default()
228        };
229
230        let ctx = ContextDomain::default().with_weather(weather);
231        assert!(ctx.weather.is_some());
232        assert_eq!(ctx.weather.as_ref().unwrap().temperature_c, Some(22.5));
233    }
234
235    #[test]
236    fn test_context_domain_chained_builders() {
237        let ctx = ContextDomain::with_facility(FacilityContext::default())
238            .with_time(TimeContext::default())
239            .with_weather(WeatherContext::default());
240
241        assert!(ctx.facility.is_some());
242        assert!(ctx.time.is_some());
243        assert!(ctx.weather.is_some());
244    }
245
246    // ==========================================================================
247    // TimeContext Tests
248    // ==========================================================================
249
250    #[test]
251    fn test_time_context_default() {
252        let time = TimeContext::default();
253        assert!(time.timezone.is_none());
254        assert!(time.local_time.is_none());
255        assert!(time.is_work_day.is_none());
256        assert!(time.shift.is_none());
257        assert!(time.is_peak_hours.is_none());
258        assert!(time.minutes_until_shift_change.is_none());
259    }
260
261    #[test]
262    fn test_time_context_full() {
263        let time = TimeContext {
264            timezone: Some("UTC".to_string()),
265            local_time: Some(Utc::now()),
266            is_work_day: Some(true),
267            shift: Some("night".to_string()),
268            is_peak_hours: Some(false),
269            minutes_until_shift_change: Some(120),
270        };
271
272        assert_eq!(time.timezone, Some("UTC".to_string()));
273        assert_eq!(time.shift, Some("night".to_string()));
274        assert_eq!(time.minutes_until_shift_change, Some(120));
275    }
276
277    #[test]
278    fn test_time_context_serialization() {
279        let time = TimeContext {
280            timezone: Some("Europe/London".to_string()),
281            is_work_day: Some(true),
282            ..Default::default()
283        };
284
285        let json = serde_json::to_string(&time).unwrap();
286        let deserialized: TimeContext = serde_json::from_str(&json).unwrap();
287        assert_eq!(deserialized.timezone, Some("Europe/London".to_string()));
288    }
289
290    // ==========================================================================
291    // FacilityContext Tests
292    // ==========================================================================
293
294    #[test]
295    fn test_facility_context_default() {
296        let facility = FacilityContext::default();
297        assert!(facility.facility_id.is_none());
298        assert!(facility.name.is_none());
299        assert!(facility.facility_type.is_none());
300        assert!(facility.operating_status.is_none());
301        assert!(facility.occupancy_pct.is_none());
302        assert!(facility.human_count.is_none());
303        assert!(facility.robot_count.is_none());
304        assert!(facility.production_rate.is_none());
305    }
306
307    #[test]
308    fn test_facility_context_full() {
309        let facility = FacilityContext {
310            facility_id: Some("fac-123".to_string()),
311            name: Some("Distribution Center A".to_string()),
312            facility_type: Some("distribution".to_string()),
313            operating_status: Some("operational".to_string()),
314            occupancy_pct: Some(75.5),
315            human_count: Some(50),
316            robot_count: Some(20),
317            production_rate: Some(1500.0),
318        };
319
320        assert_eq!(facility.occupancy_pct, Some(75.5));
321        assert_eq!(facility.human_count, Some(50));
322        assert_eq!(facility.robot_count, Some(20));
323    }
324
325    #[test]
326    fn test_facility_context_serialization() {
327        let facility = FacilityContext {
328            name: Some("Test Facility".to_string()),
329            robot_count: Some(10),
330            ..Default::default()
331        };
332
333        let json = serde_json::to_string(&facility).unwrap();
334        let deserialized: FacilityContext = serde_json::from_str(&json).unwrap();
335        assert_eq!(deserialized.robot_count, Some(10));
336    }
337
338    // ==========================================================================
339    // WeatherContext Tests
340    // ==========================================================================
341
342    #[test]
343    fn test_weather_context_default() {
344        let weather = WeatherContext::default();
345        assert!(weather.temperature_c.is_none());
346        assert!(weather.humidity_pct.is_none());
347        assert!(weather.condition.is_none());
348        assert!(weather.wind_speed_mps.is_none());
349        assert!(weather.visibility_m.is_none());
350        assert!(weather.affects_operation.is_none());
351    }
352
353    #[test]
354    fn test_weather_context_full() {
355        let weather = WeatherContext {
356            temperature_c: Some(-5.0),
357            humidity_pct: Some(85.0),
358            condition: Some("Snow".to_string()),
359            wind_speed_mps: Some(8.5),
360            visibility_m: Some(500.0),
361            affects_operation: Some(true),
362        };
363
364        assert_eq!(weather.temperature_c, Some(-5.0));
365        assert_eq!(weather.wind_speed_mps, Some(8.5));
366        assert_eq!(weather.affects_operation, Some(true));
367    }
368
369    #[test]
370    fn test_weather_context_serialization() {
371        let weather = WeatherContext {
372            condition: Some("Sunny".to_string()),
373            temperature_c: Some(28.0),
374            ..Default::default()
375        };
376
377        let json = serde_json::to_string(&weather).unwrap();
378        let deserialized: WeatherContext = serde_json::from_str(&json).unwrap();
379        assert_eq!(deserialized.condition, Some("Sunny".to_string()));
380    }
381
382    // ==========================================================================
383    // TrafficContext Tests
384    // ==========================================================================
385
386    #[test]
387    fn test_traffic_context_default() {
388        let traffic = TrafficContext::default();
389        assert!(traffic.level.is_none());
390        assert!(traffic.congestion_score.is_none());
391        assert!(traffic.active_vehicles.is_none());
392        assert!(traffic.blocked_paths.is_none());
393        assert!(traffic.avg_speed_mps.is_none());
394    }
395
396    #[test]
397    fn test_traffic_context_full() {
398        let traffic = TrafficContext {
399            level: Some("high".to_string()),
400            congestion_score: Some(85.0),
401            active_vehicles: Some(15),
402            blocked_paths: Some(2),
403            avg_speed_mps: Some(0.5),
404        };
405
406        assert_eq!(traffic.level, Some("high".to_string()));
407        assert_eq!(traffic.congestion_score, Some(85.0));
408        assert_eq!(traffic.blocked_paths, Some(2));
409    }
410
411    #[test]
412    fn test_traffic_context_serialization() {
413        let traffic = TrafficContext {
414            level: Some("medium".to_string()),
415            active_vehicles: Some(8),
416            ..Default::default()
417        };
418
419        let json = serde_json::to_string(&traffic).unwrap();
420        let deserialized: TrafficContext = serde_json::from_str(&json).unwrap();
421        assert_eq!(deserialized.active_vehicles, Some(8));
422    }
423
424    // ==========================================================================
425    // ContextDomain Serialization Tests
426    // ==========================================================================
427
428    #[test]
429    fn test_context_domain_serialization_roundtrip() {
430        let ctx = ContextDomain {
431            time: Some(TimeContext {
432                timezone: Some("UTC".to_string()),
433                ..Default::default()
434            }),
435            facility: Some(FacilityContext {
436                name: Some("Test".to_string()),
437                ..Default::default()
438            }),
439            weather: Some(WeatherContext {
440                temperature_c: Some(20.0),
441                ..Default::default()
442            }),
443            traffic: Some(TrafficContext {
444                level: Some("low".to_string()),
445                ..Default::default()
446            }),
447        };
448
449        let json = serde_json::to_string(&ctx).unwrap();
450        let deserialized: ContextDomain = serde_json::from_str(&json).unwrap();
451
452        assert!(deserialized.time.is_some());
453        assert!(deserialized.facility.is_some());
454        assert!(deserialized.weather.is_some());
455        assert!(deserialized.traffic.is_some());
456    }
457}