ruststream_pulsar/
subscription.rs1use std::time::Duration;
8
9use ruststream::SubscriptionSource;
10
11use crate::broker::ConnectedPulsarBroker;
12use crate::error::PulsarError;
13use crate::subscriber::PulsarSubscriber;
14use crate::topic::PulsarTopic;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum SubscriptionType {
19 Exclusive,
21 #[default]
23 Shared,
24 Failover,
26 KeyShared,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
42#[must_use]
43pub struct DeadLetter {
44 pub(crate) topic: String,
45 pub(crate) max_deliveries: usize,
46}
47
48impl DeadLetter {
49 pub fn new(topic: impl Into<String>) -> Self {
51 Self {
52 topic: topic.into(),
53 max_deliveries: 5,
54 }
55 }
56
57 pub fn max_deliveries(mut self, max: usize) -> Self {
59 self.max_deliveries = max;
60 self
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub(crate) enum Topics {
66 List(Vec<String>),
67 Pattern(String),
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
90#[must_use]
91pub struct PulsarSubscription {
92 pub(crate) topics: Topics,
93 pub(crate) subscription: String,
94 pub(crate) sub_type: SubscriptionType,
95 pub(crate) dead_letter: Option<DeadLetter>,
96 pub(crate) ack_timeout: Option<Duration>,
97}
98
99impl PulsarSubscription {
100 pub fn new(topic: impl Into<String>, subscription: impl Into<String>) -> Self {
103 Self {
104 topics: Topics::List(vec![topic.into()]),
105 subscription: subscription.into(),
106 sub_type: SubscriptionType::default(),
107 dead_letter: None,
108 ack_timeout: None,
109 }
110 }
111
112 pub fn topics<I, S>(topics: I, subscription: impl Into<String>) -> Self
114 where
115 I: IntoIterator<Item = S>,
116 S: Into<String>,
117 {
118 Self {
119 topics: Topics::List(topics.into_iter().map(Into::into).collect()),
120 ..Self::new(String::new(), subscription)
121 }
122 }
123
124 pub fn pattern(pattern: impl Into<String>, subscription: impl Into<String>) -> Self {
127 Self {
128 topics: Topics::Pattern(pattern.into()),
129 ..Self::new(String::new(), subscription)
130 }
131 }
132
133 pub fn subscription_type(mut self, sub_type: SubscriptionType) -> Self {
135 self.sub_type = sub_type;
136 self
137 }
138
139 pub fn dead_letter(mut self, dead_letter: DeadLetter) -> Self {
141 self.dead_letter = Some(dead_letter);
142 self
143 }
144
145 pub fn ack_timeout(mut self, timeout: Duration) -> Self {
147 self.ack_timeout = Some(timeout);
148 self
149 }
150
151 #[must_use]
153 pub fn subscription(&self) -> &str {
154 &self.subscription
155 }
156
157 pub(crate) fn display_topic(&self) -> String {
158 match &self.topics {
159 Topics::List(topics) => topics.join(","),
160 Topics::Pattern(pattern) => pattern.clone(),
161 }
162 }
163
164 pub(crate) fn validate(&self) -> Result<(), PulsarError> {
166 if self.subscription.is_empty() {
167 return Err(PulsarError::Invalid(
168 "subscription name must be non-empty".into(),
169 ));
170 }
171 match &self.topics {
172 Topics::List(topics) => {
173 if topics.is_empty() || topics.iter().any(String::is_empty) {
174 return Err(PulsarError::Invalid("topics must be non-empty".into()));
175 }
176 for topic in topics {
177 let _ = PulsarTopic::parse(topic)?;
178 }
179 }
180 Topics::Pattern(pattern) => {
181 regex::Regex::new(pattern).map_err(|e| {
182 PulsarError::Invalid(format!("invalid topic pattern '{pattern}': {e}"))
183 })?;
184 }
185 }
186 Ok(())
187 }
188}
189
190impl SubscriptionSource<ConnectedPulsarBroker> for PulsarSubscription {
191 type Subscriber = PulsarSubscriber;
192
193 fn name(&self) -> &str {
194 match &self.topics {
195 Topics::List(topics) if topics.len() == 1 => &topics[0],
196 _ => &self.subscription,
197 }
198 }
199
200 async fn subscribe(
201 self,
202 connected: &ConnectedPulsarBroker,
203 ) -> Result<PulsarSubscriber, PulsarError> {
204 connected.subscribe_descriptor(self).await
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn empty_subscription_is_rejected_before_io() {
214 assert!(matches!(
215 PulsarSubscription::new("orders", "").validate(),
216 Err(PulsarError::Invalid(_))
217 ));
218 }
219
220 #[test]
221 fn malformed_topics_are_rejected_before_io() {
222 assert!(matches!(
223 PulsarSubscription::new("a/b", "workers").validate(),
224 Err(PulsarError::Invalid(_))
225 ));
226 }
227
228 #[test]
229 fn malformed_patterns_are_rejected_before_io() {
230 assert!(matches!(
231 PulsarSubscription::pattern("orders-(", "workers").validate(),
232 Err(PulsarError::Invalid(_))
233 ));
234 }
235}