#[cfg(feature = "asyncapi")]
use ruststream::asyncapi::Bindings;
use ruststream::runtime::IntoSource;
use ruststream::{NamedCopies, SubscriptionSource};
use super::{Commit, GroupSettings, LaneKey, Reader, StartOffset, SubscriptionPlan};
use crate::broker::ConnectedKafkaBroker;
use crate::error::KafkaError;
use crate::subscriber::KafkaSubscriber;
#[derive(Debug, Clone)]
pub struct KafkaPartitions {
topic: String,
partitions: Vec<i32>,
settings: GroupSettings,
}
impl KafkaPartitions {
#[must_use]
pub fn new(topic: impl Into<String>, partitions: impl IntoIterator<Item = i32>) -> Self {
Self {
topic: topic.into(),
partitions: partitions.into_iter().collect(),
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 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
}
#[must_use]
pub fn partitions(&self) -> &[i32] {
&self.partitions
}
#[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, "`KafkaPartitions`")?;
if self.partitions.is_empty() {
return Err(KafkaError::InvalidOptions(format!(
"manual assignment of {:?} names no partition, so the reader would take \
nothing; name the partitions it reads",
self.topic,
)));
}
super::reject_commit_mode_clash(&self.topic, &self.settings.commit, &self.settings.config)?;
Ok(SubscriptionPlan {
name: self.topic.clone(),
reader: Reader::Assigned {
topic: self.topic,
partitions: self.partitions,
},
settings: self.settings,
})
}
}
impl IntoSource for KafkaPartitions {
type Source = Self;
fn into_source(self) -> Self {
self
}
}
impl SubscriptionSource<ConnectedKafkaBroker> for KafkaPartitions {
type Subscriber = KafkaSubscriber;
type Copies = NamedCopies;
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)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_reader_plans_the_partitions_it_names() {
let plan = KafkaPartitions::new("orders", [0, 2])
.start(StartOffset::Earliest)
.into_plan()
.expect("two partitions of one topic are a valid assignment");
assert_eq!(plan.name, "orders");
let Reader::Assigned { topic, partitions } = plan.reader else {
panic!("a manual assignment must plan an assigned reader");
};
assert_eq!(topic, "orders");
assert_eq!(partitions, [0, 2]);
}
#[test]
fn naming_no_partition_is_refused() {
let err = KafkaPartitions::new("orders", [])
.into_plan()
.expect_err("a reader over no partition reads nothing");
assert!(matches!(err, KafkaError::InvalidOptions(_)));
}
}