1use 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#[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#[derive(uniffi::Record, Clone, Debug, PartialEq)]
69pub struct KeepaliveConfig {
70 pub tcp_keepalive: Duration,
72 pub http2_keepalive: Duration,
74 pub timeout: Duration,
76 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#[derive(uniffi::Record, Clone, Debug, PartialEq)]
116pub struct ProxyConfig {
117 pub url: Option<String>,
119 pub tls: TlsClientConfig,
121 pub username: Option<String>,
123 pub password: Option<String>,
125 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#[derive(uniffi::Record, Clone, Debug, PartialEq)]
168pub struct ExponentialBackoff {
169 pub base: Duration,
171 pub factor: u64,
173 pub max_delay: Duration,
175 pub max_attempts: u64,
177 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#[derive(uniffi::Record, Clone, Debug, PartialEq)]
196pub struct FixedIntervalBackoff {
197 pub interval: Duration,
199 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#[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#[derive(uniffi::Record, Clone, Debug, PartialEq)]
263pub struct ClientConfig {
264 pub endpoint: String,
266
267 pub transport: Option<TransportProtocol>,
269
270 pub websocket_auth_query_param: Option<String>,
272
273 pub tls: TlsClientConfig,
275
276 pub origin: Option<String>,
278
279 pub server_name: Option<String>,
281
282 pub compression: Option<CompressionType>,
284
285 pub rate_limit: Option<String>,
287
288 pub keepalive: Option<KeepaliveConfig>,
290
291 pub proxy: Option<ProxyConfig>,
293
294 pub connect_timeout: Option<Duration>,
296
297 pub request_timeout: Option<Duration>,
299
300 pub buffer_size: Option<u64>,
302
303 pub headers: Option<HashMap<String, String>>,
305
306 pub auth: Option<ClientAuthenticationConfig>,
308
309 pub backoff: Option<BackoffConfig>,
311
312 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#[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#[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#[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 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 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 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 let core_config = CoreClientConfig::default();
630
631 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 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 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 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 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 let core_auth: slim_config::grpc::client::AuthenticationConfig = auth.into();
873 let roundtrip_auth: ClientAuthenticationConfig = core_auth.into();
874
875 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 } 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 let core_auth: slim_config::grpc::client::AuthenticationConfig = auth.into();
902 let roundtrip_auth: ClientAuthenticationConfig = core_auth.into();
903
904 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 let core_auth: slim_config::grpc::client::AuthenticationConfig = auth.into();
928 let roundtrip_auth: ClientAuthenticationConfig = core_auth.into();
929
930 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 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 let ffi_config: ClientConfig = core_config.clone().into();
972
973 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 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 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 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 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}