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