Skip to main content

kafka_client/
lib.rs

1//! Kafka Rust Client
2//!
3//! A pure Rust Kafka client library based on Tokio async runtime.
4//! Supports SASL authentication (PLAIN, SCRAM-SHA-256, SCRAM-SHA-512, GSSAPI/Kerberos)
5//! and TLS encryption.
6//!
7//! # Quick Start
8//!
9//! ```ignore
10//! use kafka_client::Client;
11//!
12//! // Create client — connects to cluster, discovers all brokers
13//! let client = Client::builder(vec!["localhost:9092".into()])
14//!     .with_plaintext()
15//!     .build()
16//!     .await?;
17//!
18//! // Producer — send messages
19//! let producer = client.producer_default().await;
20//! producer.send(ProducerRecord::new("my-topic", b"hello".into())).await?;
21//!
22//! // Consumer — read messages
23//! let mut consumer = client.consumer_default();
24//! consumer.subscribe(vec!["my-topic".into()]).await?;
25//! let records = consumer.poll().await?;
26//!
27//! client.close().await?;
28//! ```
29//!
30//! # Advanced Configuration
31//!
32//! ```ignore
33//! // Custom producer config
34//! let producer = client.producer(
35//!     ProducerConfig::new().with_acks(-1).with_retries(3)
36//! ).await;
37//!
38//! // Consumer with group coordination  
39//! let mut consumer = client.consumer(
40//!     ConsumerConfig::new("my-group").with_earliest()
41//! );
42//! ```
43
44// Internal modules (layered architecture)
45pub mod admin;
46mod cluster;
47pub mod connection; // Public for advanced users who need low-level access
48mod consumer;
49mod error;
50mod producer;
51mod sasl;
52pub mod transport; // Public for advanced users who need low-level access
53mod wire;
54
55// Public re-exports
56pub use error::{KafkaError, KafkaErrorCode, Result};
57pub use kafka_client_protocol as protocol;
58pub use krb5_gss::gss::GssContext;
59pub use krb5_gss::{KerberosCredentials, KerberosError};
60pub use sasl::{SaslCredentials, SaslMechanismType};
61pub use transport::{SecurityProtocol, TlsConfig};
62
63// Producer types
64pub use producer::{
65    Header, PartitionRouter, PartitionRouting, Producer, ProducerConfig, ProducerRecord,
66    RecordMetadata,
67};
68
69// Consumer types
70pub use consumer::{
71    AutoOffsetReset, Consumer, ConsumerConfig, ConsumerRecord, ConsumerStream, GroupHandle,
72    OffsetHandle, PartitionAssignmentStrategy,
73};
74
75// Metadata types (read-only queries)
76pub use cluster::MetadataCache;
77
78/// Library name
79pub const NAME: &str = env!("CARGO_PKG_NAME");
80
81/// Library version
82pub const VERSION: &str = env!("CARGO_PKG_VERSION");
83
84use std::sync::Arc;
85use std::time::Duration;
86
87use crate::cluster::ClusterClient;
88
89// ===========================================================================
90// Client — unified entry point
91// ===========================================================================
92
93/// Unified Kafka client.
94///
95/// Manages the lifecycle of the connection to a Kafka cluster internally.
96/// Provides factory methods for creating [`Producer`] and [`Consumer`] instances.
97///
98/// # Examples
99///
100/// ```ignore
101/// use kafka_client::Client;
102///
103/// let client = Client::builder(vec!["localhost:9092".into()])
104///     .with_plaintext()
105///     .build()
106///     .await?;
107///
108/// let producer = client.producer_default().await;
109/// let consumer = client.consumer_default();
110/// ```
111pub struct Client {
112    cluster: Arc<ClusterClient>,
113}
114
115impl Client {
116    /// Create a builder for constructing the client.
117    ///
118    /// Accepts hostnames or IP addresses (e.g. `"localhost:9092"`).
119    /// Hostnames are resolved during `build()`.
120    pub fn builder(bootstrap_servers: Vec<String>) -> ClientBuilder {
121        ClientBuilder::new(bootstrap_servers)
122    }
123
124    // ------------------------------------------------------------------
125    // Producer factories
126    // ------------------------------------------------------------------
127
128    /// Create a [`Producer`] with default configuration.
129    ///
130    /// Equivalent to `client.producer(ProducerConfig::default()).await`.
131    pub async fn producer_default(&self) -> Producer {
132        Producer::new(self.cluster.clone(), ProducerConfig::default()).await
133    }
134
135    /// Create a [`Producer`] with custom configuration.
136    ///
137    /// # Example
138    ///
139    /// ```ignore
140    /// let producer = client.producer(
141    ///     ProducerConfig::new().with_acks(-1).with_retries(5)
142    /// ).await?;
143    /// ```
144    pub async fn producer(&self, config: ProducerConfig) -> Producer {
145        Producer::new(self.cluster.clone(), config).await
146    }
147
148    // ------------------------------------------------------------------
149    // Consumer factories
150    // ------------------------------------------------------------------
151
152    /// Create a [`Consumer`] with default configuration.
153    ///
154    /// Creates a direct-mode consumer (no consumer group). All partitions
155    /// of the subscribed topics are fetched directly from the cluster.
156    /// Use [`Consumer`](Consumer) with `ConsumerConfig::new("my-group")`
157    /// for group-coordinated consumption.
158    pub fn consumer_default(&self) -> Consumer {
159        Consumer::new(self.cluster.clone(), ConsumerConfig::default())
160    }
161
162    /// Create a [`Consumer`] with custom configuration.
163    ///
164    /// # Example
165    ///
166    /// ```ignore
167    /// // Simple consumer (no consumer group)
168    /// let consumer = client.consumer(ConsumerConfig::default());
169    ///
170    /// // Group consumer
171    /// let consumer = client.consumer(
172    ///     ConsumerConfig::new("my-group").with_earliest()
173    /// );
174    /// ```
175    pub fn consumer(&self, config: ConsumerConfig) -> Consumer {
176        Consumer::new(self.cluster.clone(), config)
177    }
178
179    // ------------------------------------------------------------------
180    // Admin client
181    // ------------------------------------------------------------------
182
183    /// Create an [`AdminClient`](admin::AdminClient) for cluster management.
184    ///
185    /// Used for creating/deleting topics, listing groups, describing
186    /// the cluster, and other administrative operations.
187    pub fn admin(&self) -> admin::AdminClient {
188        admin::AdminClient::new(self.cluster.clone())
189    }
190
191    // ------------------------------------------------------------------
192    // Metadata (read-only)
193    // ------------------------------------------------------------------
194
195    /// Get a reference to the metadata cache.
196    ///
197    /// Useful for discovering topics, partitions, and broker addresses
198    /// without sending RPC requests.
199    pub fn metadata(&self) -> &MetadataCache {
200        self.cluster.metadata()
201    }
202
203    // ------------------------------------------------------------------
204    // Lifecycle
205    // ------------------------------------------------------------------
206
207    /// Force a metadata refresh from the cluster.
208    ///
209    /// Useful when you need up-to-date partition leadership information
210    /// before admin operations.
211    pub async fn refresh_metadata(&self) -> Result<()> {
212        self.cluster.refresh_metadata().await
213    }
214
215    /// Send a request to any available broker (advanced usage).
216    ///
217    /// Useful for admin operations like creating/deleting topics,
218    /// or custom protocol requests.
219    ///
220    /// # Example
221    ///
222    /// ```ignore
223    /// use kafka_client::protocol::{CreateTopicsRequest, CreateTopicsResponse};
224    ///
225    /// let response: CreateTopicsResponse = client.send_to_any_broker(&request).await?;
226    /// ```
227    pub async fn send_to_any_broker<Req, Resp>(&self, request: &Req) -> Result<Resp>
228    where
229        Req: kafka_client_protocol::Request,
230        Resp: kafka_client_protocol::Response,
231    {
232        self.cluster.send_to_any_broker(request).await
233    }
234
235    /// Close the client, releasing all broker connections.
236    pub async fn close(&self) -> Result<()> {
237        self.cluster.close().await
238    }
239}
240
241// ===========================================================================
242// ClientConfig — declarative configuration
243// ===========================================================================
244
245/// Declarative configuration for creating a [`Client`].
246///
247/// Alternative to the [`ClientBuilder`] — useful when config comes from
248/// a file, environment, or serialized source.
249///
250/// # Example
251/// ```ignore
252/// use kafka_client::{Client, ClientConfig};
253/// let client = Client::connect(ClientConfig {
254///     bootstrap_servers: vec!["localhost:9092".into()],
255///     client_id: "my-app".into(),
256///     ..Default::default()
257/// }).await?;
258/// ```
259pub struct ClientConfig {
260    /// Bootstrap server addresses (host:port strings).
261    pub bootstrap_servers: Vec<String>,
262    /// Security protocol. Defaults to `Plaintext` via `ClientBuilder`.
263    pub security_protocol: crate::transport::SecurityProtocol,
264    /// Client ID sent to Kafka brokers.
265    pub client_id: String,
266    /// SASL credentials (PLAIN, SCRAM-SHA-256, SCRAM-SHA-512).
267    pub sasl: Option<crate::sasl::SaslCredentials>,
268    /// Kerberos credentials (principal + keytab).
269    pub kerberos: Option<krb5_gss::KerberosCredentials>,
270    /// KDC hostname (Kerberos only).
271    pub kdc_host: Option<String>,
272    /// KDC port (default 88).
273    pub kdc_port: u16,
274    /// Broker hostname for the Kerberos service principal.
275    pub broker_hostname: Option<String>,
276    /// Metadata cache TTL (default 5 minutes).
277    pub metadata_ttl: Duration,
278}
279
280impl Default for ClientConfig {
281    fn default() -> Self {
282        Self {
283            bootstrap_servers: Vec::new(),
284            security_protocol: crate::transport::SecurityProtocol::Plaintext,
285            client_id: NAME.to_string(),
286            sasl: None,
287            kerberos: None,
288            kdc_host: None,
289            kdc_port: 88,
290            broker_hostname: None,
291            metadata_ttl: Duration::from_secs(300),
292        }
293    }
294}
295
296impl ClientConfig {
297    /// Apply SASL credentials and set security protocol to `SaslPlaintext`.
298    pub fn with_sasl(
299        mut self,
300        mechanism: SaslMechanismType,
301        username: String,
302        password: String,
303    ) -> Self {
304        self.sasl = Some(SaslCredentials::new(mechanism, username, password));
305        self.security_protocol = crate::transport::SecurityProtocol::SaslPlaintext;
306        self
307    }
308}
309
310// ===========================================================================
311// ClientBuilder — chainable builder (wraps ClientConfig)
312// ===========================================================================
313
314/// Builder for constructing a [`Client`].
315///
316/// Supports plaintext, TLS, SASL (PLAIN, SCRAM-SHA-256, SCRAM-SHA-512),
317/// and Kerberos (SASL/GSSAPI) authentication.
318///
319/// # Example
320/// ```ignore
321/// let client = Client::builder(vec!["localhost:9092".into()])
322///     .with_kerberos(creds)
323///     .with_kdc("kdc.example.com", 88)
324///     .build()
325///     .await?;
326/// ```
327pub struct ClientBuilder {
328    config: ClientConfig,
329}
330
331impl ClientBuilder {
332    /// Create a new builder with the given bootstrap servers.
333    pub fn new(bootstrap_servers: Vec<String>) -> Self {
334        Self {
335            config: ClientConfig {
336                bootstrap_servers,
337                security_protocol: crate::transport::SecurityProtocol::Plaintext,
338                client_id: NAME.to_string(),
339                sasl: None,
340                kerberos: None,
341                metadata_ttl: Duration::from_secs(300),
342                kdc_host: None,
343                kdc_port: 88,
344                broker_hostname: None,
345            },
346        }
347    }
348
349    // --- Security protocol ---
350
351    /// Use plaintext (no encryption, no authentication).
352    pub fn with_plaintext(mut self) -> Self {
353        self.config.security_protocol = crate::transport::SecurityProtocol::Plaintext;
354        self
355    }
356
357    /// Use TLS encryption with the given domain.
358    pub fn with_tls(mut self, domain: impl Into<String>) -> Self {
359        self.config.security_protocol =
360            crate::transport::SecurityProtocol::Ssl(crate::transport::TlsConfig {
361                domain: domain.into(),
362                ..Default::default()
363            });
364        self
365    }
366
367    /// Use TLS with full custom configuration.
368    pub fn with_tls_config(mut self, tls_config: crate::transport::TlsConfig) -> Self {
369        self.config.security_protocol = crate::transport::SecurityProtocol::Ssl(tls_config);
370        self
371    }
372
373    // --- SASL ---
374
375    /// Configure SASL authentication with a custom mechanism.
376    ///
377    /// Shortcut for `.with_sasl_credentials(...)` that also sets `SaslPlaintext`.
378    pub fn with_sasl(
379        mut self,
380        mechanism: SaslMechanismType,
381        username: impl Into<String>,
382        password: impl Into<String>,
383    ) -> Self {
384        self.config.sasl = Some(SaslCredentials::new(mechanism, username, password));
385        self.config.security_protocol = crate::transport::SecurityProtocol::SaslPlaintext;
386        self
387    }
388
389    /// Configure SASL + TLS authentication.
390    pub fn with_sasl_tls(
391        mut self,
392        tls_config: crate::transport::TlsConfig,
393        mechanism: SaslMechanismType,
394        username: impl Into<String>,
395        password: impl Into<String>,
396    ) -> Self {
397        self.config.sasl = Some(SaslCredentials::new(mechanism, username, password));
398        self.config.security_protocol = crate::transport::SecurityProtocol::SaslSsl(tls_config);
399        self
400    }
401
402    // Convenience SASL shortcuts
403
404    /// SASL PLAIN without TLS.
405    pub fn with_sasl_plaintext(
406        self,
407        username: impl Into<String>,
408        password: impl Into<String>,
409    ) -> Self {
410        self.with_sasl(SaslMechanismType::Plain, username, password)
411    }
412
413    /// SASL PLAIN with TLS (domain-based config).
414    pub fn with_sasl_ssl(
415        self,
416        domain: impl Into<String>,
417        username: impl Into<String>,
418        password: impl Into<String>,
419    ) -> Self {
420        let tls_config = crate::transport::TlsConfig {
421            domain: domain.into(),
422            ..Default::default()
423        };
424        self.with_sasl_tls(tls_config, SaslMechanismType::Plain, username, password)
425    }
426
427    /// Set SASL credentials without modifying the security protocol.
428    ///
429    /// Use this when you need to set SASL + TLS separately:
430    /// ```ignore
431    /// Client::builder(servers)
432    ///     .with_tls(domain)
433    ///     .with_sasl_credentials(mech, user, pass)
434    ///     .build()
435    /// ```
436    pub fn with_sasl_credentials(
437        mut self,
438        mechanism: SaslMechanismType,
439        username: impl Into<String>,
440        password: impl Into<String>,
441    ) -> Self {
442        self.config.sasl = Some(SaslCredentials::new(mechanism, username, password));
443        self
444    }
445
446    // --- Kerberos ---
447
448    /// Set Kerberos credentials without modifying the security protocol.
449    ///
450    /// Use together with [`with_tls`](Self::with_tls) for TLS-secured Kerberos:
451    /// ```ignore
452    /// Client::builder(servers)
453    ///     .with_tls("broker.example.com")
454    ///     .with_kerberos(creds)
455    ///     .with_kdc("kdc.example.com", 88)
456    ///     .build()
457    /// ```
458    /// Or use [`with_kerberos_tls`](Self::with_kerberos_tls) for a single call.
459    pub fn with_kerberos(mut self, credentials: krb5_gss::KerberosCredentials) -> Self {
460        self.config.kerberos = Some(credentials);
461        self
462    }
463
464    /// Configure Kerberos + TLS in one call.
465    pub fn with_kerberos_tls(
466        mut self,
467        tls_config: crate::transport::TlsConfig,
468        credentials: krb5_gss::KerberosCredentials,
469    ) -> Self {
470        self.config.kerberos = Some(credentials);
471        self.config.security_protocol = crate::transport::SecurityProtocol::SaslSsl(tls_config);
472        self
473    }
474
475    /// Set the KDC address (host:port). Only effective when Kerberos is enabled.
476    pub fn with_kdc(mut self, host: impl Into<String>, port: u16) -> Self {
477        self.config.kdc_host = Some(host.into());
478        self.config.kdc_port = port;
479        self
480    }
481
482    /// Set the broker hostname used in the Kerberos service principal.
483    pub fn with_broker_hostname(mut self, host: impl Into<String>) -> Self {
484        self.config.broker_hostname = Some(host.into());
485        self
486    }
487
488    // --- Other settings ---
489
490    /// Set a custom client ID (sent to Kafka brokers).
491    pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
492        self.config.client_id = client_id.into();
493        self
494    }
495
496    /// Override the metadata cache TTL. Default is 5 minutes.
497    pub fn with_metadata_ttl(mut self, ttl: Duration) -> Self {
498        self.config.metadata_ttl = ttl;
499        self
500    }
501
502    // --- Build ---
503
504    /// Connect to the cluster and build the [`Client`].
505    pub async fn build(self) -> Result<Client> {
506        Client::connect(self.config).await
507    }
508}
509
510impl Client {
511    /// Connect to a Kafka cluster from a [`ClientConfig`].
512    pub async fn connect(config: ClientConfig) -> Result<Self> {
513        let mut resolved = Vec::with_capacity(config.bootstrap_servers.len());
514        for server in &config.bootstrap_servers {
515            match tokio::net::lookup_host(server).await {
516                Ok(mut addrs) => {
517                    if let Some(addr) = addrs.next() {
518                        resolved.push(addr);
519                    } else {
520                        return Err(KafkaError::Io(format!(
521                            "Failed to resolve bootstrap server: {}",
522                            server
523                        )));
524                    }
525                }
526                Err(e) => {
527                    return Err(KafkaError::Io(format!(
528                        "Failed to resolve bootstrap server '{}': {}",
529                        server, e
530                    )));
531                }
532            }
533        }
534
535        let cluster_config = crate::cluster::ClusterConfig {
536            bootstrap_servers: resolved,
537            security_protocol: config.security_protocol,
538            client_id: config.client_id,
539            metadata_ttl: config.metadata_ttl,
540            sasl: config.sasl,
541            kerberos: config.kerberos,
542            kdc_host: config.kdc_host,
543            kdc_port: config.kdc_port,
544            broker_hostname: config.broker_hostname,
545        };
546
547        let cluster = ClusterClient::connect(cluster_config).await?;
548        Ok(Client {
549            cluster: Arc::new(cluster),
550        })
551    }
552}
553
554/// Convenience builder function — equivalent to `Client::builder(...)`.
555pub fn builder(bootstrap_servers: Vec<String>) -> ClientBuilder {
556    ClientBuilder::new(bootstrap_servers)
557}