1use std::fmt;
16use std::future::Future;
17use std::pin::Pin;
18use std::str::FromStr;
19use std::sync::Arc;
20use std::sync::atomic::{AtomicBool, Ordering};
21use std::task::{Context, Poll};
22
23use async_trait::async_trait;
24use bytes::BytesMut;
25use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
26use tower::Service;
27use tracing::debug;
28
29use camel_component_api::parse_uri;
30use camel_component_api::{BoxProcessor, CamelError, Exchange, Message};
31use camel_component_api::{
32 Component, ComponentMetadata, Consumer, ConsumerContext, Endpoint, ProducerContext,
33 RuntimeObservability, UriConfig,
34};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum StreamTarget {
43 Out,
45 Err,
47 In,
49}
50
51impl fmt::Display for StreamTarget {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 StreamTarget::Out => write!(f, "out"),
55 StreamTarget::Err => write!(f, "err"),
56 StreamTarget::In => write!(f, "in"),
57 }
58 }
59}
60
61impl FromStr for StreamTarget {
62 type Err = String;
63
64 fn from_str(s: &str) -> Result<Self, String> {
67 match s {
68 "out" => Ok(StreamTarget::Out),
69 "err" => Ok(StreamTarget::Err),
70 "in" => Ok(StreamTarget::In),
71 _ => Err(format!(
72 "unknown stream target: '{}'. Valid: out, err, in",
73 s
74 )),
75 }
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum StreamFrame {
86 Line,
88 Raw,
90 Fixed,
92}
93
94impl fmt::Display for StreamFrame {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 match self {
97 StreamFrame::Line => write!(f, "line"),
98 StreamFrame::Raw => write!(f, "raw"),
99 StreamFrame::Fixed => write!(f, "fixed"),
100 }
101 }
102}
103
104impl FromStr for StreamFrame {
105 type Err = String;
106
107 fn from_str(s: &str) -> Result<Self, Self::Err> {
108 match s {
109 "line" => Ok(StreamFrame::Line),
110 "raw" => Ok(StreamFrame::Raw),
111 "fixed" => Ok(StreamFrame::Fixed),
112 _ => Err(format!(
113 "unknown stream frame: '{}'. Valid: line, raw, fixed",
114 s
115 )),
116 }
117 }
118}
119
120#[derive(Debug, Clone, UriConfig)]
128#[uri_scheme = "stream"]
129#[uri_config(
130 skip_impl,
131 metadata(
132 scheme = "stream",
133 description = "stdio data-plane adapter: out/err producers, in consumer",
134 producer,
135 consumer
136 ),
137 crate = "camel_component_api"
138)]
139pub struct StreamConfig {
140 pub target: StreamTarget,
142
143 #[uri_param(name = "appendNewline", default = "true")]
146 pub append_newline: bool,
147
148 #[uri_param(name = "charset", default = "utf-8")]
150 pub charset: String,
151
152 #[uri_param(name = "frame", kind = "enum:line,raw,fixed", default = "line")]
154 pub frame: StreamFrame,
155
156 #[uri_param(name = "size")]
159 pub size: Option<u64>,
160}
161
162impl StreamConfig {
164 pub fn validate(&self) -> Result<(), CamelError> {
170 if self.charset != "utf-8" {
171 return Err(CamelError::Config(format!(
172 "unsupported charset '{}': utf-8 is the only charset supported in v1",
173 self.charset
174 )));
175 }
176 if self.frame == StreamFrame::Fixed {
177 match self.size {
178 None => {
179 return Err(CamelError::InvalidUri(
180 "frame=fixed requires the size parameter".to_string(),
181 ));
182 }
183 Some(0) => {
184 return Err(CamelError::InvalidUri(
185 "frame=fixed requires a size greater than 0".to_string(),
186 ));
187 }
188 Some(n) if n > MAX_MATERIALIZE_BYTES as u64 => {
192 return Err(CamelError::InvalidUri(format!(
193 "frame=fixed size {n} exceeds the maximum frame size of {MAX_MATERIALIZE_BYTES} bytes (100 MiB)"
194 )));
195 }
196 Some(_) => {}
197 }
198 }
199 Ok(())
200 }
201}
202
203impl UriConfig for StreamConfig {
204 fn scheme() -> &'static str {
205 "stream"
206 }
207
208 fn from_uri(uri: &str) -> Result<Self, CamelError> {
209 let parts = parse_uri(uri)?;
210 Self::from_components(parts)
211 }
212
213 fn from_components(parts: camel_component_api::UriComponents) -> Result<Self, CamelError> {
214 let config = Self::parse_uri_components(parts)?;
215 StreamConfig::validate(&config)?;
216 Ok(config)
217 }
218
219 fn validate(self) -> Result<Self, CamelError> {
220 StreamConfig::validate(&self)?;
222 Ok(self)
223 }
224}
225
226pub struct StreamComponent;
233
234impl StreamComponent {
235 pub fn new() -> Self {
236 Self
237 }
238}
239
240impl Default for StreamComponent {
241 fn default() -> Self {
242 Self::new()
243 }
244}
245
246impl Component for StreamComponent {
247 fn scheme(&self) -> &str {
248 "stream"
249 }
250
251 fn metadata(&self) -> ComponentMetadata {
252 StreamConfig::metadata()
253 }
254
255 fn create_endpoint(
256 &self,
257 uri: &str,
258 _ctx: &dyn camel_component_api::ComponentContext,
259 ) -> Result<Box<dyn Endpoint>, CamelError> {
260 let config = StreamConfig::from_uri(uri)?;
261 Ok(Box::new(StreamEndpoint {
262 uri: uri.to_string(),
263 config,
264 }))
265 }
266}
267
268struct StreamEndpoint {
273 uri: String,
274 config: StreamConfig,
275}
276
277impl Endpoint for StreamEndpoint {
278 fn uri(&self) -> &str {
279 &self.uri
280 }
281
282 fn create_consumer(
283 &self,
284 rt: std::sync::Arc<dyn camel_component_api::RuntimeObservability>,
285 ) -> Result<Box<dyn Consumer>, CamelError> {
286 match self.config.target {
287 StreamTarget::In => Ok(Box::new(StreamConsumer::new(
288 self.config.clone(),
289 Arc::clone(&rt),
290 ))),
291 StreamTarget::Out | StreamTarget::Err => Err(CamelError::EndpointCreationFailed(
294 "stream:out and stream:err do not support consumers; use stream:in".into(),
295 )),
296 }
297 }
298
299 fn create_producer(
300 &self,
301 _rt: std::sync::Arc<dyn camel_component_api::RuntimeObservability>,
302 _ctx: &ProducerContext,
303 ) -> Result<BoxProcessor, CamelError> {
304 match self.config.target {
305 StreamTarget::Out | StreamTarget::Err => {
306 Ok(BoxProcessor::new(StreamProducer::new(self.config.clone())))
307 }
308 StreamTarget::In => Err(CamelError::EndpointCreationFailed(
311 "stream:in is a consumer target; producers require stream:out or stream:err".into(),
312 )),
313 }
314 }
315}
316
317#[async_trait]
324trait Sink: Send + Sync {
325 async fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()>;
326 async fn flush(&mut self) -> std::io::Result<()>;
327}
328
329struct StdoutSink(tokio::io::Stdout);
330
331#[async_trait]
332impl Sink for StdoutSink {
333 async fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
334 self.0.write_all(buf).await
335 }
336
337 async fn flush(&mut self) -> std::io::Result<()> {
338 self.0.flush().await
339 }
340}
341
342struct StderrSink(tokio::io::Stderr);
343
344#[async_trait]
345impl Sink for StderrSink {
346 async fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
347 self.0.write_all(buf).await
348 }
349
350 async fn flush(&mut self) -> std::io::Result<()> {
351 self.0.flush().await
352 }
353}
354
355static FD1_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
364static FD2_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
365
366const MAX_MATERIALIZE_BYTES: usize = 100 * 1024 * 1024;
369
370#[derive(Clone)]
376struct StreamProducer {
377 config: StreamConfig,
378 sink: Arc<tokio::sync::Mutex<dyn Sink>>,
379}
380
381impl StreamProducer {
382 pub(crate) fn new(config: StreamConfig) -> Self {
384 let sink: Arc<tokio::sync::Mutex<dyn Sink>> = match config.target {
385 StreamTarget::Out => Arc::new(tokio::sync::Mutex::new(StdoutSink(tokio::io::stdout()))),
386 StreamTarget::Err => Arc::new(tokio::sync::Mutex::new(StderrSink(tokio::io::stderr()))),
387 StreamTarget::In => Arc::new(tokio::sync::Mutex::new(StdoutSink(tokio::io::stdout()))),
391 };
392 Self { config, sink }
393 }
394
395 #[cfg(test)]
397 pub(crate) fn with_sink(config: StreamConfig, sink: Arc<tokio::sync::Mutex<dyn Sink>>) -> Self {
398 Self { config, sink }
399 }
400
401 #[cfg(test)]
404 pub(crate) fn target_fd(&self) -> i32 {
405 match self.config.target {
406 StreamTarget::Out => 1,
407 StreamTarget::Err => 2,
408 StreamTarget::In => 0,
409 }
410 }
411}
412
413impl Service<Exchange> for StreamProducer {
414 type Response = Exchange;
415 type Error = CamelError;
416 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
417
418 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
419 Poll::Ready(Ok(()))
420 }
421
422 fn call(&mut self, exchange: Exchange) -> Self::Future {
423 let config = self.config.clone();
424 let sink = self.sink.clone();
425 Box::pin(async move {
426 let bytes = exchange
430 .input
431 .body
432 .clone()
433 .into_bytes(MAX_MATERIALIZE_BYTES)
434 .await?;
435
436 let mut buf = BytesMut::from(bytes);
439 if config.append_newline {
440 buf.extend_from_slice(b"\n");
441 }
442
443 let fd_guard = if config.target == StreamTarget::Err {
444 FD2_LOCK.lock().await
445 } else {
446 FD1_LOCK.lock().await
447 };
448
449 let mut sink = sink.lock().await;
450 sink.write_all(&buf).await?;
451 sink.flush().await?;
452 drop(sink);
453 drop(fd_guard);
454
455 Ok(exchange)
456 })
457 }
458}
459
460fn ensure_frame_bound(len: usize) -> Result<(), CamelError> {
472 if len > MAX_MATERIALIZE_BYTES {
473 return Err(CamelError::StreamLimitExceeded(MAX_MATERIALIZE_BYTES));
474 }
475 Ok(())
476}
477
478#[async_trait]
487pub(crate) trait StreamReader: Send + Sync {
488 async fn read_until_newline(&mut self, buf: &mut Vec<u8>) -> Result<usize, CamelError>;
493
494 async fn read_exact_chunk(&mut self, n: usize, buf: &mut Vec<u8>) -> Result<usize, CamelError>;
497
498 async fn read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize, CamelError>;
501}
502
503struct StdinReader {
511 stdin: BufReader<tokio::io::Stdin>,
512}
513
514#[async_trait]
515impl StreamReader for StdinReader {
516 async fn read_until_newline(&mut self, buf: &mut Vec<u8>) -> Result<usize, CamelError> {
517 let mut appended = 0;
518 loop {
519 let chunk = self
520 .stdin
521 .fill_buf()
522 .await
523 .map_err(|e| CamelError::Io(e.to_string()))?;
524 if chunk.is_empty() {
525 return Ok(appended);
526 }
527 let upto = chunk
531 .iter()
532 .position(|&b| b == b'\n')
533 .map_or(chunk.len(), |i| i + 1);
534 ensure_frame_bound(buf.len() + upto)?;
535 let hit_newline = chunk[upto - 1] == b'\n';
536 buf.extend_from_slice(&chunk[..upto]);
537 self.stdin.consume(upto);
538 appended += upto;
539 if hit_newline {
540 return Ok(appended);
541 }
542 }
543 }
544
545 async fn read_exact_chunk(&mut self, n: usize, buf: &mut Vec<u8>) -> Result<usize, CamelError> {
546 let mut chunk = vec![0u8; n];
554 let read = self
555 .stdin
556 .read(&mut chunk)
557 .await
558 .map_err(|e| CamelError::Io(e.to_string()))?;
559 buf.extend_from_slice(&chunk[..read]);
560 Ok(read)
561 }
562
563 async fn read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize, CamelError> {
564 let mut appended = 0;
565 loop {
566 let chunk = self
567 .stdin
568 .fill_buf()
569 .await
570 .map_err(|e| CamelError::Io(e.to_string()))?;
571 if chunk.is_empty() {
572 return Ok(appended);
573 }
574 ensure_frame_bound(buf.len() + chunk.len())?;
577 let len = chunk.len();
578 buf.extend_from_slice(chunk);
579 self.stdin.consume(len);
580 appended += len;
581 }
582 }
583}
584
585enum Frame {
587 Line(String),
589 Bytes(Vec<u8>),
591 InvalidUtf8,
593 Eof,
595}
596
597async fn read_frame(
603 reader: &mut dyn StreamReader,
604 mode: StreamFrame,
605 size: Option<u64>,
606) -> Result<Frame, CamelError> {
607 let mut buf = Vec::new();
608 match mode {
609 StreamFrame::Line => {
610 if reader.read_until_newline(&mut buf).await? == 0 {
611 return Ok(Frame::Eof);
612 }
613 if buf.last() == Some(&b'\n') {
619 buf.pop();
620 if buf.last() == Some(&b'\r') {
621 buf.pop();
622 }
623 }
624 match String::from_utf8(buf) {
627 Ok(text) => Ok(Frame::Line(text)),
628 Err(_) => Ok(Frame::InvalidUtf8),
629 }
630 }
631 StreamFrame::Raw => {
632 reader.read_to_end(&mut buf).await?;
635 if buf.is_empty() {
636 Ok(Frame::Eof)
637 } else {
638 Ok(Frame::Bytes(buf))
639 }
640 }
641 StreamFrame::Fixed => {
642 let n = usize::try_from(size.unwrap_or(0)).unwrap_or(0);
647 let start = buf.len();
652 while buf.len() - start < n {
653 let read = reader
654 .read_exact_chunk(n - (buf.len() - start), &mut buf)
655 .await?;
656 if read == 0 {
657 break;
658 }
659 }
660 if buf.len() == start {
661 Ok(Frame::Eof)
662 } else {
663 Ok(Frame::Bytes(buf))
664 }
665 }
666 }
667}
668
669pub(crate) struct StreamConsumer {
682 config: StreamConfig,
683 started: AtomicBool,
685 runtime: Arc<dyn RuntimeObservability>,
687 reader: Box<dyn StreamReader>,
688}
689
690impl StreamConsumer {
691 pub(crate) fn new(config: StreamConfig, runtime: Arc<dyn RuntimeObservability>) -> Self {
693 Self::with_reader(
694 config,
695 runtime,
696 Box::new(StdinReader {
697 stdin: BufReader::new(tokio::io::stdin()),
698 }),
699 )
700 }
701
702 pub(crate) fn with_reader(
704 config: StreamConfig,
705 runtime: Arc<dyn RuntimeObservability>,
706 reader: Box<dyn StreamReader>,
707 ) -> Self {
708 Self {
709 config,
710 started: AtomicBool::new(false),
711 runtime,
712 reader,
713 }
714 }
715
716 #[cfg(test)]
719 pub(crate) fn mark_started_for_test(&self) {
720 self.started.store(true, Ordering::SeqCst);
721 }
722}
723
724#[async_trait]
725impl Consumer for StreamConsumer {
726 async fn start(&mut self, context: ConsumerContext) -> Result<(), CamelError> {
727 self.started
728 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
729 .map_err(|_| {
730 CamelError::EndpointCreationFailed("stream consumer already started".to_string())
731 })?;
732
733 let cancel_token = context.cancel_token();
734 let frame_mode = self.config.frame;
735 let frame_size = self.config.size;
736 let reader = &mut *self.reader;
739
740 loop {
741 tokio::select! {
742 _ = cancel_token.cancelled() => {
743 debug!(stream_target = %self.config.target, "stream consumer cancelled, stopping");
744 break;
745 }
746 frame = read_frame(reader, frame_mode, frame_size) => {
747 let message = match frame {
748 Ok(Frame::Line(text)) => Message::new(text),
749 Ok(Frame::Bytes(bytes)) => Message::new(bytes),
750 Ok(Frame::InvalidUtf8) => {
751 self.runtime
752 .metrics()
753 .increment_errors(context.route_id(), "b-prime:stream:decode");
754 continue;
755 }
756 Ok(Frame::Eof) => break,
757 Err(error) => {
758 self.started.store(false, Ordering::SeqCst);
765 return Err(error);
766 }
767 };
768
769 if context.send(Exchange::new(message)).await.is_err() {
770 self.runtime
774 .metrics()
775 .increment_errors(context.route_id(), "b-prime:stream:fire-send");
776 break;
777 }
778 }
781 }
782 }
783
784 self.started.store(false, Ordering::SeqCst);
786 Ok(())
787 }
788
789 async fn stop(&mut self) -> Result<(), CamelError> {
790 self.started.store(false, Ordering::SeqCst);
791 debug!(stream_target = %self.config.target, "stream consumer stopped");
792 Ok(())
793 }
794}
795
796#[cfg(test)]
801mod stream_producer_tests {
802 use super::*;
803 use camel_component_api::{Body, Message, StreamBody};
804 use serde_json::json;
805
806 #[derive(Default, Clone)]
809 struct VecSink(Arc<std::sync::Mutex<Vec<u8>>>);
810
811 impl VecSink {
812 fn bytes(&self) -> Vec<u8> {
813 self.0.lock().unwrap().clone()
814 }
815 }
816
817 #[async_trait::async_trait]
818 impl Sink for VecSink {
819 async fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
820 self.0.lock().unwrap().extend_from_slice(buf);
821 Ok(())
822 }
823
824 async fn flush(&mut self) -> std::io::Result<()> {
825 Ok(())
826 }
827 }
828
829 struct ChunkSink(Arc<std::sync::Mutex<Vec<u8>>>);
836
837 #[async_trait::async_trait]
838 impl Sink for ChunkSink {
839 async fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
840 let mid = buf.len() / 2;
841 self.0.lock().unwrap().extend_from_slice(&buf[..mid]);
842 tokio::task::yield_now().await;
843 self.0.lock().unwrap().extend_from_slice(&buf[mid..]);
844 Ok(())
845 }
846
847 async fn flush(&mut self) -> std::io::Result<()> {
848 Ok(())
849 }
850 }
851
852 fn test_sink() -> (Arc<tokio::sync::Mutex<dyn Sink>>, VecSink) {
855 let vec_sink = VecSink::default();
856 (
857 Arc::new(tokio::sync::Mutex::new(vec_sink.clone())),
858 vec_sink,
859 )
860 }
861
862 #[tokio::test]
863 async fn line_mode_appends_single_newline() {
864 let (sink, reader) = test_sink();
865 let config = StreamConfig::from_uri("stream:out").unwrap();
866 let mut producer = StreamProducer::with_sink(config, sink);
867
868 let exchange = Exchange::new(Message::new("hello"));
869 let result = producer.call(exchange).await.unwrap();
870
871 assert_eq!(result.input.body.as_text(), Some("hello"));
872 assert_eq!(reader.bytes(), b"hello\n");
873 }
874
875 #[tokio::test]
876 async fn raw_mode_writes_verbatim() {
877 let (sink, reader) = test_sink();
878 let config = StreamConfig::from_uri("stream:out?appendNewline=false").unwrap();
879 let mut producer = StreamProducer::with_sink(config, sink);
880
881 let exchange = Exchange::new(Message::new(b"a,b,c".to_vec()));
882 producer.call(exchange).await.unwrap();
883
884 assert_eq!(reader.bytes(), b"a,b,c");
885 }
886
887 #[tokio::test]
888 async fn err_target_dispatches_stderr() {
889 let config = StreamConfig::from_uri("stream:err").unwrap();
890 assert_eq!(StreamProducer::new(config).target_fd(), 2);
891
892 let config = StreamConfig::from_uri("stream:out").unwrap();
893 assert_eq!(StreamProducer::new(config).target_fd(), 1);
894 }
895
896 #[tokio::test]
897 async fn structured_body_materializes_to_bytes() {
898 let (sink, reader) = test_sink();
899 let config = StreamConfig::from_uri("stream:out?appendNewline=false").unwrap();
900 let mut producer = StreamProducer::with_sink(config, sink);
901
902 let value = json!({"name": "café", "count": 3, "nested": {"ok": true}});
903 let mut exchange = Exchange::new(Message::new(""));
904 exchange.input.body = Body::Json(value.clone());
905 producer.call(exchange).await.unwrap();
906
907 let expected = Body::Json(value)
908 .into_bytes(100 * 1024 * 1024)
909 .await
910 .unwrap();
911 assert_eq!(reader.bytes(), expected.as_ref());
912 }
913
914 #[tokio::test]
915 async fn credential_body_passes_verbatim() {
916 let (sink, reader) = test_sink();
917 let config = StreamConfig::from_uri("stream:out?appendNewline=false").unwrap();
918 let mut producer = StreamProducer::with_sink(config, sink);
919
920 let exchange = Exchange::new(Message::new("password=hunter2"));
921 producer.call(exchange).await.unwrap();
922
923 assert_eq!(reader.bytes(), b"password=hunter2");
925 }
926
927 #[tokio::test]
928 async fn empty_body_line_mode_writes_just_newline() {
929 let (sink, reader) = test_sink();
930 let config = StreamConfig::from_uri("stream:out").unwrap();
931 let mut producer = StreamProducer::with_sink(config, sink);
932
933 let exchange = Exchange::new(Message::default());
934 producer.call(exchange).await.unwrap();
935
936 assert_eq!(reader.bytes(), b"\n");
937 }
938
939 #[tokio::test]
940 async fn concurrent_producers_serialize_on_fd() {
941 let shared = Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
947 let sink1: Arc<tokio::sync::Mutex<dyn Sink>> =
948 Arc::new(tokio::sync::Mutex::new(ChunkSink(shared.clone())));
949 let sink2: Arc<tokio::sync::Mutex<dyn Sink>> =
950 Arc::new(tokio::sync::Mutex::new(ChunkSink(shared.clone())));
951
952 let config = StreamConfig::from_uri("stream:out").unwrap();
953 let mut p1 = StreamProducer::with_sink(config.clone(), sink1);
954 let mut p2 = StreamProducer::with_sink(config, sink2);
955
956 let t1 = tokio::spawn(async move { p1.call(Exchange::new(Message::new("aa\naa"))).await });
957 let t2 = tokio::spawn(async move { p2.call(Exchange::new(Message::new("bb\nbb"))).await });
958 t1.await.unwrap().unwrap();
959 t2.await.unwrap().unwrap();
960
961 let out = shared.lock().unwrap().clone();
964 let ok = out.as_slice() == b"aa\naa\nbb\nbb\n" || out.as_slice() == b"bb\nbb\naa\naa\n";
965 assert!(ok, "interleaved or corrupted writes: {out:?}");
966 }
967
968 #[tokio::test]
969 async fn materialization_error_writes_nothing() {
970 let (sink, reader) = test_sink();
971 let config = StreamConfig::from_uri("stream:out").unwrap();
972 let mut producer = StreamProducer::with_sink(config, sink);
973
974 let body = Body::Stream(StreamBody {
977 stream: Arc::new(tokio::sync::Mutex::new(None)),
978 metadata: Default::default(),
979 });
980 let exchange = Exchange::new(Message::new(body));
981
982 let result = producer.call(exchange).await;
983 assert!(
984 matches!(result, Err(CamelError::AlreadyConsumed)),
985 "expected AlreadyConsumed, got: {result:?}"
986 );
987 assert!(
988 reader.bytes().is_empty(),
989 "nothing may be written when materialization fails"
990 );
991 }
992}
993
994#[cfg(test)]
999#[path = "consumer_tests.rs"]
1000mod stream_consumer_tests;
1001
1002#[cfg(test)]
1003mod tests {
1004 use super::*;
1005
1006 #[test]
1007 fn config_parses_out_default() {
1008 let config = StreamConfig::from_uri("stream:out").unwrap();
1009 assert_eq!(config.target, StreamTarget::Out);
1010 assert!(config.append_newline);
1011 assert_eq!(config.charset, "utf-8");
1012 assert_eq!(config.frame, StreamFrame::Line);
1013 }
1014
1015 #[test]
1016 fn config_parses_in_raw() {
1017 let config = StreamConfig::from_uri("stream:in?frame=raw").unwrap();
1018 assert_eq!(config.target, StreamTarget::In);
1019 assert_eq!(config.frame, StreamFrame::Raw);
1020 }
1021
1022 #[test]
1023 fn config_rejects_unknown_path() {
1024 let result = StreamConfig::from_uri("stream:logfile");
1025 assert!(
1026 matches!(result, Err(CamelError::InvalidUri(_))),
1027 "unknown path must be rejected with InvalidUri, got: {result:?}"
1028 );
1029 }
1030
1031 #[test]
1032 fn config_rejects_non_utf8_charset() {
1033 let result = StreamConfig::from_uri("stream:out?charset=latin-1");
1034 match result {
1035 Err(CamelError::Config(msg)) => {
1036 assert!(msg.contains("utf-8"), "error must name utf-8, got: {msg}")
1037 }
1038 other => panic!("expected CamelError::Config, got: {other:?}"),
1039 }
1040 }
1041
1042 #[test]
1043 fn config_rejects_fixed_without_size() {
1044 let result = StreamConfig::from_uri("stream:in?frame=fixed");
1045 assert!(
1046 matches!(result, Err(CamelError::InvalidUri(_))),
1047 "frame=fixed without size must be rejected with InvalidUri, got: {result:?}"
1048 );
1049 let result = StreamConfig::from_uri("stream:in?frame=fixed&size=0");
1050 assert!(
1051 matches!(result, Err(CamelError::InvalidUri(_))),
1052 "frame=fixed with size=0 must be rejected with InvalidUri, got: {result:?}"
1053 );
1054 }
1055
1056 #[test]
1059 fn config_rejects_huge_fixed_size() {
1060 let result = StreamConfig::from_uri("stream:in?frame=fixed&size=99999999999");
1061 match result {
1062 Err(CamelError::InvalidUri(msg)) => {
1063 assert!(
1064 msg.contains("104857600"),
1065 "error must name the size bound, got: {msg}"
1066 );
1067 }
1068 other => panic!("expected CamelError::InvalidUri, got: {other:?}"),
1069 }
1070 }
1071
1072 #[test]
1073 fn component_scheme_is_stream() {
1074 assert_eq!(StreamComponent::new().scheme(), "stream");
1075 }
1076
1077 #[test]
1080 fn ensure_frame_bound_allows_up_to_cap() {
1081 assert!(ensure_frame_bound(MAX_MATERIALIZE_BYTES - 1).is_ok());
1082 assert!(ensure_frame_bound(MAX_MATERIALIZE_BYTES).is_ok());
1083 }
1084
1085 #[test]
1086 fn ensure_frame_bound_rejects_past_cap() {
1087 match ensure_frame_bound(MAX_MATERIALIZE_BYTES + 1) {
1088 Err(CamelError::StreamLimitExceeded(max)) => {
1089 assert_eq!(max, MAX_MATERIALIZE_BYTES);
1090 }
1091 other => panic!("expected StreamLimitExceeded, got: {other:?}"),
1092 }
1093 }
1094}