phytrace_sdk/models/domains/
context.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct ContextDomain {
9 #[serde(skip_serializing_if = "Option::is_none")]
11 pub time: Option<TimeContext>,
12
13 #[serde(skip_serializing_if = "Option::is_none")]
15 pub facility: Option<FacilityContext>,
16
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub weather: Option<WeatherContext>,
20
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub traffic: Option<TrafficContext>,
24}
25
26#[derive(Debug, Clone, Default, Serialize, Deserialize)]
28pub struct TimeContext {
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub timezone: Option<String>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub local_time: Option<DateTime<Utc>>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub is_work_day: Option<bool>,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub shift: Option<String>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub is_peak_hours: Option<bool>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub minutes_until_shift_change: Option<i32>,
52}
53
54#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct FacilityContext {
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub facility_id: Option<String>,
60
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub name: Option<String>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub facility_type: Option<String>,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub operating_status: Option<String>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub occupancy_pct: Option<f64>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub human_count: Option<u32>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub robot_count: Option<u32>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub production_rate: Option<f64>,
88}
89
90#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct WeatherContext {
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub temperature_c: Option<f64>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub humidity_pct: Option<f64>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub condition: Option<String>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub wind_speed_mps: Option<f64>,
108
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub visibility_m: Option<f64>,
112
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub affects_operation: Option<bool>,
116}
117
118#[derive(Debug, Clone, Default, Serialize, Deserialize)]
120pub struct TrafficContext {
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub level: Option<String>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub congestion_score: Option<f64>,
128
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub active_vehicles: Option<u32>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub blocked_paths: Option<u32>,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub avg_speed_mps: Option<f64>,
140}
141
142impl ContextDomain {
143 pub fn with_facility(facility: FacilityContext) -> Self {
145 Self {
146 facility: Some(facility),
147 ..Default::default()
148 }
149 }
150
151 pub fn with_time(mut self, time: TimeContext) -> Self {
153 self.time = Some(time);
154 self
155 }
156
157 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 #[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 #[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 #[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 #[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 #[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 #[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}