kafrust 0.3.3

A pure Rust Kafka client with no librdkafka or C client binding dependency.
Documentation
#![allow(clippy::expect_used)]

use kafrust::streams::{StreamsGroupHeartbeatSubtopology, StreamsGroupHeartbeatTopology};
use kafrust::{
    AdminClient, ClientConfig, ConsumerConfig, ConsumerGroupConfig, ListGroupsOptions,
    ProducerConfig, SecurityProtocol, ShareConsumerConfig,
};

fn shared_client_config() -> ClientConfig {
    ClientConfig::new(["broker-a:9092", "broker-b:9092"])
        .client_id("shared-client")
        .client_rack("rack-a")
        .request_timeout_ms(1_250)
        .max_response_bytes(2 * 1024 * 1024)
}

fn assert_shared_policy(config: &ClientConfig) {
    assert_eq!(
        config.bootstrap_servers(),
        &["broker-a:9092".to_owned(), "broker-b:9092".to_owned()]
    );
    assert_eq!(config.client_id_ref(), Some("shared-client"));
    assert_eq!(config.client_rack_ref(), Some("rack-a"));
    assert_eq!(config.request_timeout().as_millis(), 1_250);
    assert_eq!(config.max_response_bytes_ref(), 2 * 1024 * 1024);
}

fn assert_mtls_policy(config: &ClientConfig) {
    assert_eq!(config.security_protocol_ref(), SecurityProtocol::Tls);
    assert_eq!(config.tls_client_certificates_der(), &[vec![1, 2, 3]]);
    assert!(config.has_tls_client_private_key());
}

#[test]
fn high_level_builders_accept_the_shared_client_configuration() {
    let shared = shared_client_config();

    let producer = ProducerConfig::new(["ignored:9092"]).with_client_config(shared.clone());
    assert_shared_policy(producer.client_config());

    let consumer = ConsumerConfig::new(["ignored:9092"]).with_client_config(shared.clone());
    assert_shared_policy(consumer.client_config());

    let group =
        ConsumerGroupConfig::new(["ignored:9092"], "orders").with_client_config(shared.clone());
    assert_shared_policy(group.client_config());

    let share =
        ShareConsumerConfig::new(["ignored:9092"], "orders").with_client_config(shared.clone());
    assert_shared_policy(share.client_config());
}

#[test]
fn exposes_modern_list_groups_options() {
    let options = ListGroupsOptions::new()
        .state("Stable")
        .group_type("consumer");

    assert_eq!(options.states_ref(), &["Stable"]);
    assert_eq!(options.group_types_ref(), &["consumer"]);
}

#[test]
fn high_level_builders_forward_mutual_tls_configuration() {
    let producer = ProducerConfig::new(["ignored:9093"])
        .security_protocol(SecurityProtocol::Tls)
        .tls_client_certificate_der([1, 2, 3])
        .tls_client_private_key_der([4, 5, 6])
        .build_config()
        .expect("producer mTLS configuration should validate without connecting");
    assert_mtls_policy(producer.client_config());

    let consumer = ConsumerConfig::new(["ignored:9093"])
        .security_protocol(SecurityProtocol::Tls)
        .tls_client_certificate_der([1, 2, 3])
        .tls_client_private_key_der([4, 5, 6])
        .build_config()
        .expect("consumer mTLS configuration should validate without connecting");
    assert_mtls_policy(consumer.client_config());

    let group = ConsumerGroupConfig::new(["ignored:9093"], "orders")
        .security_protocol(SecurityProtocol::Tls)
        .tls_client_certificate_der([1, 2, 3])
        .tls_client_private_key_der([4, 5, 6])
        .subscribe("orders")
        .build_config()
        .expect("group mTLS configuration should validate without connecting");
    assert_mtls_policy(group.client_config());

    let share = ShareConsumerConfig::new(["ignored:9093"], "orders")
        .security_protocol(SecurityProtocol::Tls)
        .tls_client_certificate_der([1, 2, 3])
        .tls_client_private_key_der([4, 5, 6])
        .subscribe("orders")
        .build_config()
        .expect("ShareConsumer mTLS configuration should validate without connecting");
    assert_mtls_policy(share.client_config());
}

#[test]
fn high_level_builders_offer_connection_free_build_config_preflight() {
    let shared = shared_client_config();

    let producer = ProducerConfig::new(["ignored:9092"])
        .with_client_config(shared.clone())
        .build_config()
        .expect("producer configuration should validate without connecting");
    assert_shared_policy(producer.client_config());

    let consumer = ConsumerConfig::new(["ignored:9092"])
        .with_client_config(shared.clone())
        .build_config()
        .expect("consumer configuration should validate without connecting");
    assert_shared_policy(consumer.client_config());

    let group = ConsumerGroupConfig::new(["ignored:9092"], "orders")
        .with_client_config(shared.clone())
        .subscribe("orders")
        .build_config()
        .expect("group configuration should validate without connecting");
    assert_shared_policy(group.client_config());

    let share = ShareConsumerConfig::new(["ignored:9092"], "orders")
        .with_client_config(shared.clone())
        .subscribe("orders")
        .build_config()
        .expect("share configuration should validate without connecting");
    assert_shared_policy(share.client_config());

    let streams = kafrust::StreamsGroupConfig::new(
        shared.bootstrap_servers().to_vec(),
        "streams",
        StreamsGroupHeartbeatTopology {
            epoch: 1,
            subtopologies: vec![StreamsGroupHeartbeatSubtopology {
                subtopology_id: "subtopology-0".to_owned(),
                source_topics: vec!["orders".to_owned()],
                source_topic_regex: Vec::new(),
                state_changelog_topics: Vec::new(),
                repartition_sink_topics: Vec::new(),
                repartition_source_topics: Vec::new(),
                copartition_groups: Vec::new(),
            }],
        },
    )
    .security_protocol(SecurityProtocol::Tls)
    .tls_client_certificate_der([1, 2, 3])
    .tls_client_private_key_der([4, 5, 6])
    .build_config()
    .expect("Streams configuration should validate without connecting");
    assert_eq!(streams.group_id(), "streams");
    let assignment = kafrust::StreamsGroupSessionAssignment::default();
    assert!(assignment.active_tasks.is_none());

    let admin_config = shared_client_config();
    let admin_metrics = admin_config.metrics_ref();
    let admin = AdminClient::new(admin_config)
        .build_config()
        .expect("admin configuration should validate without connecting");
    assert_eq!(admin.metrics(), admin_metrics);
}