1use serde::{Deserialize, Serialize};
7use std::path::Path;
8use std::time::Duration;
9
10use crate::core::license::DEFAULT_GRACE_PERIOD_HOURS;
11use crate::error::{ConfigError, PhyTraceResult};
12use crate::models::enums::SourceType;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct PhyTraceConfig {
17 pub source: SourceConfig,
19
20 #[serde(default)]
22 pub transport: TransportConfig,
23
24 #[serde(default)]
26 pub license: LicenseConfig,
27
28 #[serde(default)]
30 pub buffer: BufferConfig,
31
32 #[serde(default)]
34 pub retry: RetryConfig,
35
36 #[serde(default)]
38 pub provenance: ProvenanceConfig,
39
40 #[serde(default)]
42 pub emitter: EmitterConfig,
43
44 #[serde(default)]
46 pub logging: LoggingConfig,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct SourceConfig {
52 pub source_id: String,
54
55 #[serde(default)]
57 pub source_type: SourceType,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub fleet_id: Option<String>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub site_id: Option<String>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub organization_id: Option<String>,
70
71 #[serde(default)]
73 pub tags: Vec<String>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct TransportConfig {
79 #[serde(default = "default_transport_type")]
81 pub transport_type: String,
82
83 #[serde(default)]
85 pub endpoint: String,
86
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub api_key: Option<String>,
90
91 #[serde(default = "default_connect_timeout")]
93 pub connect_timeout_secs: u64,
94
95 #[serde(default = "default_request_timeout")]
97 pub request_timeout_secs: u64,
98
99 #[serde(default = "default_true")]
101 pub tls_verify: bool,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub ca_cert_path: Option<String>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct LicenseConfig {
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub token: Option<String>,
114
115 #[serde(default = "default_grace_period")]
117 pub grace_period_hours: i64,
118}
119
120impl Default for LicenseConfig {
121 fn default() -> Self {
122 Self {
123 token: None,
124 grace_period_hours: DEFAULT_GRACE_PERIOD_HOURS,
125 }
126 }
127}
128
129fn default_grace_period() -> i64 {
130 DEFAULT_GRACE_PERIOD_HOURS
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct BufferConfig {
136 #[serde(default = "default_true")]
138 pub enabled: bool,
139
140 #[serde(default = "default_buffer_path")]
142 pub path: String,
143
144 #[serde(default = "default_max_buffer_size")]
146 pub max_size_bytes: u64,
147
148 #[serde(default = "default_max_age")]
150 pub max_age_secs: u64,
151
152 #[serde(default = "default_flush_interval")]
154 pub flush_interval_secs: u64,
155
156 #[serde(default = "default_batch_size")]
158 pub batch_size: usize,
159
160 #[serde(default = "default_true")]
162 pub compress: bool,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct RetryConfig {
168 #[serde(default = "default_max_retries")]
170 pub max_retries: u32,
171
172 #[serde(default = "default_initial_backoff")]
174 pub initial_backoff_ms: u64,
175
176 #[serde(default = "default_max_backoff")]
178 pub max_backoff_ms: u64,
179
180 #[serde(default = "default_backoff_multiplier")]
182 pub backoff_multiplier: f64,
183
184 #[serde(default = "default_true")]
186 pub jitter: bool,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct ProvenanceConfig {
192 #[serde(default)]
194 pub enabled: bool,
195
196 #[serde(default = "default_algorithm")]
198 pub algorithm: String,
199
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub key_id: Option<String>,
203
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub secret_key: Option<String>,
207
208 #[serde(default = "default_signed_fields")]
210 pub signed_fields: Vec<String>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct EmitterConfig {
216 #[serde(default = "default_emit_interval")]
218 pub interval_ms: u64,
219
220 #[serde(default = "default_true")]
222 pub on_change_enabled: bool,
223
224 #[serde(default = "default_change_threshold")]
226 pub change_threshold: f64,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct LoggingConfig {
232 #[serde(default = "default_log_level")]
234 pub level: String,
235
236 #[serde(default)]
238 pub structured: bool,
239}
240
241fn default_transport_type() -> String {
243 "http".to_string()
244}
245
246fn default_connect_timeout() -> u64 {
247 10
248}
249
250fn default_request_timeout() -> u64 {
251 30
252}
253
254fn default_true() -> bool {
255 true
256}
257
258fn default_buffer_path() -> String {
259 "./phytrace_buffer".to_string()
260}
261
262fn default_max_buffer_size() -> u64 {
263 100 * 1024 * 1024 }
265
266fn default_max_age() -> u64 {
267 86400 }
269
270fn default_flush_interval() -> u64 {
271 5
272}
273
274fn default_batch_size() -> usize {
275 100
276}
277
278fn default_max_retries() -> u32 {
279 5
280}
281
282fn default_initial_backoff() -> u64 {
283 100
284}
285
286fn default_max_backoff() -> u64 {
287 30000
288}
289
290fn default_backoff_multiplier() -> f64 {
291 2.0
292}
293
294fn default_algorithm() -> String {
295 "hmac-sha256".to_string()
296}
297
298fn default_signed_fields() -> Vec<String> {
299 vec![
300 "event_id".to_string(),
301 "timestamp".to_string(),
302 "source_type".to_string(),
303 "identity".to_string(),
304 ]
305}
306
307fn default_emit_interval() -> u64 {
308 1000
309}
310
311fn default_change_threshold() -> f64 {
312 0.01
313}
314
315fn default_log_level() -> String {
316 "info".to_string()
317}
318
319impl Default for TransportConfig {
320 fn default() -> Self {
321 Self {
322 transport_type: default_transport_type(),
323 endpoint: String::new(),
324 api_key: None,
325 connect_timeout_secs: default_connect_timeout(),
326 request_timeout_secs: default_request_timeout(),
327 tls_verify: true,
328 ca_cert_path: None,
329 }
330 }
331}
332
333impl Default for BufferConfig {
334 fn default() -> Self {
335 Self {
336 enabled: true,
337 path: default_buffer_path(),
338 max_size_bytes: default_max_buffer_size(),
339 max_age_secs: default_max_age(),
340 flush_interval_secs: default_flush_interval(),
341 batch_size: default_batch_size(),
342 compress: true,
343 }
344 }
345}
346
347impl Default for RetryConfig {
348 fn default() -> Self {
349 Self {
350 max_retries: default_max_retries(),
351 initial_backoff_ms: default_initial_backoff(),
352 max_backoff_ms: default_max_backoff(),
353 backoff_multiplier: default_backoff_multiplier(),
354 jitter: true,
355 }
356 }
357}
358
359impl Default for ProvenanceConfig {
360 fn default() -> Self {
361 Self {
362 enabled: false,
363 algorithm: default_algorithm(),
364 key_id: None,
365 secret_key: None,
366 signed_fields: default_signed_fields(),
367 }
368 }
369}
370
371impl Default for EmitterConfig {
372 fn default() -> Self {
373 Self {
374 interval_ms: default_emit_interval(),
375 on_change_enabled: true,
376 change_threshold: default_change_threshold(),
377 }
378 }
379}
380
381impl Default for LoggingConfig {
382 fn default() -> Self {
383 Self {
384 level: default_log_level(),
385 structured: false,
386 }
387 }
388}
389
390impl PhyTraceConfig {
391 pub fn new(source_id: impl Into<String>) -> Self {
393 Self {
394 source: SourceConfig {
395 source_id: source_id.into(),
396 source_type: SourceType::default(),
397 fleet_id: None,
398 site_id: None,
399 organization_id: None,
400 tags: Vec::new(),
401 },
402 transport: TransportConfig::default(),
403 license: LicenseConfig::default(),
404 buffer: BufferConfig::default(),
405 retry: RetryConfig::default(),
406 provenance: ProvenanceConfig::default(),
407 emitter: EmitterConfig::default(),
408 logging: LoggingConfig::default(),
409 }
410 }
411
412 pub fn from_file<P: AsRef<Path>>(path: P) -> PhyTraceResult<Self> {
414 let content = std::fs::read_to_string(path.as_ref())
415 .map_err(|e| ConfigError::FileRead(e.to_string()))?;
416 Self::from_yaml(&content)
417 }
418
419 pub fn from_yaml(yaml: &str) -> PhyTraceResult<Self> {
421 serde_yaml::from_str(yaml).map_err(|e| ConfigError::Parse(e.to_string()).into())
422 }
423
424 pub fn to_yaml(&self) -> PhyTraceResult<String> {
426 serde_yaml::to_string(self).map_err(|e| ConfigError::Parse(e.to_string()).into())
427 }
428
429 pub fn with_env_overrides(mut self) -> Self {
437 if let Ok(source_id) = std::env::var("PHYTRACE_SOURCE_ID") {
438 self.source.source_id = source_id;
439 }
440 if let Ok(endpoint) = std::env::var("PHYTRACE_ENDPOINT") {
441 self.transport.endpoint = endpoint;
442 }
443 if let Ok(api_key) = std::env::var("PHYTRACE_API_KEY") {
444 self.transport.api_key = Some(api_key);
445 }
446 if let Ok(buffer_path) = std::env::var("PHYTRACE_BUFFER_PATH") {
447 self.buffer.path = buffer_path;
448 }
449 self
450 }
451
452 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
454 self.transport.endpoint = endpoint.into();
455 self
456 }
457
458 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
460 self.transport.api_key = Some(api_key.into());
461 self
462 }
463
464 pub fn with_source_type(mut self, source_type: SourceType) -> Self {
466 self.source.source_type = source_type;
467 self
468 }
469
470 pub fn with_signing(
472 mut self,
473 key_id: impl Into<String>,
474 secret_key: impl Into<String>,
475 ) -> Self {
476 self.provenance.enabled = true;
477 self.provenance.key_id = Some(key_id.into());
478 self.provenance.secret_key = Some(secret_key.into());
479 self
480 }
481
482 pub fn validate(&self) -> Result<(), ConfigError> {
484 if self.source.source_id.is_empty() {
485 return Err(ConfigError::Validation("source_id is required".to_string()));
486 }
487 if self.transport.endpoint.is_empty() && self.transport.transport_type != "mock" {
488 return Err(ConfigError::Validation(
489 "endpoint is required for non-mock transport".to_string(),
490 ));
491 }
492 if self.provenance.enabled && self.provenance.secret_key.is_none() {
493 return Err(ConfigError::Validation(
494 "secret_key is required when provenance is enabled".to_string(),
495 ));
496 }
497 Ok(())
498 }
499
500 pub fn connect_timeout(&self) -> Duration {
502 Duration::from_secs(self.transport.connect_timeout_secs)
503 }
504
505 pub fn request_timeout(&self) -> Duration {
507 Duration::from_secs(self.transport.request_timeout_secs)
508 }
509
510 pub fn emit_interval(&self) -> Duration {
512 Duration::from_millis(self.emitter.interval_ms)
513 }
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn test_config_creation() {
522 let config = PhyTraceConfig::new("robot-001");
523 assert_eq!(config.source.source_id, "robot-001");
524 assert_eq!(config.source.source_type, SourceType::Amr);
525 }
526
527 #[test]
528 fn test_config_builder() {
529 let config = PhyTraceConfig::new("robot-001")
530 .with_endpoint("https://api.phyware.io")
531 .with_api_key("test-key")
532 .with_source_type(SourceType::DeliveryRobot);
533
534 assert_eq!(config.transport.endpoint, "https://api.phyware.io");
535 assert_eq!(config.transport.api_key, Some("test-key".to_string()));
536 assert_eq!(config.source.source_type, SourceType::DeliveryRobot);
537 }
538
539 #[test]
540 fn test_config_yaml_roundtrip() {
541 let config = PhyTraceConfig::new("robot-001").with_endpoint("https://api.phyware.io");
542
543 let yaml = config.to_yaml().unwrap();
544 let parsed = PhyTraceConfig::from_yaml(&yaml).unwrap();
545
546 assert_eq!(parsed.source.source_id, "robot-001");
547 assert_eq!(parsed.transport.endpoint, "https://api.phyware.io");
548 }
549
550 #[test]
551 fn test_config_validation() {
552 let config = PhyTraceConfig {
554 source: SourceConfig {
555 source_id: String::new(),
556 source_type: SourceType::Amr,
557 fleet_id: None,
558 site_id: None,
559 organization_id: None,
560 tags: Vec::new(),
561 },
562 transport: TransportConfig::default(),
563 license: LicenseConfig::default(),
564 buffer: BufferConfig::default(),
565 retry: RetryConfig::default(),
566 provenance: ProvenanceConfig::default(),
567 emitter: EmitterConfig::default(),
568 logging: LoggingConfig::default(),
569 };
570
571 assert!(config.validate().is_err());
572 }
573
574 #[test]
575 fn test_default_values() {
576 let config = PhyTraceConfig::new("test");
577
578 assert_eq!(config.retry.max_retries, 5);
579 assert_eq!(config.buffer.batch_size, 100);
580 assert!(config.buffer.enabled);
581 assert!(!config.provenance.enabled);
582 }
583}