Skip to main content

slim_bindings/
client_config.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashMap;
5use std::time::Duration;
6
7use slim_config::grpc::client::ClientConfig as CoreClientConfig;
8use slim_config::grpc::compression::CompressionType as CoreCompressionType;
9use slim_config::grpc::proxy::ProxyConfig as CoreProxyConfig;
10
11use slim_auth::metadata::MetadataMap;
12use slim_config::backoff::exponential::Config as ExponentialBackoffConfig;
13use slim_config::backoff::fixedinterval::Config as FixedIntervalBackoffConfig;
14use slim_config::grpc::client::{
15    BackoffConfig as CoreBackoffConfig, KeepaliveConfig as CoreKeepaliveConfig,
16};
17
18use crate::common_config::{ClientAuthenticationConfig, TlsClientConfig};
19use crate::errors::SlimError;
20
21use slim_config::component::configuration::Configuration;
22
23/// Compression type for gRPC messages
24#[derive(uniffi::Enum, Clone, Debug, PartialEq)]
25pub enum CompressionType {
26    Gzip,
27    Zlib,
28    Deflate,
29    Snappy,
30    Zstd,
31    Lz4,
32    None,
33    Empty,
34}
35
36impl From<CompressionType> for CoreCompressionType {
37    fn from(compression: CompressionType) -> Self {
38        match compression {
39            CompressionType::Gzip => CoreCompressionType::Gzip,
40            CompressionType::Zlib => CoreCompressionType::Zlib,
41            CompressionType::Deflate => CoreCompressionType::Deflate,
42            CompressionType::Snappy => CoreCompressionType::Snappy,
43            CompressionType::Zstd => CoreCompressionType::Zstd,
44            CompressionType::Lz4 => CoreCompressionType::Lz4,
45            CompressionType::None => CoreCompressionType::None,
46            CompressionType::Empty => CoreCompressionType::Empty,
47        }
48    }
49}
50
51impl From<CoreCompressionType> for CompressionType {
52    fn from(compression: CoreCompressionType) -> Self {
53        match compression {
54            CoreCompressionType::Gzip => CompressionType::Gzip,
55            CoreCompressionType::Zlib => CompressionType::Zlib,
56            CoreCompressionType::Deflate => CompressionType::Deflate,
57            CoreCompressionType::Snappy => CompressionType::Snappy,
58            CoreCompressionType::Zstd => CompressionType::Zstd,
59            CoreCompressionType::Lz4 => CompressionType::Lz4,
60            CoreCompressionType::None => CompressionType::None,
61            CoreCompressionType::Empty => CompressionType::Empty,
62        }
63    }
64}
65
66/// Keepalive configuration for the client
67#[derive(uniffi::Record, Clone, Debug, PartialEq)]
68pub struct KeepaliveConfig {
69    /// TCP keepalive duration
70    pub tcp_keepalive: Duration,
71    /// HTTP2 keepalive duration
72    pub http2_keepalive: Duration,
73    /// Keepalive timeout
74    pub timeout: Duration,
75    /// Whether to permit keepalive without an active stream
76    pub keep_alive_while_idle: bool,
77}
78
79impl Default for KeepaliveConfig {
80    fn default() -> Self {
81        let core_defaults = CoreKeepaliveConfig::default();
82        KeepaliveConfig {
83            tcp_keepalive: *core_defaults.tcp_keepalive,
84            http2_keepalive: *core_defaults.http2_keepalive,
85            timeout: *core_defaults.timeout,
86            keep_alive_while_idle: core_defaults.keep_alive_while_idle,
87        }
88    }
89}
90
91impl From<KeepaliveConfig> for CoreKeepaliveConfig {
92    fn from(config: KeepaliveConfig) -> Self {
93        CoreKeepaliveConfig {
94            tcp_keepalive: config.tcp_keepalive.into(),
95            http2_keepalive: config.http2_keepalive.into(),
96            timeout: config.timeout.into(),
97            keep_alive_while_idle: config.keep_alive_while_idle,
98        }
99    }
100}
101
102impl From<CoreKeepaliveConfig> for KeepaliveConfig {
103    fn from(config: CoreKeepaliveConfig) -> Self {
104        KeepaliveConfig {
105            tcp_keepalive: *config.tcp_keepalive,
106            http2_keepalive: *config.http2_keepalive,
107            timeout: *config.timeout,
108            keep_alive_while_idle: config.keep_alive_while_idle,
109        }
110    }
111}
112
113/// HTTP Proxy configuration
114#[derive(uniffi::Record, Clone, Debug, PartialEq)]
115pub struct ProxyConfig {
116    /// The HTTP proxy URL (e.g., "http://proxy.example.com:8080")
117    pub url: Option<String>,
118    /// TLS configuration for proxy connection
119    pub tls: TlsClientConfig,
120    /// Optional username for proxy authentication
121    pub username: Option<String>,
122    /// Optional password for proxy authentication
123    pub password: Option<String>,
124    /// Headers to send with proxy requests
125    pub headers: HashMap<String, String>,
126}
127
128impl Default for ProxyConfig {
129    fn default() -> Self {
130        let core_defaults = CoreProxyConfig::default();
131        ProxyConfig {
132            url: core_defaults.url,
133            tls: core_defaults.tls_setting.into(),
134            username: core_defaults.username,
135            password: core_defaults.password,
136            headers: core_defaults.headers,
137        }
138    }
139}
140
141impl From<ProxyConfig> for CoreProxyConfig {
142    fn from(config: ProxyConfig) -> Self {
143        CoreProxyConfig {
144            url: config.url,
145            tls_setting: config.tls.into(),
146            username: config.username,
147            password: config.password,
148            headers: config.headers,
149        }
150    }
151}
152
153impl From<CoreProxyConfig> for ProxyConfig {
154    fn from(config: CoreProxyConfig) -> Self {
155        ProxyConfig {
156            url: config.url,
157            tls: config.tls_setting.into(),
158            username: config.username,
159            password: config.password,
160            headers: config.headers,
161        }
162    }
163}
164
165/// Exponential backoff configuration
166#[derive(uniffi::Record, Clone, Debug, PartialEq)]
167pub struct ExponentialBackoff {
168    /// Base delay
169    pub base: Duration,
170    /// Multiplication factor for each retry
171    pub factor: u64,
172    /// Maximum delay
173    pub max_delay: Duration,
174    /// Maximum number of retry attempts
175    pub max_attempts: u64,
176    /// Whether to add random jitter to delays
177    pub jitter: bool,
178}
179
180impl Default for ExponentialBackoff {
181    fn default() -> Self {
182        let core_defaults = ExponentialBackoffConfig::default();
183        ExponentialBackoff {
184            base: Duration::from_millis(core_defaults.base),
185            factor: core_defaults.factor,
186            max_delay: *core_defaults.max_delay,
187            max_attempts: core_defaults.max_attempts as u64,
188            jitter: core_defaults.jitter,
189        }
190    }
191}
192
193/// Fixed interval backoff configuration
194#[derive(uniffi::Record, Clone, Debug, PartialEq)]
195pub struct FixedIntervalBackoff {
196    /// Fixed interval between retries
197    pub interval: Duration,
198    /// Maximum number of retry attempts
199    pub max_attempts: u64,
200}
201
202impl Default for FixedIntervalBackoff {
203    fn default() -> Self {
204        let core_defaults = FixedIntervalBackoffConfig::default();
205        FixedIntervalBackoff {
206            interval: *core_defaults.interval,
207            max_attempts: core_defaults.max_attempts as u64,
208        }
209    }
210}
211
212/// Backoff retry configuration
213#[derive(uniffi::Enum, Clone, Debug, PartialEq)]
214pub enum BackoffConfig {
215    Exponential { config: ExponentialBackoff },
216    FixedInterval { config: FixedIntervalBackoff },
217}
218
219impl From<BackoffConfig> for CoreBackoffConfig {
220    fn from(config: BackoffConfig) -> Self {
221        match config {
222            BackoffConfig::Exponential { config } => {
223                CoreBackoffConfig::Exponential(ExponentialBackoffConfig::new(
224                    config.base.as_millis() as u64,
225                    config.factor,
226                    config.max_delay,
227                    config.max_attempts as usize,
228                    config.jitter,
229                ))
230            }
231            BackoffConfig::FixedInterval { config } => CoreBackoffConfig::FixedInterval(
232                FixedIntervalBackoffConfig::new(config.interval, config.max_attempts as usize),
233            ),
234        }
235    }
236}
237
238impl From<CoreBackoffConfig> for BackoffConfig {
239    fn from(config: CoreBackoffConfig) -> Self {
240        match config {
241            CoreBackoffConfig::Exponential(core_config) => BackoffConfig::Exponential {
242                config: ExponentialBackoff {
243                    base: Duration::from_millis(core_config.base),
244                    factor: core_config.factor,
245                    max_delay: *core_config.max_delay,
246                    max_attempts: core_config.max_attempts as u64,
247                    jitter: core_config.jitter,
248                },
249            },
250            CoreBackoffConfig::FixedInterval(core_config) => BackoffConfig::FixedInterval {
251                config: FixedIntervalBackoff {
252                    interval: *core_config.interval,
253                    max_attempts: core_config.max_attempts as u64,
254                },
255            },
256        }
257    }
258}
259
260/// Client configuration for connecting to a SLIM server
261#[derive(uniffi::Record, Clone, Debug, PartialEq)]
262pub struct ClientConfig {
263    /// The target endpoint the client will connect to.
264    ///
265    /// The transport protocol is inferred from the endpoint scheme:
266    /// `ws://`/`wss://` → WebSocket, otherwise gRPC.
267    pub endpoint: String,
268
269    /// TLS client configuration
270    pub tls: TlsClientConfig,
271
272    /// Origin (HTTP Host authority override) for the client
273    pub origin: Option<String>,
274
275    /// Optional TLS SNI server name override
276    pub server_name: Option<String>,
277
278    /// Compression type
279    pub compression: Option<CompressionType>,
280
281    /// Rate limit string (e.g., "100/s" for 100 requests per second)
282    pub rate_limit: Option<String>,
283
284    /// Keepalive parameters
285    pub keepalive: Option<KeepaliveConfig>,
286
287    /// HTTP Proxy configuration
288    pub proxy: Option<ProxyConfig>,
289
290    /// Connection timeout
291    pub connect_timeout: Option<Duration>,
292
293    /// Request timeout
294    pub request_timeout: Option<Duration>,
295
296    /// Read buffer size in bytes
297    pub buffer_size: Option<u64>,
298
299    /// Headers associated with gRPC requests
300    pub headers: Option<HashMap<String, String>>,
301
302    /// Authentication configuration for outgoing RPCs
303    pub auth: Option<ClientAuthenticationConfig>,
304
305    /// Backoff retry configuration
306    pub backoff: Option<BackoffConfig>,
307
308    /// Arbitrary user-provided metadata as JSON string
309    pub metadata: Option<String>,
310
311    /// When true, reject inter-node messages without a valid header MAC (strict mode).
312    pub require_header_mac: Option<bool>,
313}
314
315impl From<ClientConfig> for CoreClientConfig {
316    fn from(config: ClientConfig) -> Self {
317        let core_defaults = CoreClientConfig::default();
318        CoreClientConfig {
319            endpoint: config.endpoint,
320            origin: config.origin,
321            server_name: config.server_name,
322            compression: config.compression.map(Into::into),
323            rate_limit: config.rate_limit,
324            tls_setting: config.tls.into(),
325            keepalive: config.keepalive.map(Into::into),
326            proxy: config.proxy.map(Into::into).unwrap_or(core_defaults.proxy),
327            connect_timeout: config
328                .connect_timeout
329                .map(Into::into)
330                .unwrap_or(core_defaults.connect_timeout),
331            request_timeout: config
332                .request_timeout
333                .map(Into::into)
334                .unwrap_or(core_defaults.request_timeout),
335            buffer_size: config.buffer_size.map(|s| s as usize),
336            headers: config.headers.unwrap_or(core_defaults.headers),
337            auth: config.auth.map(Into::into).unwrap_or(core_defaults.auth),
338            backoff: config
339                .backoff
340                .map(Into::into)
341                .unwrap_or(core_defaults.backoff),
342            metadata: config
343                .metadata
344                .and_then(|json| serde_json::from_str::<MetadataMap>(&json).ok()),
345            link_id: core_defaults.link_id,
346            require_header_mac: config
347                .require_header_mac
348                .unwrap_or(core_defaults.require_header_mac),
349            connection_type: core_defaults.connection_type,
350        }
351    }
352}
353
354impl From<CoreClientConfig> for ClientConfig {
355    fn from(config: CoreClientConfig) -> Self {
356        ClientConfig {
357            endpoint: config.endpoint,
358            origin: config.origin,
359            server_name: config.server_name,
360            compression: config.compression.map(Into::into),
361            rate_limit: config.rate_limit,
362            tls: config.tls_setting.into(),
363            keepalive: config.keepalive.map(Into::into),
364            proxy: Some(config.proxy.into()),
365            connect_timeout: Some(*config.connect_timeout),
366            request_timeout: Some(*config.request_timeout),
367            buffer_size: config.buffer_size.map(|s| s as u64),
368            headers: Some(config.headers),
369            auth: Some(config.auth.into()),
370            backoff: Some(config.backoff.into()),
371            metadata: config.metadata.and_then(|m| serde_json::to_string(&m).ok()),
372            require_header_mac: Some(config.require_header_mac),
373        }
374    }
375}
376
377impl Default for ClientConfig {
378    fn default() -> Self {
379        let core_defaults = CoreClientConfig::default();
380        Self {
381            endpoint: core_defaults.endpoint,
382            origin: None,
383            server_name: None,
384            compression: None,
385            rate_limit: None,
386            tls: core_defaults.tls_setting.into(),
387            keepalive: None,
388            proxy: None,
389            connect_timeout: None,
390            request_timeout: None,
391            buffer_size: None,
392            headers: None,
393            auth: None,
394            backoff: None,
395            metadata: None,
396            require_header_mac: None,
397        }
398    }
399}
400
401/// Create a new insecure client config (no TLS)
402#[uniffi::export]
403pub fn new_insecure_client_config(endpoint: String) -> ClientConfig {
404    ClientConfig {
405        endpoint,
406        tls: TlsClientConfig {
407            insecure: true,
408            ..Default::default()
409        },
410        ..Default::default()
411    }
412}
413
414/// Create a new secure client config (TLS enabled with default settings)
415#[uniffi::export]
416pub fn new_secure_client_config(endpoint: String) -> ClientConfig {
417    ClientConfig {
418        endpoint,
419        tls: TlsClientConfig::default(),
420        ..Default::default()
421    }
422}
423
424/// Parse and validate a SLIM gRPC client configuration from JSON.
425///
426/// The JSON must match [`CoreClientConfig`] (same as
427/// `data-plane/core/config/src/grpc/schema/client-config.schema.json`).
428#[uniffi::export]
429pub fn new_config_from_json(json: String) -> Result<ClientConfig, SlimError> {
430    let core: CoreClientConfig =
431        serde_json::from_str(&json).map_err(|e| SlimError::ConfigError {
432            message: format!("invalid JSON for client config: {e}"),
433        })?;
434    core.validate().map_err(|e| SlimError::ConfigError {
435        message: e.to_string(),
436    })?;
437    Ok(core.into())
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443    use crate::common_config::{CaSource, TlsSource};
444    use crate::errors::SlimError;
445    use slim_config::transport::TransportProtocol as CoreTransportProtocol;
446    use std::collections::HashMap;
447
448    #[test]
449    fn test_client_config_creation() {
450        let config = ClientConfig {
451            endpoint: "example.com:443".to_string(),
452            origin: None,
453            server_name: None,
454            compression: None,
455            rate_limit: None,
456            tls: TlsClientConfig {
457                insecure: false,
458                insecure_skip_verify: false,
459                source: TlsSource::None,
460                ca_source: CaSource::File {
461                    path: "/ca.pem".to_string(),
462                },
463                include_system_ca_certs_pool: true,
464                tls_version: "tls1.2".to_string(),
465            },
466            keepalive: None,
467            proxy: None,
468            connect_timeout: Some(Duration::from_secs(10)),
469            request_timeout: Some(Duration::from_secs(30)),
470            buffer_size: None,
471            headers: None,
472            auth: None,
473            backoff: None,
474            metadata: None,
475            require_header_mac: None,
476        };
477
478        assert_eq!(config.endpoint, "example.com:443");
479        assert_eq!(config.tls.tls_version, "tls1.2");
480        assert!(!config.tls.insecure);
481    }
482
483    #[test]
484    fn test_client_config_default() {
485        let config = ClientConfig::default();
486
487        // Verify defaults are all None (core defaults applied during conversion)
488        assert_eq!(config.endpoint, "");
489        assert_eq!(config.origin, None);
490        assert_eq!(config.server_name, None);
491        assert_eq!(config.compression, None);
492        assert_eq!(config.rate_limit, None);
493        assert_eq!(config.tls, TlsClientConfig::default());
494        assert_eq!(config.keepalive, None);
495        assert_eq!(config.proxy, None);
496        assert_eq!(config.connect_timeout, None);
497        assert_eq!(config.request_timeout, None);
498        assert_eq!(config.buffer_size, None);
499        assert_eq!(config.headers, None);
500        assert_eq!(config.auth, None);
501        assert_eq!(config.backoff, None);
502        assert_eq!(config.metadata, None);
503
504        // Verify core defaults are applied when converting to CoreClientConfig
505        let core: CoreClientConfig = config.into();
506        assert_eq!(*core.connect_timeout, Duration::from_secs(0));
507        assert_eq!(*core.request_timeout, Duration::from_secs(0));
508        assert!(core.headers.is_empty());
509        assert_eq!(
510            core.auth,
511            slim_config::grpc::client::AuthenticationConfig::None
512        );
513    }
514
515    #[test]
516    fn test_client_config_new_insecure() {
517        let config = new_insecure_client_config("localhost:50051".to_string());
518
519        assert_eq!(config.endpoint, "localhost:50051");
520        assert!(config.tls.insecure);
521    }
522
523    #[test]
524    fn test_client_config_new_secure() {
525        let config = new_secure_client_config("api.example.com:443".to_string());
526
527        assert_eq!(config.endpoint, "api.example.com:443");
528        assert!(!config.tls.insecure);
529        assert!(!config.tls.insecure_skip_verify);
530        assert_eq!(config.tls, TlsClientConfig::default());
531        // All optional fields should be None
532        assert_eq!(config.origin, None);
533        assert_eq!(config.keepalive, None);
534        assert_eq!(config.proxy, None);
535        assert_eq!(config.connect_timeout, None);
536        assert_eq!(config.request_timeout, None);
537        assert_eq!(config.auth, None);
538        assert_eq!(config.backoff, None);
539    }
540
541    #[test]
542    fn new_config_from_json_minimal_insecure() {
543        let json = r#"{"endpoint":"http://127.0.0.1:46357","tls":{"insecure":true}}"#;
544        let cfg = new_config_from_json(json.to_string()).expect("parse");
545        assert_eq!(cfg.endpoint, "http://127.0.0.1:46357");
546        assert!(cfg.tls.insecure);
547    }
548
549    #[test]
550    fn new_config_from_json_invalid_json() {
551        let err = new_config_from_json("{not json".to_string()).unwrap_err();
552        assert!(matches!(err, SlimError::ConfigError { .. }));
553        let SlimError::ConfigError { message } = err else {
554            unreachable!();
555        };
556        assert!(message.contains("invalid JSON"), "message was: {message}");
557    }
558
559    #[test]
560    fn new_config_from_json_missing_endpoint() {
561        let json = r#"{"endpoint":"","tls":{"insecure":true}}"#;
562        let err = new_config_from_json(json.to_string()).unwrap_err();
563        assert!(matches!(err, SlimError::ConfigError { .. }));
564    }
565
566    #[test]
567    fn test_client_config_to_core_conversion() {
568        let mut headers = HashMap::new();
569        headers.insert("x-api-key".to_string(), "test-key".to_string());
570
571        let ffi_config = ClientConfig {
572            endpoint: "ws://api.example.com:443".to_string(),
573            origin: Some("example.com".to_string()),
574            server_name: Some("sni.example.com".to_string()),
575            compression: Some(CompressionType::Gzip),
576            rate_limit: Some("100/s".to_string()),
577            tls: TlsClientConfig::default(),
578            keepalive: Some(KeepaliveConfig {
579                tcp_keepalive: Duration::from_secs(60),
580                http2_keepalive: Duration::from_secs(30),
581                timeout: Duration::from_secs(20),
582                keep_alive_while_idle: true,
583            }),
584            proxy: Some(ProxyConfig::default()),
585            connect_timeout: Some(Duration::from_secs(15)),
586            request_timeout: Some(Duration::from_secs(60)),
587            buffer_size: Some(8192),
588            headers: Some(headers.clone()),
589            auth: Some(ClientAuthenticationConfig::None),
590            backoff: Some(BackoffConfig::FixedInterval {
591                config: FixedIntervalBackoff {
592                    interval: Duration::from_secs(1),
593                    max_attempts: 5,
594                },
595            }),
596            metadata: Some(r#"{"client":"test"}"#.to_string()),
597            require_header_mac: None,
598        };
599
600        let core_config: CoreClientConfig = ffi_config.into();
601
602        assert_eq!(core_config.endpoint, "ws://api.example.com:443");
603        assert_eq!(
604            core_config.resolved_transport(),
605            CoreTransportProtocol::Websocket
606        );
607        assert_eq!(core_config.origin, Some("example.com".to_string()));
608        assert_eq!(core_config.server_name, Some("sni.example.com".to_string()));
609        assert!(core_config.compression.is_some());
610        assert_eq!(core_config.rate_limit, Some("100/s".to_string()));
611        assert!(core_config.keepalive.is_some());
612        assert_eq!(core_config.buffer_size, Some(8192));
613        assert_eq!(core_config.headers.len(), 1);
614        assert!(core_config.metadata.is_some());
615    }
616
617    #[test]
618    fn test_client_config_from_core_conversion() {
619        // Test the new From<CoreClientConfig> for ClientConfig implementation
620        let core_config = CoreClientConfig::default();
621
622        // Use the From trait to convert
623        let ffi_config: ClientConfig = core_config.clone().into();
624
625        assert_eq!(ffi_config.endpoint, core_config.endpoint);
626        assert_eq!(ffi_config.origin, core_config.origin);
627        assert_eq!(ffi_config.server_name, core_config.server_name);
628        assert_eq!(ffi_config.rate_limit, core_config.rate_limit);
629        assert_eq!(
630            ffi_config.connect_timeout,
631            Some(*core_config.connect_timeout)
632        );
633        assert_eq!(
634            ffi_config.request_timeout,
635            Some(*core_config.request_timeout)
636        );
637    }
638
639    #[test]
640    fn test_client_config_roundtrip_conversion() {
641        let original = ClientConfig {
642            endpoint: "localhost:8080".to_string(),
643            origin: Some("test.local".to_string()),
644            server_name: None,
645            compression: Some(CompressionType::Zstd),
646            rate_limit: Some("50/s".to_string()),
647            tls: TlsClientConfig::default(),
648            keepalive: None,
649            proxy: Some(ProxyConfig::default()),
650            connect_timeout: Some(Duration::from_secs(5)),
651            request_timeout: Some(Duration::from_secs(10)),
652            buffer_size: Some(4096),
653            headers: Some(HashMap::new()),
654            auth: Some(ClientAuthenticationConfig::None),
655            backoff: Some(BackoffConfig::Exponential {
656                config: ExponentialBackoff::default(),
657            }),
658            metadata: None,
659            require_header_mac: None,
660        };
661
662        // FFI -> Core -> FFI using the new From implementation
663        let core: CoreClientConfig = original.clone().into();
664        let roundtrip: ClientConfig = core.into();
665
666        assert_eq!(roundtrip.endpoint, original.endpoint);
667        assert_eq!(roundtrip.origin, original.origin);
668        assert_eq!(roundtrip.rate_limit, original.rate_limit);
669        assert_eq!(roundtrip.buffer_size, original.buffer_size);
670    }
671
672    #[test]
673    fn test_compression_type_conversion() {
674        let compressions = vec![
675            CompressionType::Gzip,
676            CompressionType::Zlib,
677            CompressionType::Deflate,
678            CompressionType::Snappy,
679            CompressionType::Zstd,
680            CompressionType::Lz4,
681            CompressionType::None,
682            CompressionType::Empty,
683        ];
684
685        for compression in compressions {
686            let core: CoreCompressionType = compression.clone().into();
687            let back: CompressionType = core.into();
688            // Verify roundtrip works (can't directly compare enums without PartialEq)
689            match (compression, back) {
690                (CompressionType::Gzip, CompressionType::Gzip) => {}
691                (CompressionType::Zlib, CompressionType::Zlib) => {}
692                (CompressionType::Deflate, CompressionType::Deflate) => {}
693                (CompressionType::Snappy, CompressionType::Snappy) => {}
694                (CompressionType::Zstd, CompressionType::Zstd) => {}
695                (CompressionType::Lz4, CompressionType::Lz4) => {}
696                (CompressionType::None, CompressionType::None) => {}
697                (CompressionType::Empty, CompressionType::Empty) => {}
698                _ => panic!("Compression roundtrip failed"),
699            }
700        }
701    }
702
703    #[test]
704    fn test_keepalive_conversion() {
705        let ffi_keepalive = KeepaliveConfig {
706            tcp_keepalive: Duration::from_secs(120),
707            http2_keepalive: Duration::from_secs(60),
708            timeout: Duration::from_secs(30),
709            keep_alive_while_idle: false,
710        };
711
712        let core_keepalive: CoreKeepaliveConfig = ffi_keepalive.into();
713
714        assert_eq!(*core_keepalive.tcp_keepalive, Duration::from_secs(120));
715        assert_eq!(*core_keepalive.http2_keepalive, Duration::from_secs(60));
716        assert_eq!(*core_keepalive.timeout, Duration::from_secs(30));
717        assert!(!core_keepalive.keep_alive_while_idle);
718    }
719
720    #[test]
721    fn test_backoff_exponential_conversion() {
722        let ffi_backoff = BackoffConfig::Exponential {
723            config: ExponentialBackoff {
724                base: Duration::from_millis(100),
725                factor: 2,
726                max_delay: Duration::from_secs(60),
727                max_attempts: 10,
728                jitter: true,
729            },
730        };
731
732        let core_backoff: CoreBackoffConfig = ffi_backoff.into();
733
734        match core_backoff {
735            CoreBackoffConfig::Exponential(config) => {
736                assert_eq!(config.base, 100);
737                assert_eq!(config.factor, 2);
738                assert_eq!(*config.max_delay, Duration::from_secs(60));
739                assert_eq!(config.max_attempts, 10);
740                assert!(config.jitter);
741            }
742            _ => panic!("Expected Exponential backoff"),
743        }
744    }
745
746    #[test]
747    fn test_backoff_fixed_interval_conversion() {
748        let ffi_backoff = BackoffConfig::FixedInterval {
749            config: FixedIntervalBackoff {
750                interval: Duration::from_secs(2),
751                max_attempts: 3,
752            },
753        };
754
755        let core_backoff: CoreBackoffConfig = ffi_backoff.into();
756
757        match core_backoff {
758            CoreBackoffConfig::FixedInterval(config) => {
759                assert_eq!(*config.interval, Duration::from_secs(2));
760                assert_eq!(config.max_attempts, 3);
761            }
762            _ => panic!("Expected FixedInterval backoff"),
763        }
764    }
765
766    #[test]
767    fn test_proxy_conversion() {
768        let mut headers = HashMap::new();
769        headers.insert(
770            "Proxy-Authorization".to_string(),
771            "Bearer token".to_string(),
772        );
773
774        let ffi_proxy = ProxyConfig {
775            url: Some("http://proxy.example.com:8080".to_string()),
776            tls: TlsClientConfig::default(),
777            username: Some("user".to_string()),
778            password: Some("pass".to_string()),
779            headers: headers.clone(),
780        };
781
782        let core_proxy: CoreProxyConfig = ffi_proxy.into();
783
784        assert_eq!(
785            core_proxy.url,
786            Some("http://proxy.example.com:8080".to_string())
787        );
788        assert_eq!(core_proxy.username, Some("user".to_string()));
789        assert_eq!(core_proxy.password, Some("pass".to_string()));
790        assert_eq!(core_proxy.headers.len(), 1);
791    }
792
793    #[test]
794    fn test_metadata_serialization() {
795        let config = ClientConfig {
796            endpoint: "test:443".to_string(),
797            tls: TlsClientConfig::default(),
798            metadata: Some(r#"{"env":"production","region":"us-west"}"#.to_string()),
799            ..Default::default()
800        };
801
802        let core: CoreClientConfig = config.into();
803
804        // Metadata should be deserialized successfully
805        assert!(core.metadata.is_some());
806        let metadata = core.metadata.unwrap();
807        assert_eq!(metadata.len(), 2);
808    }
809
810    #[test]
811    fn test_metadata_invalid_json() {
812        let config = ClientConfig {
813            endpoint: "test:443".to_string(),
814            tls: TlsClientConfig::default(),
815            metadata: Some("not valid json".to_string()),
816            ..Default::default()
817        };
818
819        let core: CoreClientConfig = config.into();
820
821        // Invalid JSON should result in None metadata
822        assert!(core.metadata.is_none());
823    }
824
825    #[test]
826    fn test_jwt_auth_roundtrip() {
827        use crate::identity_config::{
828            ClientJwtAuth, JwtAlgorithm, JwtKeyConfig, JwtKeyData, JwtKeyFormat, JwtKeyType,
829        };
830
831        let jwt_config = ClientJwtAuth {
832            key: JwtKeyType::Encoding {
833                key: JwtKeyConfig {
834                    algorithm: JwtAlgorithm::RS256,
835                    format: JwtKeyFormat::Pem,
836                    key: JwtKeyData::File {
837                        path: "/path/to/private_key.pem".to_string(),
838                    },
839                },
840            },
841            audience: Some(vec!["api.example.com".to_string()]),
842            issuer: Some("auth.example.com".to_string()),
843            subject: Some("user123".to_string()),
844            duration: Duration::from_secs(7200),
845        };
846
847        let auth = ClientAuthenticationConfig::Jwt {
848            config: jwt_config.clone(),
849        };
850
851        // Convert to core and back
852        let core_auth: slim_config::grpc::client::AuthenticationConfig = auth.into();
853        let roundtrip_auth: ClientAuthenticationConfig = core_auth.into();
854
855        // Verify roundtrip preserves the configuration
856        if let ClientAuthenticationConfig::Jwt { config } = roundtrip_auth {
857            assert_eq!(config.key, jwt_config.key);
858            assert_eq!(config.audience, jwt_config.audience);
859            assert_eq!(config.issuer, jwt_config.issuer);
860            assert_eq!(config.subject, jwt_config.subject);
861            // Note: duration might not be exactly preserved due to conversion limitations
862        } else {
863            panic!("Expected Jwt authentication config");
864        }
865    }
866
867    #[test]
868    fn test_basic_auth_roundtrip() {
869        use crate::common_config::BasicAuth;
870
871        let basic_config = BasicAuth {
872            username: "admin".to_string(),
873            password: "secret123".to_string(),
874        };
875
876        let auth = ClientAuthenticationConfig::Basic {
877            config: basic_config.clone(),
878        };
879
880        // Convert to core and back
881        let core_auth: slim_config::grpc::client::AuthenticationConfig = auth.into();
882        let roundtrip_auth: ClientAuthenticationConfig = core_auth.into();
883
884        // Verify roundtrip preserves the configuration
885        if let ClientAuthenticationConfig::Basic { config } = roundtrip_auth {
886            assert_eq!(config.username, basic_config.username);
887            assert_eq!(config.password, basic_config.password);
888        } else {
889            panic!("Expected Basic authentication config");
890        }
891    }
892
893    #[test]
894    fn test_static_jwt_auth_roundtrip() {
895        use crate::identity_config::StaticJwtAuth;
896
897        let jwt_config = StaticJwtAuth {
898            token_file: "/path/to/token.jwt".to_string(),
899            duration: Duration::from_secs(1800),
900        };
901
902        let auth = ClientAuthenticationConfig::StaticJwt {
903            config: jwt_config.clone(),
904        };
905
906        // Convert to core and back
907        let core_auth: slim_config::grpc::client::AuthenticationConfig = auth.into();
908        let roundtrip_auth: ClientAuthenticationConfig = core_auth.into();
909
910        // Verify roundtrip preserves the configuration
911        if let ClientAuthenticationConfig::StaticJwt { config } = roundtrip_auth {
912            assert_eq!(config.token_file, jwt_config.token_file);
913            assert_eq!(config.duration, jwt_config.duration);
914        } else {
915            panic!("Expected StaticJwt authentication config");
916        }
917    }
918
919    #[test]
920    fn test_client_config_from_core_with_all_fields() {
921        // Test the new From<CoreClientConfig> for ClientConfig with comprehensive field coverage
922        use slim_config::backoff::exponential::Config as CoreExponentialBackoffConfig;
923        use slim_config::grpc::client::BackoffConfig as CoreBackoffConfig;
924
925        let mut headers = HashMap::new();
926        headers.insert("X-Custom-Header".to_string(), "value".to_string());
927
928        let mut metadata = MetadataMap::new();
929        metadata.insert("service".to_string(), "test-service".to_string());
930        metadata.insert("version".to_string(), "1.0".to_string());
931
932        let core_config = CoreClientConfig {
933            endpoint: "test.example.com:9443".to_string(),
934            origin: Some("origin.example.com".to_string()),
935            server_name: Some("server.example.com".to_string()),
936            rate_limit: Some("100/s".to_string()),
937            buffer_size: Some(8192),
938            headers: headers.clone(),
939            metadata: Some(metadata),
940            backoff: CoreBackoffConfig::Exponential(CoreExponentialBackoffConfig {
941                base: 50,
942                factor: 3,
943                max_delay: Duration::from_secs(120).into(),
944                max_attempts: 5,
945                jitter: true,
946            }),
947            ..Default::default()
948        };
949
950        // Use the new From implementation
951        let ffi_config: ClientConfig = core_config.clone().into();
952
953        // Verify all fields are correctly converted
954        assert_eq!(ffi_config.endpoint, "test.example.com:9443");
955        assert_eq!(ffi_config.origin, Some("origin.example.com".to_string()));
956        assert_eq!(
957            ffi_config.server_name,
958            Some("server.example.com".to_string())
959        );
960        assert_eq!(ffi_config.rate_limit, Some("100/s".to_string()));
961        assert_eq!(ffi_config.buffer_size, Some(8192));
962        let headers_map = ffi_config.headers.unwrap();
963        assert_eq!(headers_map.len(), 1);
964        assert_eq!(
965            headers_map.get("X-Custom-Header"),
966            Some(&"value".to_string())
967        );
968
969        // Verify metadata is serialized correctly
970        assert!(ffi_config.metadata.is_some());
971        let metadata_str = ffi_config.metadata.unwrap();
972        assert!(metadata_str.contains("test-service"));
973        assert!(metadata_str.contains("1.0"));
974    }
975
976    #[test]
977    fn test_client_config_from_core_with_keepalive() {
978        use slim_config::grpc::client::KeepaliveConfig as CoreKeepaliveConfig;
979
980        let core_config = CoreClientConfig {
981            keepalive: Some(CoreKeepaliveConfig {
982                tcp_keepalive: Duration::from_secs(90).into(),
983                http2_keepalive: Duration::from_secs(45).into(),
984                timeout: Duration::from_secs(20).into(),
985                keep_alive_while_idle: true,
986            }),
987            ..Default::default()
988        };
989
990        let ffi_config: ClientConfig = core_config.into();
991
992        let keepalive = ffi_config.keepalive.unwrap();
993        assert_eq!(keepalive.tcp_keepalive, Duration::from_secs(90));
994        assert_eq!(keepalive.http2_keepalive, Duration::from_secs(45));
995        assert_eq!(keepalive.timeout, Duration::from_secs(20));
996        assert!(keepalive.keep_alive_while_idle);
997    }
998
999    #[test]
1000    fn test_client_config_from_core_with_compression() {
1001        use slim_config::grpc::compression::CompressionType as CoreCompressionType;
1002
1003        let compressions = vec![
1004            CoreCompressionType::Gzip,
1005            CoreCompressionType::Zstd,
1006            CoreCompressionType::Snappy,
1007        ];
1008
1009        for core_compression in compressions {
1010            let core_config = CoreClientConfig {
1011                compression: Some(core_compression.clone()),
1012                ..Default::default()
1013            };
1014
1015            let ffi_config: ClientConfig = core_config.into();
1016
1017            assert!(ffi_config.compression.is_some());
1018        }
1019    }
1020
1021    #[test]
1022    fn test_client_config_from_core_with_proxy() {
1023        use slim_config::grpc::proxy::ProxyConfig as CoreProxyConfig;
1024
1025        let mut proxy_headers = HashMap::new();
1026        proxy_headers.insert("Proxy-Auth".to_string(), "token123".to_string());
1027
1028        let core_config = CoreClientConfig {
1029            proxy: CoreProxyConfig {
1030                url: Some("http://proxy.internal:3128".to_string()),
1031                tls_setting: Default::default(),
1032                username: Some("proxy_user".to_string()),
1033                password: Some("proxy_pass".to_string()),
1034                headers: proxy_headers.clone(),
1035            },
1036            ..Default::default()
1037        };
1038
1039        let ffi_config: ClientConfig = core_config.into();
1040
1041        let proxy = ffi_config.proxy.unwrap();
1042        assert_eq!(proxy.url, Some("http://proxy.internal:3128".to_string()));
1043        assert_eq!(proxy.username, Some("proxy_user".to_string()));
1044        assert_eq!(proxy.password, Some("proxy_pass".to_string()));
1045        assert_eq!(proxy.headers.len(), 1);
1046    }
1047
1048    #[test]
1049    fn test_client_config_from_core_buffer_size_conversion() {
1050        // Test that buffer_size is correctly converted from usize to u64
1051        let core_config = CoreClientConfig {
1052            buffer_size: Some(16384),
1053            ..Default::default()
1054        };
1055
1056        let ffi_config: ClientConfig = core_config.into();
1057
1058        assert_eq!(ffi_config.buffer_size, Some(16384u64));
1059    }
1060
1061    #[test]
1062    fn test_client_config_from_core_metadata_serialization_failure() {
1063        // Test that invalid metadata (non-serializable) results in None
1064        // metadata is already Option, so we just test with None
1065        let core_config = CoreClientConfig {
1066            metadata: None,
1067            ..Default::default()
1068        };
1069
1070        let ffi_config: ClientConfig = core_config.into();
1071
1072        assert!(ffi_config.metadata.is_none());
1073    }
1074
1075    #[test]
1076    fn test_client_config_from_core_fixed_interval_backoff() {
1077        use slim_config::backoff::fixedinterval::Config as CoreFixedIntervalBackoffConfig;
1078        use slim_config::grpc::client::BackoffConfig as CoreBackoffConfig;
1079
1080        let core_config = CoreClientConfig {
1081            backoff: CoreBackoffConfig::FixedInterval(CoreFixedIntervalBackoffConfig {
1082                interval: Duration::from_secs(5).into(),
1083                max_attempts: 8,
1084            }),
1085            ..Default::default()
1086        };
1087
1088        let ffi_config: ClientConfig = core_config.into();
1089
1090        match ffi_config.backoff.unwrap() {
1091            BackoffConfig::FixedInterval { config } => {
1092                assert_eq!(config.interval, Duration::from_secs(5));
1093                assert_eq!(config.max_attempts, 8);
1094            }
1095            _ => panic!("Expected FixedInterval backoff"),
1096        }
1097    }
1098
1099    #[test]
1100    fn test_client_config_from_core_auth_types() {
1101        use slim_config::auth::basic::Config as BasicAuthConfig;
1102        use slim_config::grpc::client::AuthenticationConfig as CoreAuthConfig;
1103
1104        // Test with Basic auth
1105        let core_config = CoreClientConfig {
1106            auth: CoreAuthConfig::Basic(BasicAuthConfig::new("test_user", "test_pass")),
1107            ..Default::default()
1108        };
1109
1110        let ffi_config: ClientConfig = core_config.into();
1111
1112        match ffi_config.auth.unwrap() {
1113            ClientAuthenticationConfig::Basic { config } => {
1114                assert_eq!(config.username, "test_user");
1115                assert_eq!(config.password, "test_pass");
1116            }
1117            _ => panic!("Expected Basic auth"),
1118        }
1119    }
1120}