runtara_sdk/
config.rs

1// Copyright (C) 2025 SyncMyOrders Sp. z o.o.
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! SDK configuration for connecting to runtara-core.
4
5use std::env;
6use std::net::SocketAddr;
7
8use crate::error::{Result, SdkError};
9
10/// SDK configuration for connecting to runtara-core.
11#[derive(Debug, Clone)]
12pub struct SdkConfig {
13    /// Instance ID (required) - unique identifier for this instance
14    pub instance_id: String,
15    /// Tenant ID (required) - tenant this instance belongs to
16    pub tenant_id: String,
17    /// Server address (default: "127.0.0.1:8001")
18    pub server_addr: SocketAddr,
19    /// Server name for TLS verification (default: "localhost")
20    pub server_name: String,
21    /// Skip TLS certificate verification (default: false, use true for dev)
22    pub skip_cert_verification: bool,
23    /// Connection timeout in milliseconds (default: 10_000)
24    pub connect_timeout_ms: u64,
25    /// Request timeout in milliseconds (default: 30_000)
26    pub request_timeout_ms: u64,
27    /// Signal poll interval in milliseconds (default: 1_000)
28    pub signal_poll_interval_ms: u64,
29    /// Background heartbeat interval in milliseconds (default: 30_000).
30    /// Set to 0 to disable automatic heartbeats.
31    /// Heartbeats run in a background task and keep the instance alive
32    /// during long-running operations that don't checkpoint frequently.
33    pub heartbeat_interval_ms: u64,
34}
35
36impl SdkConfig {
37    /// Load configuration from environment variables.
38    ///
39    /// # Required Environment Variables
40    /// - `RUNTARA_INSTANCE_ID` - Unique identifier for this instance
41    /// - `RUNTARA_TENANT_ID` - Tenant this instance belongs to
42    ///
43    /// # Optional Environment Variables
44    /// - `RUNTARA_SERVER_ADDR` - Server address (default: "127.0.0.1:8001")
45    /// - `RUNTARA_SERVER_NAME` - Server name for TLS (default: "localhost")
46    /// - `RUNTARA_SKIP_CERT_VERIFICATION` - Skip TLS verification (default: false)
47    /// - `RUNTARA_CONNECT_TIMEOUT_MS` - Connection timeout (default: 10000)
48    /// - `RUNTARA_REQUEST_TIMEOUT_MS` - Request timeout (default: 30000)
49    /// - `RUNTARA_SIGNAL_POLL_INTERVAL_MS` - Signal poll interval (default: 1000)
50    /// - `RUNTARA_HEARTBEAT_INTERVAL_MS` - Background heartbeat interval (default: 30000, 0 to disable)
51    pub fn from_env() -> Result<Self> {
52        let instance_id = env::var("RUNTARA_INSTANCE_ID")
53            .map_err(|_| SdkError::Config("RUNTARA_INSTANCE_ID is required".to_string()))?;
54
55        let tenant_id = env::var("RUNTARA_TENANT_ID")
56            .map_err(|_| SdkError::Config("RUNTARA_TENANT_ID is required".to_string()))?;
57
58        let server_addr = env::var("RUNTARA_SERVER_ADDR")
59            .unwrap_or_else(|_| "127.0.0.1:8001".to_string())
60            .parse()
61            .map_err(|e| SdkError::Config(format!("invalid RUNTARA_SERVER_ADDR: {}", e)))?;
62
63        let server_name =
64            env::var("RUNTARA_SERVER_NAME").unwrap_or_else(|_| "localhost".to_string());
65
66        let skip_cert_verification = env::var("RUNTARA_SKIP_CERT_VERIFICATION")
67            .map(|v| v == "true" || v == "1")
68            .unwrap_or(false);
69
70        let connect_timeout_ms = env::var("RUNTARA_CONNECT_TIMEOUT_MS")
71            .ok()
72            .and_then(|v| v.parse().ok())
73            .unwrap_or(10_000);
74
75        let request_timeout_ms = env::var("RUNTARA_REQUEST_TIMEOUT_MS")
76            .ok()
77            .and_then(|v| v.parse().ok())
78            .unwrap_or(30_000);
79
80        let signal_poll_interval_ms = env::var("RUNTARA_SIGNAL_POLL_INTERVAL_MS")
81            .ok()
82            .and_then(|v| v.parse().ok())
83            .unwrap_or(1_000);
84
85        let heartbeat_interval_ms = env::var("RUNTARA_HEARTBEAT_INTERVAL_MS")
86            .ok()
87            .and_then(|v| v.parse().ok())
88            .unwrap_or(30_000);
89
90        Ok(Self {
91            instance_id,
92            tenant_id,
93            server_addr,
94            server_name,
95            skip_cert_verification,
96            connect_timeout_ms,
97            request_timeout_ms,
98            signal_poll_interval_ms,
99            heartbeat_interval_ms,
100        })
101    }
102
103    /// Create a configuration for local development.
104    ///
105    /// This sets up reasonable defaults for local development:
106    /// - Connects to `127.0.0.1:8001`
107    /// - Skips TLS certificate verification
108    pub fn localhost(instance_id: impl Into<String>, tenant_id: impl Into<String>) -> Self {
109        Self {
110            instance_id: instance_id.into(),
111            tenant_id: tenant_id.into(),
112            server_addr: "127.0.0.1:8001".parse().unwrap(),
113            server_name: "localhost".to_string(),
114            skip_cert_verification: true,
115            connect_timeout_ms: 10_000,
116            request_timeout_ms: 30_000,
117            signal_poll_interval_ms: 1_000,
118            heartbeat_interval_ms: 30_000,
119        }
120    }
121
122    /// Create a new configuration with the given instance and tenant IDs.
123    pub fn new(instance_id: impl Into<String>, tenant_id: impl Into<String>) -> Self {
124        Self {
125            instance_id: instance_id.into(),
126            tenant_id: tenant_id.into(),
127            server_addr: "127.0.0.1:8001".parse().unwrap(),
128            server_name: "localhost".to_string(),
129            skip_cert_verification: false,
130            connect_timeout_ms: 10_000,
131            request_timeout_ms: 30_000,
132            signal_poll_interval_ms: 1_000,
133            heartbeat_interval_ms: 30_000,
134        }
135    }
136
137    /// Set the server address.
138    pub fn with_server_addr(mut self, addr: SocketAddr) -> Self {
139        self.server_addr = addr;
140        self
141    }
142
143    /// Set the server name for TLS verification.
144    pub fn with_server_name(mut self, name: impl Into<String>) -> Self {
145        self.server_name = name.into();
146        self
147    }
148
149    /// Skip TLS certificate verification (for development only!).
150    pub fn with_skip_cert_verification(mut self, skip: bool) -> Self {
151        self.skip_cert_verification = skip;
152        self
153    }
154
155    /// Set the signal poll interval.
156    pub fn with_signal_poll_interval_ms(mut self, interval_ms: u64) -> Self {
157        self.signal_poll_interval_ms = interval_ms;
158        self
159    }
160
161    /// Set the background heartbeat interval.
162    /// Set to 0 to disable automatic heartbeats.
163    pub fn with_heartbeat_interval_ms(mut self, interval_ms: u64) -> Self {
164        self.heartbeat_interval_ms = interval_ms;
165        self
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    // ============================================================================
174    // Basic Configuration Tests
175    // ============================================================================
176
177    #[test]
178    fn test_localhost_config() {
179        let config = SdkConfig::localhost("test-instance", "test-tenant");
180        assert_eq!(config.instance_id, "test-instance");
181        assert_eq!(config.tenant_id, "test-tenant");
182        assert!(config.skip_cert_verification);
183        assert_eq!(config.server_addr, "127.0.0.1:8001".parse().unwrap());
184    }
185
186    #[test]
187    fn test_new_config() {
188        let config = SdkConfig::new("my-instance", "my-tenant");
189        assert_eq!(config.instance_id, "my-instance");
190        assert_eq!(config.tenant_id, "my-tenant");
191        // new() defaults to cert verification enabled
192        assert!(!config.skip_cert_verification);
193        assert_eq!(config.server_addr, "127.0.0.1:8001".parse().unwrap());
194        assert_eq!(config.server_name, "localhost");
195    }
196
197    #[test]
198    fn test_config_with_string_types() {
199        let config = SdkConfig::new(String::from("inst"), String::from("tenant"));
200        assert_eq!(config.instance_id, "inst");
201        assert_eq!(config.tenant_id, "tenant");
202    }
203
204    #[test]
205    fn test_localhost_config_with_string_types() {
206        let config = SdkConfig::localhost(String::from("inst"), String::from("tenant"));
207        assert_eq!(config.instance_id, "inst");
208        assert_eq!(config.tenant_id, "tenant");
209    }
210
211    // ============================================================================
212    // Builder Pattern Tests
213    // ============================================================================
214
215    #[test]
216    fn test_builder_pattern() {
217        let config = SdkConfig::new("inst", "tenant")
218            .with_server_addr("192.168.1.1:8000".parse().unwrap())
219            .with_skip_cert_verification(true)
220            .with_signal_poll_interval_ms(500);
221
222        assert_eq!(config.server_addr, "192.168.1.1:8000".parse().unwrap());
223        assert!(config.skip_cert_verification);
224        assert_eq!(config.signal_poll_interval_ms, 500);
225    }
226
227    #[test]
228    fn test_with_server_addr() {
229        let config =
230            SdkConfig::new("inst", "tenant").with_server_addr("10.0.0.1:9000".parse().unwrap());
231        assert_eq!(config.server_addr, "10.0.0.1:9000".parse().unwrap());
232    }
233
234    #[test]
235    fn test_with_server_name() {
236        let config = SdkConfig::new("inst", "tenant").with_server_name("my-server.example.com");
237        assert_eq!(config.server_name, "my-server.example.com");
238    }
239
240    #[test]
241    fn test_with_server_name_string() {
242        let config =
243            SdkConfig::new("inst", "tenant").with_server_name(String::from("server.local"));
244        assert_eq!(config.server_name, "server.local");
245    }
246
247    #[test]
248    fn test_with_skip_cert_verification_true() {
249        let config = SdkConfig::new("inst", "tenant").with_skip_cert_verification(true);
250        assert!(config.skip_cert_verification);
251    }
252
253    #[test]
254    fn test_with_skip_cert_verification_false() {
255        // Start with localhost (which has skip=true) and set to false
256        let config = SdkConfig::localhost("inst", "tenant").with_skip_cert_verification(false);
257        assert!(!config.skip_cert_verification);
258    }
259
260    #[test]
261    fn test_with_signal_poll_interval_ms() {
262        let config = SdkConfig::new("inst", "tenant").with_signal_poll_interval_ms(250);
263        assert_eq!(config.signal_poll_interval_ms, 250);
264    }
265
266    #[test]
267    fn test_builder_chaining() {
268        let config = SdkConfig::new("inst", "tenant")
269            .with_server_addr("172.16.0.1:7000".parse().unwrap())
270            .with_server_name("custom-server")
271            .with_skip_cert_verification(true)
272            .with_signal_poll_interval_ms(100)
273            .with_heartbeat_interval_ms(5000);
274
275        assert_eq!(config.server_addr, "172.16.0.1:7000".parse().unwrap());
276        assert_eq!(config.server_name, "custom-server");
277        assert!(config.skip_cert_verification);
278        assert_eq!(config.signal_poll_interval_ms, 100);
279        assert_eq!(config.heartbeat_interval_ms, 5000);
280    }
281
282    // ============================================================================
283    // Heartbeat Interval Tests
284    // ============================================================================
285
286    #[test]
287    fn test_heartbeat_interval_default() {
288        let config = SdkConfig::new("inst", "tenant");
289        assert_eq!(config.heartbeat_interval_ms, 30_000);
290    }
291
292    #[test]
293    fn test_heartbeat_interval_localhost_default() {
294        let config = SdkConfig::localhost("inst", "tenant");
295        assert_eq!(config.heartbeat_interval_ms, 30_000);
296    }
297
298    #[test]
299    fn test_heartbeat_interval_builder() {
300        let config = SdkConfig::new("inst", "tenant").with_heartbeat_interval_ms(15_000);
301        assert_eq!(config.heartbeat_interval_ms, 15_000);
302    }
303
304    #[test]
305    fn test_heartbeat_interval_disabled() {
306        let config = SdkConfig::new("inst", "tenant").with_heartbeat_interval_ms(0);
307        assert_eq!(config.heartbeat_interval_ms, 0);
308    }
309
310    #[test]
311    fn test_heartbeat_interval_custom_value() {
312        let config = SdkConfig::new("inst", "tenant").with_heartbeat_interval_ms(60_000);
313        assert_eq!(config.heartbeat_interval_ms, 60_000);
314    }
315
316    // ============================================================================
317    // Default Timeout Tests
318    // ============================================================================
319
320    #[test]
321    fn test_connect_timeout_default() {
322        let config = SdkConfig::new("inst", "tenant");
323        assert_eq!(config.connect_timeout_ms, 10_000);
324    }
325
326    #[test]
327    fn test_request_timeout_default() {
328        let config = SdkConfig::new("inst", "tenant");
329        assert_eq!(config.request_timeout_ms, 30_000);
330    }
331
332    #[test]
333    fn test_signal_poll_interval_default() {
334        let config = SdkConfig::new("inst", "tenant");
335        assert_eq!(config.signal_poll_interval_ms, 1_000);
336    }
337
338    // ============================================================================
339    // Clone and Debug Tests
340    // ============================================================================
341
342    #[test]
343    fn test_config_clone() {
344        let config = SdkConfig::new("inst", "tenant")
345            .with_server_addr("10.0.0.1:8080".parse().unwrap())
346            .with_skip_cert_verification(true);
347
348        let cloned = config.clone();
349        assert_eq!(cloned.instance_id, config.instance_id);
350        assert_eq!(cloned.tenant_id, config.tenant_id);
351        assert_eq!(cloned.server_addr, config.server_addr);
352        assert_eq!(cloned.skip_cert_verification, config.skip_cert_verification);
353    }
354
355    #[test]
356    fn test_config_debug() {
357        let config = SdkConfig::new("test-inst", "test-tenant");
358        let debug_str = format!("{:?}", config);
359        assert!(debug_str.contains("test-inst"));
360        assert!(debug_str.contains("test-tenant"));
361        assert!(debug_str.contains("server_addr"));
362    }
363
364    // ============================================================================
365    // Edge Cases
366    // ============================================================================
367
368    #[test]
369    fn test_empty_instance_id() {
370        let config = SdkConfig::new("", "tenant");
371        assert_eq!(config.instance_id, "");
372    }
373
374    #[test]
375    fn test_empty_tenant_id() {
376        let config = SdkConfig::new("inst", "");
377        assert_eq!(config.tenant_id, "");
378    }
379
380    #[test]
381    fn test_special_characters_in_ids() {
382        let config = SdkConfig::new("inst-123_test", "tenant/special:chars");
383        assert_eq!(config.instance_id, "inst-123_test");
384        assert_eq!(config.tenant_id, "tenant/special:chars");
385    }
386
387    #[test]
388    fn test_ipv6_server_addr() {
389        let config =
390            SdkConfig::new("inst", "tenant").with_server_addr("[::1]:8001".parse().unwrap());
391        assert_eq!(config.server_addr, "[::1]:8001".parse().unwrap());
392    }
393}