Skip to main content

llm_optimizer_processor/kafka/
config.rs

1//! Kafka configuration structures for sources and sinks.
2//!
3//! This module provides comprehensive configuration types for Kafka consumers and producers,
4//! with support for all major rdkafka settings including security, performance tuning,
5//! and reliability options.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::time::Duration;
10
11/// Kafka source (consumer) configuration.
12///
13/// This structure provides all necessary settings for consuming from Kafka topics,
14/// including consumer group management, offset handling, and performance tuning.
15///
16/// # Examples
17///
18/// ```rust
19/// use processor::kafka::config::{KafkaSourceConfig, SecurityProtocol};
20/// use std::time::Duration;
21///
22/// let config = KafkaSourceConfig::builder()
23///     .bootstrap_servers(vec!["localhost:9092".to_string()])
24///     .group_id("my-consumer-group".to_string())
25///     .topics(vec!["events".to_string(), "metrics".to_string()])
26///     .auto_offset_reset("earliest".to_string())
27///     .enable_auto_commit(false)
28///     .max_poll_records(500)
29///     .session_timeout(Duration::from_secs(30))
30///     .build()
31///     .expect("valid configuration");
32///
33/// assert!(config.validate().is_ok());
34/// ```
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct KafkaSourceConfig {
37    // Connection settings
38    /// Kafka broker addresses (host:port).
39    pub bootstrap_servers: Vec<String>,
40
41    /// Consumer group ID for coordinated consumption.
42    pub group_id: String,
43
44    /// Topics to subscribe to.
45    pub topics: Vec<String>,
46
47    // Consumer settings
48    /// What to do when there is no initial offset: "earliest", "latest", "none".
49    #[serde(default = "default_auto_offset_reset")]
50    pub auto_offset_reset: String,
51
52    /// Enable automatic offset commits.
53    #[serde(default = "default_enable_auto_commit")]
54    pub enable_auto_commit: bool,
55
56    /// Frequency of auto-commits if enabled.
57    #[serde(
58        default = "default_auto_commit_interval",
59        with = "duration_millis"
60    )]
61    pub auto_commit_interval: Duration,
62
63    /// Maximum number of records to poll in a single batch.
64    #[serde(default = "default_max_poll_records")]
65    pub max_poll_records: usize,
66
67    /// Maximum time to wait for poll batch to fill.
68    #[serde(default = "default_fetch_wait_max", with = "duration_millis")]
69    pub fetch_wait_max: Duration,
70
71    /// Minimum bytes to fetch per request.
72    #[serde(default = "default_fetch_min_bytes")]
73    pub fetch_min_bytes: usize,
74
75    /// Maximum bytes to fetch per partition.
76    #[serde(default = "default_max_partition_fetch_bytes")]
77    pub max_partition_fetch_bytes: usize,
78
79    // Session and heartbeat
80    /// Session timeout for consumer group membership.
81    #[serde(default = "default_session_timeout", with = "duration_millis")]
82    pub session_timeout: Duration,
83
84    /// Heartbeat interval to coordinator.
85    #[serde(default = "default_heartbeat_interval", with = "duration_millis")]
86    pub heartbeat_interval: Duration,
87
88    // Security settings
89    /// Security protocol to use.
90    #[serde(default)]
91    pub security_protocol: SecurityProtocol,
92
93    /// SASL mechanism if using SASL authentication.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub sasl_mechanism: Option<SaslMechanism>,
96
97    /// SASL username.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub sasl_username: Option<String>,
100
101    /// SASL password.
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub sasl_password: Option<String>,
104
105    /// SSL configuration.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub ssl_config: Option<SslConfig>,
108
109    // Performance tuning
110    /// Number of consumer threads for parallel consumption.
111    #[serde(default = "default_num_consumer_threads")]
112    pub num_consumer_threads: usize,
113
114    /// Size of the internal queue for buffering messages.
115    #[serde(default = "default_queued_max_messages")]
116    pub queued_max_messages: usize,
117
118    /// Socket receive buffer size.
119    #[serde(default = "default_socket_receive_buffer_bytes")]
120    pub socket_receive_buffer_bytes: usize,
121
122    // Isolation level
123    /// Transaction isolation level: "read_uncommitted" or "read_committed".
124    #[serde(default = "default_isolation_level")]
125    pub isolation_level: String,
126
127    // Additional properties
128    /// Additional rdkafka configuration properties.
129    #[serde(default)]
130    pub additional_properties: HashMap<String, String>,
131}
132
133/// Kafka sink (producer) configuration.
134///
135/// This structure provides all necessary settings for producing to Kafka topics,
136/// including delivery guarantees, batching, compression, and performance tuning.
137///
138/// # Examples
139///
140/// ```rust
141/// use processor::kafka::config::{KafkaSinkConfig, CompressionType};
142/// use std::time::Duration;
143///
144/// let config = KafkaSinkConfig::builder()
145///     .bootstrap_servers(vec!["localhost:9092".to_string()])
146///     .topic("processed-events".to_string())
147///     .compression(CompressionType::Lz4)
148///     .batch_size(16384)
149///     .linger(Duration::from_millis(10))
150///     .acks("all".to_string())
151///     .max_in_flight_requests(5)
152///     .enable_idempotence(true)
153///     .build()
154///     .expect("valid configuration");
155///
156/// assert!(config.validate().is_ok());
157/// ```
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct KafkaSinkConfig {
160    // Connection settings
161    /// Kafka broker addresses (host:port).
162    pub bootstrap_servers: Vec<String>,
163
164    /// Default topic to produce to.
165    pub topic: String,
166
167    // Producer settings
168    /// Number of acknowledgments required: "0", "1", or "all".
169    #[serde(default = "default_acks")]
170    pub acks: String,
171
172    /// Number of retries for transient errors.
173    #[serde(default = "default_retries")]
174    pub retries: u32,
175
176    /// Maximum in-flight requests per connection.
177    #[serde(default = "default_max_in_flight_requests")]
178    pub max_in_flight_requests: usize,
179
180    /// Enable idempotent producer.
181    #[serde(default = "default_enable_idempotence")]
182    pub enable_idempotence: bool,
183
184    /// Request timeout.
185    #[serde(default = "default_request_timeout", with = "duration_millis")]
186    pub request_timeout: Duration,
187
188    // Batching and buffering
189    /// Batch size in bytes.
190    #[serde(default = "default_batch_size")]
191    pub batch_size: usize,
192
193    /// Time to wait before sending a batch (even if not full).
194    #[serde(default = "default_linger", with = "duration_millis")]
195    pub linger: Duration,
196
197    /// Total memory buffer for producer.
198    #[serde(default = "default_buffer_memory")]
199    pub buffer_memory: usize,
200
201    /// Maximum time to block on send.
202    #[serde(default = "default_max_block", with = "duration_millis")]
203    pub max_block: Duration,
204
205    // Compression
206    /// Compression type for messages.
207    #[serde(default)]
208    pub compression: CompressionType,
209
210    // Message size
211    /// Maximum size of a single message.
212    #[serde(default = "default_max_request_size")]
213    pub max_request_size: usize,
214
215    // Security settings
216    /// Security protocol to use.
217    #[serde(default)]
218    pub security_protocol: SecurityProtocol,
219
220    /// SASL mechanism if using SASL authentication.
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub sasl_mechanism: Option<SaslMechanism>,
223
224    /// SASL username.
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub sasl_username: Option<String>,
227
228    /// SASL password.
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub sasl_password: Option<String>,
231
232    /// SSL configuration.
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub ssl_config: Option<SslConfig>,
235
236    // Performance tuning
237    /// Socket send buffer size.
238    #[serde(default = "default_socket_send_buffer_bytes")]
239    pub socket_send_buffer_bytes: usize,
240
241    /// Size of the internal queue for buffering messages.
242    #[serde(default = "default_queued_max_messages_producer")]
243    pub queued_max_messages: usize,
244
245    // Partitioning
246    /// Custom partitioner class (rdkafka specific).
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub partitioner: Option<String>,
249
250    // Additional properties
251    /// Additional rdkafka configuration properties.
252    #[serde(default)]
253    pub additional_properties: HashMap<String, String>,
254}
255
256/// Security protocol for Kafka connections.
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
258#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
259pub enum SecurityProtocol {
260    /// Plaintext connection (no encryption).
261    #[default]
262    Plaintext,
263    /// SSL/TLS encryption.
264    Ssl,
265    /// SASL authentication over plaintext.
266    SaslPlaintext,
267    /// SASL authentication over SSL/TLS.
268    SaslSsl,
269}
270
271impl SecurityProtocol {
272    /// Convert to rdkafka string representation.
273    pub fn as_str(&self) -> &'static str {
274        match self {
275            SecurityProtocol::Plaintext => "PLAINTEXT",
276            SecurityProtocol::Ssl => "SSL",
277            SecurityProtocol::SaslPlaintext => "SASL_PLAINTEXT",
278            SecurityProtocol::SaslSsl => "SASL_SSL",
279        }
280    }
281}
282
283/// SASL authentication mechanism.
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
285pub enum SaslMechanism {
286    /// PLAIN authentication.
287    Plain,
288    /// SCRAM-SHA-256 authentication.
289    #[serde(rename = "SCRAM-SHA-256")]
290    ScramSha256,
291    /// SCRAM-SHA-512 authentication.
292    #[serde(rename = "SCRAM-SHA-512")]
293    ScramSha512,
294    /// GSSAPI (Kerberos) authentication.
295    Gssapi,
296    /// OAUTHBEARER authentication.
297    OauthBearer,
298}
299
300impl SaslMechanism {
301    /// Convert to rdkafka string representation.
302    pub fn as_str(&self) -> &'static str {
303        match self {
304            SaslMechanism::Plain => "PLAIN",
305            SaslMechanism::ScramSha256 => "SCRAM-SHA-256",
306            SaslMechanism::ScramSha512 => "SCRAM-SHA-512",
307            SaslMechanism::Gssapi => "GSSAPI",
308            SaslMechanism::OauthBearer => "OAUTHBEARER",
309        }
310    }
311}
312
313/// SSL/TLS configuration.
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct SslConfig {
316    /// Path to CA certificate file.
317    pub ca_location: Option<String>,
318
319    /// Path to client certificate file.
320    pub certificate_location: Option<String>,
321
322    /// Path to client private key file.
323    pub key_location: Option<String>,
324
325    /// Password for private key.
326    pub key_password: Option<String>,
327}
328
329/// Compression type for messages.
330#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
331#[serde(rename_all = "lowercase")]
332pub enum CompressionType {
333    /// No compression.
334    #[default]
335    None,
336    /// Gzip compression.
337    Gzip,
338    /// Snappy compression.
339    Snappy,
340    /// LZ4 compression.
341    Lz4,
342    /// Zstandard compression.
343    Zstd,
344}
345
346impl CompressionType {
347    /// Convert to rdkafka string representation.
348    pub fn as_str(&self) -> &'static str {
349        match self {
350            CompressionType::None => "none",
351            CompressionType::Gzip => "gzip",
352            CompressionType::Snappy => "snappy",
353            CompressionType::Lz4 => "lz4",
354            CompressionType::Zstd => "zstd",
355        }
356    }
357}
358
359// Default value functions for KafkaSourceConfig
360fn default_auto_offset_reset() -> String {
361    "latest".to_string()
362}
363
364fn default_enable_auto_commit() -> bool {
365    true
366}
367
368fn default_auto_commit_interval() -> Duration {
369    Duration::from_secs(5)
370}
371
372fn default_max_poll_records() -> usize {
373    500
374}
375
376fn default_fetch_wait_max() -> Duration {
377    Duration::from_millis(500)
378}
379
380fn default_fetch_min_bytes() -> usize {
381    1
382}
383
384fn default_max_partition_fetch_bytes() -> usize {
385    1048576 // 1MB
386}
387
388fn default_session_timeout() -> Duration {
389    Duration::from_secs(30)
390}
391
392fn default_heartbeat_interval() -> Duration {
393    Duration::from_secs(3)
394}
395
396fn default_num_consumer_threads() -> usize {
397    1
398}
399
400fn default_queued_max_messages() -> usize {
401    100000
402}
403
404fn default_socket_receive_buffer_bytes() -> usize {
405    65536 // 64KB
406}
407
408fn default_isolation_level() -> String {
409    "read_uncommitted".to_string()
410}
411
412// Default value functions for KafkaSinkConfig
413fn default_acks() -> String {
414    "all".to_string()
415}
416
417fn default_retries() -> u32 {
418    2147483647 // Max retries
419}
420
421fn default_max_in_flight_requests() -> usize {
422    5
423}
424
425fn default_enable_idempotence() -> bool {
426    true
427}
428
429fn default_request_timeout() -> Duration {
430    Duration::from_secs(30)
431}
432
433fn default_batch_size() -> usize {
434    16384 // 16KB
435}
436
437fn default_linger() -> Duration {
438    Duration::from_millis(0)
439}
440
441fn default_buffer_memory() -> usize {
442    33554432 // 32MB
443}
444
445fn default_max_block() -> Duration {
446    Duration::from_secs(60)
447}
448
449fn default_max_request_size() -> usize {
450    1048576 // 1MB
451}
452
453fn default_socket_send_buffer_bytes() -> usize {
454    131072 // 128KB
455}
456
457fn default_queued_max_messages_producer() -> usize {
458    100000
459}
460
461// Serde helper for Duration as milliseconds
462mod duration_millis {
463    use serde::{Deserialize, Deserializer, Serializer};
464    use std::time::Duration;
465
466    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
467    where
468        S: Serializer,
469    {
470        serializer.serialize_u64(duration.as_millis() as u64)
471    }
472
473    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
474    where
475        D: Deserializer<'de>,
476    {
477        let millis = u64::deserialize(deserializer)?;
478        Ok(Duration::from_millis(millis))
479    }
480}
481
482impl KafkaSourceConfig {
483    /// Create a new builder for KafkaSourceConfig.
484    pub fn builder() -> KafkaSourceConfigBuilder {
485        KafkaSourceConfigBuilder::default()
486    }
487
488    /// Validate the configuration.
489    ///
490    /// # Errors
491    ///
492    /// Returns an error if:
493    /// - Bootstrap servers are empty
494    /// - Group ID is empty
495    /// - Topics are empty
496    /// - Invalid auto_offset_reset value
497    /// - Invalid isolation_level value
498    /// - SASL credentials missing when SASL is enabled
499    pub fn validate(&self) -> Result<(), String> {
500        if self.bootstrap_servers.is_empty() {
501            return Err("bootstrap_servers cannot be empty".to_string());
502        }
503
504        if self.group_id.is_empty() {
505            return Err("group_id cannot be empty".to_string());
506        }
507
508        if self.topics.is_empty() {
509            return Err("topics cannot be empty".to_string());
510        }
511
512        if !["earliest", "latest", "none"].contains(&self.auto_offset_reset.as_str()) {
513            return Err(format!(
514                "invalid auto_offset_reset: {}. Must be 'earliest', 'latest', or 'none'",
515                self.auto_offset_reset
516            ));
517        }
518
519        if !["read_uncommitted", "read_committed"].contains(&self.isolation_level.as_str()) {
520            return Err(format!(
521                "invalid isolation_level: {}. Must be 'read_uncommitted' or 'read_committed'",
522                self.isolation_level
523            ));
524        }
525
526        // Validate SASL configuration
527        if matches!(
528            self.security_protocol,
529            SecurityProtocol::SaslPlaintext | SecurityProtocol::SaslSsl
530        ) {
531            if self.sasl_mechanism.is_none() {
532                return Err("sasl_mechanism required when using SASL security protocol".to_string());
533            }
534            if self.sasl_username.is_none() {
535                return Err("sasl_username required when using SASL security protocol".to_string());
536            }
537            if self.sasl_password.is_none() {
538                return Err("sasl_password required when using SASL security protocol".to_string());
539            }
540        }
541
542        Ok(())
543    }
544
545    /// Convert to rdkafka ClientConfig properties.
546    pub fn to_rdkafka_properties(&self) -> HashMap<String, String> {
547        let mut props = HashMap::new();
548
549        // Connection settings
550        props.insert(
551            "bootstrap.servers".to_string(),
552            self.bootstrap_servers.join(","),
553        );
554        props.insert("group.id".to_string(), self.group_id.clone());
555
556        // Consumer settings
557        props.insert(
558            "auto.offset.reset".to_string(),
559            self.auto_offset_reset.clone(),
560        );
561        props.insert(
562            "enable.auto.commit".to_string(),
563            self.enable_auto_commit.to_string(),
564        );
565        props.insert(
566            "auto.commit.interval.ms".to_string(),
567            self.auto_commit_interval.as_millis().to_string(),
568        );
569        props.insert(
570            "fetch.wait.max.ms".to_string(),
571            self.fetch_wait_max.as_millis().to_string(),
572        );
573        props.insert(
574            "fetch.min.bytes".to_string(),
575            self.fetch_min_bytes.to_string(),
576        );
577        props.insert(
578            "max.partition.fetch.bytes".to_string(),
579            self.max_partition_fetch_bytes.to_string(),
580        );
581
582        // Session and heartbeat
583        props.insert(
584            "session.timeout.ms".to_string(),
585            self.session_timeout.as_millis().to_string(),
586        );
587        props.insert(
588            "heartbeat.interval.ms".to_string(),
589            self.heartbeat_interval.as_millis().to_string(),
590        );
591
592        // Security
593        props.insert(
594            "security.protocol".to_string(),
595            self.security_protocol.as_str().to_string(),
596        );
597
598        if let Some(mechanism) = &self.sasl_mechanism {
599            props.insert("sasl.mechanism".to_string(), mechanism.as_str().to_string());
600        }
601
602        if let Some(username) = &self.sasl_username {
603            props.insert("sasl.username".to_string(), username.clone());
604        }
605
606        if let Some(password) = &self.sasl_password {
607            props.insert("sasl.password".to_string(), password.clone());
608        }
609
610        if let Some(ssl_config) = &self.ssl_config {
611            if let Some(ca_location) = &ssl_config.ca_location {
612                props.insert("ssl.ca.location".to_string(), ca_location.clone());
613            }
614            if let Some(cert_location) = &ssl_config.certificate_location {
615                props.insert("ssl.certificate.location".to_string(), cert_location.clone());
616            }
617            if let Some(key_location) = &ssl_config.key_location {
618                props.insert("ssl.key.location".to_string(), key_location.clone());
619            }
620            if let Some(key_password) = &ssl_config.key_password {
621                props.insert("ssl.key.password".to_string(), key_password.clone());
622            }
623        }
624
625        // Performance tuning
626        props.insert(
627            "queued.max.messages.kbytes".to_string(),
628            (self.queued_max_messages / 1000).to_string(),
629        );
630        props.insert(
631            "receive.message.max.bytes".to_string(),
632            self.socket_receive_buffer_bytes.to_string(),
633        );
634
635        // Isolation level
636        props.insert("isolation.level".to_string(), self.isolation_level.clone());
637
638        // Additional properties (can override defaults)
639        props.extend(self.additional_properties.clone());
640
641        props
642    }
643}
644
645impl KafkaSinkConfig {
646    /// Create a new builder for KafkaSinkConfig.
647    pub fn builder() -> KafkaSinkConfigBuilder {
648        KafkaSinkConfigBuilder::default()
649    }
650
651    /// Validate the configuration.
652    ///
653    /// # Errors
654    ///
655    /// Returns an error if:
656    /// - Bootstrap servers are empty
657    /// - Topic is empty
658    /// - Invalid acks value
659    /// - SASL credentials missing when SASL is enabled
660    pub fn validate(&self) -> Result<(), String> {
661        if self.bootstrap_servers.is_empty() {
662            return Err("bootstrap_servers cannot be empty".to_string());
663        }
664
665        if self.topic.is_empty() {
666            return Err("topic cannot be empty".to_string());
667        }
668
669        if !["0", "1", "all", "-1"].contains(&self.acks.as_str()) {
670            return Err(format!(
671                "invalid acks: {}. Must be '0', '1', or 'all'",
672                self.acks
673            ));
674        }
675
676        // Validate SASL configuration
677        if matches!(
678            self.security_protocol,
679            SecurityProtocol::SaslPlaintext | SecurityProtocol::SaslSsl
680        ) {
681            if self.sasl_mechanism.is_none() {
682                return Err("sasl_mechanism required when using SASL security protocol".to_string());
683            }
684            if self.sasl_username.is_none() {
685                return Err("sasl_username required when using SASL security protocol".to_string());
686            }
687            if self.sasl_password.is_none() {
688                return Err("sasl_password required when using SASL security protocol".to_string());
689            }
690        }
691
692        Ok(())
693    }
694
695    /// Convert to rdkafka ClientConfig properties.
696    pub fn to_rdkafka_properties(&self) -> HashMap<String, String> {
697        let mut props = HashMap::new();
698
699        // Connection settings
700        props.insert(
701            "bootstrap.servers".to_string(),
702            self.bootstrap_servers.join(","),
703        );
704
705        // Producer settings
706        props.insert("acks".to_string(), self.acks.clone());
707        props.insert("retries".to_string(), self.retries.to_string());
708        props.insert(
709            "max.in.flight.requests.per.connection".to_string(),
710            self.max_in_flight_requests.to_string(),
711        );
712        props.insert(
713            "enable.idempotence".to_string(),
714            self.enable_idempotence.to_string(),
715        );
716        props.insert(
717            "request.timeout.ms".to_string(),
718            self.request_timeout.as_millis().to_string(),
719        );
720
721        // Batching and buffering
722        props.insert("batch.size".to_string(), self.batch_size.to_string());
723        props.insert(
724            "linger.ms".to_string(),
725            self.linger.as_millis().to_string(),
726        );
727        props.insert("buffer.memory".to_string(), self.buffer_memory.to_string());
728        props.insert(
729            "max.block.ms".to_string(),
730            self.max_block.as_millis().to_string(),
731        );
732
733        // Compression
734        props.insert(
735            "compression.type".to_string(),
736            self.compression.as_str().to_string(),
737        );
738
739        // Message size
740        props.insert(
741            "max.request.size".to_string(),
742            self.max_request_size.to_string(),
743        );
744
745        // Security
746        props.insert(
747            "security.protocol".to_string(),
748            self.security_protocol.as_str().to_string(),
749        );
750
751        if let Some(mechanism) = &self.sasl_mechanism {
752            props.insert("sasl.mechanism".to_string(), mechanism.as_str().to_string());
753        }
754
755        if let Some(username) = &self.sasl_username {
756            props.insert("sasl.username".to_string(), username.clone());
757        }
758
759        if let Some(password) = &self.sasl_password {
760            props.insert("sasl.password".to_string(), password.clone());
761        }
762
763        if let Some(ssl_config) = &self.ssl_config {
764            if let Some(ca_location) = &ssl_config.ca_location {
765                props.insert("ssl.ca.location".to_string(), ca_location.clone());
766            }
767            if let Some(cert_location) = &ssl_config.certificate_location {
768                props.insert("ssl.certificate.location".to_string(), cert_location.clone());
769            }
770            if let Some(key_location) = &ssl_config.key_location {
771                props.insert("ssl.key.location".to_string(), key_location.clone());
772            }
773            if let Some(key_password) = &ssl_config.key_password {
774                props.insert("ssl.key.password".to_string(), key_password.clone());
775            }
776        }
777
778        // Performance tuning
779        props.insert(
780            "send.buffer.bytes".to_string(),
781            self.socket_send_buffer_bytes.to_string(),
782        );
783        props.insert(
784            "queue.buffering.max.messages".to_string(),
785            self.queued_max_messages.to_string(),
786        );
787
788        // Partitioner
789        if let Some(partitioner) = &self.partitioner {
790            props.insert("partitioner".to_string(), partitioner.clone());
791        }
792
793        // Additional properties (can override defaults)
794        props.extend(self.additional_properties.clone());
795
796        props
797    }
798}
799
800/// Builder for KafkaSourceConfig.
801#[derive(Default)]
802pub struct KafkaSourceConfigBuilder {
803    bootstrap_servers: Option<Vec<String>>,
804    group_id: Option<String>,
805    topics: Option<Vec<String>>,
806    auto_offset_reset: Option<String>,
807    enable_auto_commit: Option<bool>,
808    auto_commit_interval: Option<Duration>,
809    max_poll_records: Option<usize>,
810    fetch_wait_max: Option<Duration>,
811    fetch_min_bytes: Option<usize>,
812    max_partition_fetch_bytes: Option<usize>,
813    session_timeout: Option<Duration>,
814    heartbeat_interval: Option<Duration>,
815    security_protocol: Option<SecurityProtocol>,
816    sasl_mechanism: Option<SaslMechanism>,
817    sasl_username: Option<String>,
818    sasl_password: Option<String>,
819    ssl_config: Option<SslConfig>,
820    num_consumer_threads: Option<usize>,
821    queued_max_messages: Option<usize>,
822    socket_receive_buffer_bytes: Option<usize>,
823    isolation_level: Option<String>,
824    additional_properties: Option<HashMap<String, String>>,
825}
826
827impl KafkaSourceConfigBuilder {
828    pub fn bootstrap_servers(mut self, servers: Vec<String>) -> Self {
829        self.bootstrap_servers = Some(servers);
830        self
831    }
832
833    pub fn group_id(mut self, group_id: String) -> Self {
834        self.group_id = Some(group_id);
835        self
836    }
837
838    pub fn topics(mut self, topics: Vec<String>) -> Self {
839        self.topics = Some(topics);
840        self
841    }
842
843    pub fn auto_offset_reset(mut self, reset: String) -> Self {
844        self.auto_offset_reset = Some(reset);
845        self
846    }
847
848    pub fn enable_auto_commit(mut self, enable: bool) -> Self {
849        self.enable_auto_commit = Some(enable);
850        self
851    }
852
853    pub fn auto_commit_interval(mut self, interval: Duration) -> Self {
854        self.auto_commit_interval = Some(interval);
855        self
856    }
857
858    pub fn max_poll_records(mut self, records: usize) -> Self {
859        self.max_poll_records = Some(records);
860        self
861    }
862
863    pub fn fetch_wait_max(mut self, duration: Duration) -> Self {
864        self.fetch_wait_max = Some(duration);
865        self
866    }
867
868    pub fn fetch_min_bytes(mut self, bytes: usize) -> Self {
869        self.fetch_min_bytes = Some(bytes);
870        self
871    }
872
873    pub fn max_partition_fetch_bytes(mut self, bytes: usize) -> Self {
874        self.max_partition_fetch_bytes = Some(bytes);
875        self
876    }
877
878    pub fn session_timeout(mut self, timeout: Duration) -> Self {
879        self.session_timeout = Some(timeout);
880        self
881    }
882
883    pub fn heartbeat_interval(mut self, interval: Duration) -> Self {
884        self.heartbeat_interval = Some(interval);
885        self
886    }
887
888    pub fn security_protocol(mut self, protocol: SecurityProtocol) -> Self {
889        self.security_protocol = Some(protocol);
890        self
891    }
892
893    pub fn sasl_mechanism(mut self, mechanism: SaslMechanism) -> Self {
894        self.sasl_mechanism = Some(mechanism);
895        self
896    }
897
898    pub fn sasl_username(mut self, username: String) -> Self {
899        self.sasl_username = Some(username);
900        self
901    }
902
903    pub fn sasl_password(mut self, password: String) -> Self {
904        self.sasl_password = Some(password);
905        self
906    }
907
908    pub fn ssl_config(mut self, config: SslConfig) -> Self {
909        self.ssl_config = Some(config);
910        self
911    }
912
913    pub fn num_consumer_threads(mut self, threads: usize) -> Self {
914        self.num_consumer_threads = Some(threads);
915        self
916    }
917
918    pub fn queued_max_messages(mut self, messages: usize) -> Self {
919        self.queued_max_messages = Some(messages);
920        self
921    }
922
923    pub fn socket_receive_buffer_bytes(mut self, bytes: usize) -> Self {
924        self.socket_receive_buffer_bytes = Some(bytes);
925        self
926    }
927
928    pub fn isolation_level(mut self, level: String) -> Self {
929        self.isolation_level = Some(level);
930        self
931    }
932
933    pub fn additional_properties(mut self, properties: HashMap<String, String>) -> Self {
934        self.additional_properties = Some(properties);
935        self
936    }
937
938    pub fn build(self) -> Result<KafkaSourceConfig, String> {
939        let config = KafkaSourceConfig {
940            bootstrap_servers: self
941                .bootstrap_servers
942                .ok_or("bootstrap_servers is required")?,
943            group_id: self.group_id.ok_or("group_id is required")?,
944            topics: self.topics.ok_or("topics is required")?,
945            auto_offset_reset: self.auto_offset_reset.unwrap_or_else(default_auto_offset_reset),
946            enable_auto_commit: self
947                .enable_auto_commit
948                .unwrap_or_else(default_enable_auto_commit),
949            auto_commit_interval: self
950                .auto_commit_interval
951                .unwrap_or_else(default_auto_commit_interval),
952            max_poll_records: self.max_poll_records.unwrap_or_else(default_max_poll_records),
953            fetch_wait_max: self.fetch_wait_max.unwrap_or_else(default_fetch_wait_max),
954            fetch_min_bytes: self.fetch_min_bytes.unwrap_or_else(default_fetch_min_bytes),
955            max_partition_fetch_bytes: self
956                .max_partition_fetch_bytes
957                .unwrap_or_else(default_max_partition_fetch_bytes),
958            session_timeout: self.session_timeout.unwrap_or_else(default_session_timeout),
959            heartbeat_interval: self
960                .heartbeat_interval
961                .unwrap_or_else(default_heartbeat_interval),
962            security_protocol: self.security_protocol.unwrap_or_default(),
963            sasl_mechanism: self.sasl_mechanism,
964            sasl_username: self.sasl_username,
965            sasl_password: self.sasl_password,
966            ssl_config: self.ssl_config,
967            num_consumer_threads: self
968                .num_consumer_threads
969                .unwrap_or_else(default_num_consumer_threads),
970            queued_max_messages: self
971                .queued_max_messages
972                .unwrap_or_else(default_queued_max_messages),
973            socket_receive_buffer_bytes: self
974                .socket_receive_buffer_bytes
975                .unwrap_or_else(default_socket_receive_buffer_bytes),
976            isolation_level: self.isolation_level.unwrap_or_else(default_isolation_level),
977            additional_properties: self.additional_properties.unwrap_or_default(),
978        };
979
980        config.validate()?;
981        Ok(config)
982    }
983}
984
985/// Builder for KafkaSinkConfig.
986#[derive(Default)]
987pub struct KafkaSinkConfigBuilder {
988    bootstrap_servers: Option<Vec<String>>,
989    topic: Option<String>,
990    acks: Option<String>,
991    retries: Option<u32>,
992    max_in_flight_requests: Option<usize>,
993    enable_idempotence: Option<bool>,
994    request_timeout: Option<Duration>,
995    batch_size: Option<usize>,
996    linger: Option<Duration>,
997    buffer_memory: Option<usize>,
998    max_block: Option<Duration>,
999    compression: Option<CompressionType>,
1000    max_request_size: Option<usize>,
1001    security_protocol: Option<SecurityProtocol>,
1002    sasl_mechanism: Option<SaslMechanism>,
1003    sasl_username: Option<String>,
1004    sasl_password: Option<String>,
1005    ssl_config: Option<SslConfig>,
1006    socket_send_buffer_bytes: Option<usize>,
1007    queued_max_messages: Option<usize>,
1008    partitioner: Option<String>,
1009    additional_properties: Option<HashMap<String, String>>,
1010}
1011
1012impl KafkaSinkConfigBuilder {
1013    pub fn bootstrap_servers(mut self, servers: Vec<String>) -> Self {
1014        self.bootstrap_servers = Some(servers);
1015        self
1016    }
1017
1018    pub fn topic(mut self, topic: String) -> Self {
1019        self.topic = Some(topic);
1020        self
1021    }
1022
1023    pub fn acks(mut self, acks: String) -> Self {
1024        self.acks = Some(acks);
1025        self
1026    }
1027
1028    pub fn retries(mut self, retries: u32) -> Self {
1029        self.retries = Some(retries);
1030        self
1031    }
1032
1033    pub fn max_in_flight_requests(mut self, requests: usize) -> Self {
1034        self.max_in_flight_requests = Some(requests);
1035        self
1036    }
1037
1038    pub fn enable_idempotence(mut self, enable: bool) -> Self {
1039        self.enable_idempotence = Some(enable);
1040        self
1041    }
1042
1043    pub fn request_timeout(mut self, timeout: Duration) -> Self {
1044        self.request_timeout = Some(timeout);
1045        self
1046    }
1047
1048    pub fn batch_size(mut self, size: usize) -> Self {
1049        self.batch_size = Some(size);
1050        self
1051    }
1052
1053    pub fn linger(mut self, duration: Duration) -> Self {
1054        self.linger = Some(duration);
1055        self
1056    }
1057
1058    pub fn buffer_memory(mut self, memory: usize) -> Self {
1059        self.buffer_memory = Some(memory);
1060        self
1061    }
1062
1063    pub fn max_block(mut self, duration: Duration) -> Self {
1064        self.max_block = Some(duration);
1065        self
1066    }
1067
1068    pub fn compression(mut self, compression: CompressionType) -> Self {
1069        self.compression = Some(compression);
1070        self
1071    }
1072
1073    pub fn max_request_size(mut self, size: usize) -> Self {
1074        self.max_request_size = Some(size);
1075        self
1076    }
1077
1078    pub fn security_protocol(mut self, protocol: SecurityProtocol) -> Self {
1079        self.security_protocol = Some(protocol);
1080        self
1081    }
1082
1083    pub fn sasl_mechanism(mut self, mechanism: SaslMechanism) -> Self {
1084        self.sasl_mechanism = Some(mechanism);
1085        self
1086    }
1087
1088    pub fn sasl_username(mut self, username: String) -> Self {
1089        self.sasl_username = Some(username);
1090        self
1091    }
1092
1093    pub fn sasl_password(mut self, password: String) -> Self {
1094        self.sasl_password = Some(password);
1095        self
1096    }
1097
1098    pub fn ssl_config(mut self, config: SslConfig) -> Self {
1099        self.ssl_config = Some(config);
1100        self
1101    }
1102
1103    pub fn socket_send_buffer_bytes(mut self, bytes: usize) -> Self {
1104        self.socket_send_buffer_bytes = Some(bytes);
1105        self
1106    }
1107
1108    pub fn queued_max_messages(mut self, messages: usize) -> Self {
1109        self.queued_max_messages = Some(messages);
1110        self
1111    }
1112
1113    pub fn partitioner(mut self, partitioner: String) -> Self {
1114        self.partitioner = Some(partitioner);
1115        self
1116    }
1117
1118    pub fn additional_properties(mut self, properties: HashMap<String, String>) -> Self {
1119        self.additional_properties = Some(properties);
1120        self
1121    }
1122
1123    pub fn build(self) -> Result<KafkaSinkConfig, String> {
1124        let config = KafkaSinkConfig {
1125            bootstrap_servers: self
1126                .bootstrap_servers
1127                .ok_or("bootstrap_servers is required")?,
1128            topic: self.topic.ok_or("topic is required")?,
1129            acks: self.acks.unwrap_or_else(default_acks),
1130            retries: self.retries.unwrap_or_else(default_retries),
1131            max_in_flight_requests: self
1132                .max_in_flight_requests
1133                .unwrap_or_else(default_max_in_flight_requests),
1134            enable_idempotence: self
1135                .enable_idempotence
1136                .unwrap_or_else(default_enable_idempotence),
1137            request_timeout: self.request_timeout.unwrap_or_else(default_request_timeout),
1138            batch_size: self.batch_size.unwrap_or_else(default_batch_size),
1139            linger: self.linger.unwrap_or_else(default_linger),
1140            buffer_memory: self.buffer_memory.unwrap_or_else(default_buffer_memory),
1141            max_block: self.max_block.unwrap_or_else(default_max_block),
1142            compression: self.compression.unwrap_or_default(),
1143            max_request_size: self.max_request_size.unwrap_or_else(default_max_request_size),
1144            security_protocol: self.security_protocol.unwrap_or_default(),
1145            sasl_mechanism: self.sasl_mechanism,
1146            sasl_username: self.sasl_username,
1147            sasl_password: self.sasl_password,
1148            ssl_config: self.ssl_config,
1149            socket_send_buffer_bytes: self
1150                .socket_send_buffer_bytes
1151                .unwrap_or_else(default_socket_send_buffer_bytes),
1152            queued_max_messages: self
1153                .queued_max_messages
1154                .unwrap_or_else(default_queued_max_messages_producer),
1155            partitioner: self.partitioner,
1156            additional_properties: self.additional_properties.unwrap_or_default(),
1157        };
1158
1159        config.validate()?;
1160        Ok(config)
1161    }
1162}
1163
1164#[cfg(test)]
1165mod tests {
1166    use super::*;
1167
1168    #[test]
1169    fn test_kafka_source_config_validation() {
1170        let config = KafkaSourceConfig::builder()
1171            .bootstrap_servers(vec!["localhost:9092".to_string()])
1172            .group_id("test-group".to_string())
1173            .topics(vec!["test-topic".to_string()])
1174            .build();
1175
1176        assert!(config.is_ok());
1177    }
1178
1179    #[test]
1180    fn test_kafka_sink_config_validation() {
1181        let config = KafkaSinkConfig::builder()
1182            .bootstrap_servers(vec!["localhost:9092".to_string()])
1183            .topic("test-topic".to_string())
1184            .build();
1185
1186        assert!(config.is_ok());
1187    }
1188
1189    #[test]
1190    fn test_security_protocol_string_conversion() {
1191        assert_eq!(SecurityProtocol::Plaintext.as_str(), "PLAINTEXT");
1192        assert_eq!(SecurityProtocol::Ssl.as_str(), "SSL");
1193        assert_eq!(SecurityProtocol::SaslPlaintext.as_str(), "SASL_PLAINTEXT");
1194        assert_eq!(SecurityProtocol::SaslSsl.as_str(), "SASL_SSL");
1195    }
1196
1197    #[test]
1198    fn test_compression_type_string_conversion() {
1199        assert_eq!(CompressionType::None.as_str(), "none");
1200        assert_eq!(CompressionType::Gzip.as_str(), "gzip");
1201        assert_eq!(CompressionType::Snappy.as_str(), "snappy");
1202        assert_eq!(CompressionType::Lz4.as_str(), "lz4");
1203        assert_eq!(CompressionType::Zstd.as_str(), "zstd");
1204    }
1205
1206    #[test]
1207    fn test_sasl_mechanism_string_conversion() {
1208        assert_eq!(SaslMechanism::Plain.as_str(), "PLAIN");
1209        assert_eq!(SaslMechanism::ScramSha256.as_str(), "SCRAM-SHA-256");
1210        assert_eq!(SaslMechanism::ScramSha512.as_str(), "SCRAM-SHA-512");
1211    }
1212}