phytrace_sdk/core/
config.rs

1//! Configuration management for PhyTrace SDK.
2//!
3//! Supports loading configuration from YAML files, environment variables,
4//! or programmatic construction.
5
6use 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/// Main SDK configuration.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct PhyTraceConfig {
17    /// Source configuration.
18    pub source: SourceConfig,
19
20    /// Transport configuration.
21    #[serde(default)]
22    pub transport: TransportConfig,
23
24    /// License configuration.
25    #[serde(default)]
26    pub license: LicenseConfig,
27
28    /// Buffer configuration.
29    #[serde(default)]
30    pub buffer: BufferConfig,
31
32    /// Retry configuration.
33    #[serde(default)]
34    pub retry: RetryConfig,
35
36    /// Provenance configuration.
37    #[serde(default)]
38    pub provenance: ProvenanceConfig,
39
40    /// Emitter configuration.
41    #[serde(default)]
42    pub emitter: EmitterConfig,
43
44    /// Logging configuration.
45    #[serde(default)]
46    pub logging: LoggingConfig,
47}
48
49/// Source identification configuration.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct SourceConfig {
52    /// Unique source identifier (e.g., robot ID).
53    pub source_id: String,
54
55    /// Type of source.
56    #[serde(default)]
57    pub source_type: SourceType,
58
59    /// Fleet identifier (optional).
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub fleet_id: Option<String>,
62
63    /// Site identifier (optional).
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub site_id: Option<String>,
66
67    /// Organization identifier (optional).
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub organization_id: Option<String>,
70
71    /// Additional tags.
72    #[serde(default)]
73    pub tags: Vec<String>,
74}
75
76/// Transport layer configuration.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct TransportConfig {
79    /// Transport type (http, grpc, mqtt, etc.).
80    #[serde(default = "default_transport_type")]
81    pub transport_type: String,
82
83    /// Base URL for the API endpoint.
84    #[serde(default)]
85    pub endpoint: String,
86
87    /// API key for authentication.
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub api_key: Option<String>,
90
91    /// Connection timeout in seconds.
92    #[serde(default = "default_connect_timeout")]
93    pub connect_timeout_secs: u64,
94
95    /// Request timeout in seconds.
96    #[serde(default = "default_request_timeout")]
97    pub request_timeout_secs: u64,
98
99    /// Enable TLS verification.
100    #[serde(default = "default_true")]
101    pub tls_verify: bool,
102
103    /// Custom CA certificate path (optional).
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub ca_cert_path: Option<String>,
106}
107
108/// License configuration.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct LicenseConfig {
111    /// JWT license token from PhyWare customer portal.
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub token: Option<String>,
114
115    /// Grace period in hours (default 72).
116    #[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/// Buffer configuration for offline/reliability.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct BufferConfig {
136    /// Enable buffering.
137    #[serde(default = "default_true")]
138    pub enabled: bool,
139
140    /// Buffer directory path.
141    #[serde(default = "default_buffer_path")]
142    pub path: String,
143
144    /// Maximum buffer size in bytes.
145    #[serde(default = "default_max_buffer_size")]
146    pub max_size_bytes: u64,
147
148    /// Maximum age of buffered events in seconds.
149    #[serde(default = "default_max_age")]
150    pub max_age_secs: u64,
151
152    /// Flush interval in seconds.
153    #[serde(default = "default_flush_interval")]
154    pub flush_interval_secs: u64,
155
156    /// Maximum events per batch.
157    #[serde(default = "default_batch_size")]
158    pub batch_size: usize,
159
160    /// Enable compression for buffered data.
161    #[serde(default = "default_true")]
162    pub compress: bool,
163}
164
165/// Retry configuration.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct RetryConfig {
168    /// Maximum retry attempts.
169    #[serde(default = "default_max_retries")]
170    pub max_retries: u32,
171
172    /// Initial backoff delay in milliseconds.
173    #[serde(default = "default_initial_backoff")]
174    pub initial_backoff_ms: u64,
175
176    /// Maximum backoff delay in milliseconds.
177    #[serde(default = "default_max_backoff")]
178    pub max_backoff_ms: u64,
179
180    /// Backoff multiplier.
181    #[serde(default = "default_backoff_multiplier")]
182    pub backoff_multiplier: f64,
183
184    /// Add jitter to backoff.
185    #[serde(default = "default_true")]
186    pub jitter: bool,
187}
188
189/// Provenance (signing) configuration.
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct ProvenanceConfig {
192    /// Enable event signing.
193    #[serde(default)]
194    pub enabled: bool,
195
196    /// Signing algorithm.
197    #[serde(default = "default_algorithm")]
198    pub algorithm: String,
199
200    /// Key ID.
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub key_id: Option<String>,
203
204    /// Secret key (base64-encoded for HMAC).
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub secret_key: Option<String>,
207
208    /// Fields to include in signature.
209    #[serde(default = "default_signed_fields")]
210    pub signed_fields: Vec<String>,
211}
212
213/// Emitter configuration.
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct EmitterConfig {
216    /// Default emission interval in milliseconds.
217    #[serde(default = "default_emit_interval")]
218    pub interval_ms: u64,
219
220    /// Enable on-change detection.
221    #[serde(default = "default_true")]
222    pub on_change_enabled: bool,
223
224    /// Minimum change threshold for on-change emission.
225    #[serde(default = "default_change_threshold")]
226    pub change_threshold: f64,
227}
228
229/// Logging configuration.
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct LoggingConfig {
232    /// Log level (trace, debug, info, warn, error).
233    #[serde(default = "default_log_level")]
234    pub level: String,
235
236    /// Enable structured logging.
237    #[serde(default)]
238    pub structured: bool,
239}
240
241// Default value functions
242fn 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 // 100 MB
264}
265
266fn default_max_age() -> u64 {
267    86400 // 24 hours
268}
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    /// Create a minimal configuration with required fields.
392    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    /// Load configuration from a YAML file.
413    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    /// Parse configuration from YAML string.
420    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    /// Serialize configuration to YAML string.
425    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    /// Apply environment variable overrides.
430    ///
431    /// Supported environment variables:
432    /// - PHYTRACE_SOURCE_ID
433    /// - PHYTRACE_ENDPOINT
434    /// - PHYTRACE_API_KEY
435    /// - PHYTRACE_BUFFER_PATH
436    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    /// Set the transport endpoint.
453    pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
454        self.transport.endpoint = endpoint.into();
455        self
456    }
457
458    /// Set the API key.
459    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    /// Set the source type.
465    pub fn with_source_type(mut self, source_type: SourceType) -> Self {
466        self.source.source_type = source_type;
467        self
468    }
469
470    /// Enable provenance signing with a secret key.
471    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    /// Validate the configuration.
483    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    /// Get connect timeout as Duration.
501    pub fn connect_timeout(&self) -> Duration {
502        Duration::from_secs(self.transport.connect_timeout_secs)
503    }
504
505    /// Get request timeout as Duration.
506    pub fn request_timeout(&self) -> Duration {
507        Duration::from_secs(self.transport.request_timeout_secs)
508    }
509
510    /// Get emit interval as Duration.
511    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        // Missing source_id
553        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}