ruststream_rdkafka/testing/
broker.rs1use std::fmt;
4use std::sync::{Arc, OnceLock};
5
6use bytes::Bytes;
7use ruststream::testing::{Coordinator, TestableBroker};
8use ruststream::{Broker, DescribeServer, OutgoingMessage, RawMessage, ServerSpec, Subscribe};
9
10use super::publisher::KafkaTestPublisher;
11use super::router::KeyRouter;
12use super::subscriber::KafkaTestSubscriber;
13use crate::error::KafkaError;
14
15pub(crate) struct TestBrokerState {
16 pub(crate) router: KeyRouter,
17 coordinator: OnceLock<Coordinator>,
18}
19
20impl TestBrokerState {
21 pub(crate) fn install(&self, coordinator: Coordinator) {
22 let _ = self.coordinator.set(coordinator);
25 }
26
27 pub(crate) fn coordinator(&self) -> Option<Coordinator> {
28 self.coordinator.get().cloned()
29 }
30}
31
32impl Default for TestBrokerState {
33 fn default() -> Self {
34 Self {
35 router: KeyRouter::default(),
36 coordinator: OnceLock::new(),
37 }
38 }
39}
40
41impl fmt::Debug for TestBrokerState {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 f.debug_struct("TestBrokerState")
44 .field("router", &self.router)
45 .finish_non_exhaustive()
46 }
47}
48
49#[derive(Debug, Clone, Default)]
68pub struct KafkaTestBroker {
69 state: Arc<TestBrokerState>,
70}
71
72impl KafkaTestBroker {
73 #[must_use]
75 pub fn new() -> Self {
76 Self::default()
77 }
78
79 #[allow(clippy::unused_async)]
87 pub async fn subscribe(
88 &self,
89 topic: impl Into<String>,
90 ) -> Result<KafkaTestSubscriber, KafkaError> {
91 self.subscribe_topics(std::slice::from_ref(&topic.into()))
92 .await
93 }
94
95 #[allow(clippy::unused_async)]
105 pub async fn subscribe_topics(
106 &self,
107 topics: &[String],
108 ) -> Result<KafkaTestSubscriber, KafkaError> {
109 for topic in topics {
110 if topic.is_empty() {
111 return Err(KafkaError::InvalidOptions(
112 "topic name must not be empty; subscribe with the topic the handler \
113 consumes"
114 .to_owned(),
115 ));
116 }
117 if topic.starts_with('^') {
118 return Err(KafkaError::InvalidOptions(format!(
119 "the in-process test broker routes by exact topic name; the pattern \
120 {topic:?} needs a real cluster",
121 )));
122 }
123 }
124 Ok(KafkaTestSubscriber::open_many(&self.state, topics))
125 }
126
127 #[must_use]
129 pub fn publisher(&self) -> KafkaTestPublisher {
130 KafkaTestPublisher::new(Arc::clone(&self.state))
131 }
132}
133
134impl Broker for KafkaTestBroker {
135 type Error = KafkaError;
136
137 async fn connect(&self) -> Result<(), Self::Error> {
138 Ok(())
139 }
140
141 async fn shutdown(&self) -> Result<(), Self::Error> {
142 self.state.router.clear();
143 Ok(())
144 }
145}
146
147#[allow(clippy::use_self)]
150impl Subscribe for KafkaTestBroker {
151 type Subscriber = KafkaTestSubscriber;
152
153 async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, Self::Error> {
154 KafkaTestBroker::subscribe(self, name).await
155 }
156}
157
158impl DescribeServer for KafkaTestBroker {
159 fn describe_server(&self) -> ServerSpec {
160 ServerSpec::in_process("kafka")
161 }
162}
163
164impl TestableBroker for KafkaTestBroker {
166 fn install_coordinator(&self, coordinator: Coordinator) {
167 self.state.install(coordinator);
168 }
169
170 fn inject(&self, message: OutgoingMessage<'_>) {
171 self.state.router.publish(
172 message.name(),
173 &Bytes::copy_from_slice(message.payload()),
174 message.headers(),
175 self.state.coordinator().as_ref(),
176 );
177 }
178
179 fn published(&self, name: &str) -> Vec<RawMessage> {
180 self.state.router.published(name)
181 }
182}
183
184ruststream::register_testable_broker!(KafkaTestBroker);
185