use std::future::{Future, ready};
#[cfg(feature = "asyncapi")]
use ruststream::asyncapi::Bindings;
use ruststream::runtime::IntoSource;
use ruststream::{AddressedCopies, RedeliveryAddress, RedeliveryAddressed, SubscriptionSource};
use super::{Assignment, Commit, GroupSettings, LaneKey, Reader, StartOffset, SubscriptionPlan};
use crate::broker::ConnectedKafkaBroker;
use crate::error::KafkaError;
use crate::subscriber::KafkaSubscriber;
#[derive(Debug, Clone)]
pub struct KafkaTopic {
topic: String,
settings: GroupSettings,
}
impl KafkaTopic {
#[must_use]
pub fn new(topic: impl Into<String>) -> Self {
Self {
topic: topic.into(),
settings: GroupSettings::default(),
}
}
#[must_use]
pub fn group(mut self, group: impl Into<String>) -> Self {
self.settings.group = Some(group.into());
self
}
#[must_use]
pub fn start(mut self, start: StartOffset) -> Self {
self.settings.start = start;
self
}
#[must_use]
pub fn commit(mut self, commit: Commit) -> Self {
self.settings.commit = commit;
self
}
#[must_use]
pub fn assignment(mut self, assignment: Assignment) -> Self {
self.settings.assignment = Some(assignment);
self
}
#[must_use]
pub fn lane_key(mut self, lane_key: LaneKey) -> Self {
self.settings.lane_key = lane_key;
self
}
#[must_use]
pub fn config(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.settings.config.set(key.into(), value.into());
self
}
#[must_use]
pub fn topic(&self) -> &str {
&self.topic
}
#[cfg(feature = "asyncapi")]
fn channel_bindings(&self) -> Bindings {
crate::bindings::channel(&self.topic)
}
#[cfg(feature = "asyncapi")]
fn operation_bindings(&self) -> Bindings {
crate::bindings::operation(self.settings.group.as_deref(), &self.settings.config)
}
fn into_plan(self) -> Result<SubscriptionPlan, KafkaError> {
super::reject_empty(&self.topic)?;
super::reject_pattern(&self.topic, "`KafkaTopic`")?;
super::reject_commit_mode_clash(&self.topic, &self.settings.commit, &self.settings.config)?;
Ok(SubscriptionPlan {
name: self.topic.clone(),
reader: Reader::Subscribed(vec![self.topic]),
settings: self.settings,
})
}
}
impl IntoSource for KafkaTopic {
type Source = Self;
fn into_source(self) -> Self {
self
}
}
impl SubscriptionSource<ConnectedKafkaBroker> for KafkaTopic {
type Subscriber = KafkaSubscriber;
type Copies = AddressedCopies;
fn name(&self) -> &str {
&self.topic
}
async fn subscribe(
self,
connected: &ConnectedKafkaBroker,
) -> Result<Self::Subscriber, KafkaError> {
connected.open(self.into_plan()?).await
}
#[cfg(feature = "asyncapi")]
fn channel_bindings(&self) -> Bindings {
Self::channel_bindings(self)
}
#[cfg(feature = "asyncapi")]
fn operation_bindings(&self) -> Bindings {
Self::operation_bindings(self)
}
}
impl RedeliveryAddressed<ConnectedKafkaBroker> for KafkaTopic {
fn redelivery_address(
&self,
_connected: &ConnectedKafkaBroker,
) -> impl Future<Output = Result<RedeliveryAddress, KafkaError>> {
ready(Ok(RedeliveryAddress::new(self.topic.clone())))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_plain_subscription_plans_one_subscribed_topic() {
let plan = KafkaTopic::new("orders")
.group("orders-svc")
.into_plan()
.expect("a literal topic name is a valid single-topic subscription");
assert_eq!(plan.name, "orders");
assert!(matches!(plan.reader, Reader::Subscribed(names) if names == ["orders"]));
}
#[test]
fn a_pattern_name_is_refused_and_points_at_the_pattern_form() {
let err = KafkaTopic::new("^orders\\..*")
.into_plan()
.expect_err("a topic regex is not a single topic");
let KafkaError::InvalidOptions(message) = err else {
panic!("a refused name must be an options error");
};
assert!(
message.contains("KafkaTopics::pattern"),
"the error must name the form that does subscribe by pattern, got: {message}"
);
}
#[test]
fn an_empty_topic_name_is_refused() {
let err = KafkaTopic::new("")
.into_plan()
.expect_err("an empty name subscribes to nothing");
assert!(matches!(err, KafkaError::InvalidOptions(_)));
}
}