1use std::sync::Arc;
2
3use async_trait::async_trait;
4use tokio::sync::{mpsc, oneshot, watch};
5use tokio::task::JoinHandle;
6use tokio_util::sync::CancellationToken;
7
8use camel_api::security_policy::SecurityPolicy;
9use camel_api::{CamelError, Exchange};
10use camel_auth::{CredentialSource, TokenAuthenticator};
11
12pub struct ExchangeEnvelope {
18 pub exchange: Exchange,
19 pub reply_tx: Option<oneshot::Sender<Result<Exchange, CamelError>>>,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum ConsumerStartupMode {
39 #[default]
42 Immediate,
43 Explicit,
46}
47
48#[derive(Clone, Debug)]
50enum StartupState {
51 Pending,
53 Ready,
55 Failed(String),
57}
58
59#[derive(Clone)]
66pub struct StartupSignal {
67 tx: watch::Sender<StartupState>,
68}
69
70impl StartupSignal {
71 pub fn pair() -> (Self, StartupReceiver) {
73 let (tx, rx) = watch::channel(StartupState::Pending);
74 (Self { tx }, StartupReceiver { rx })
75 }
76
77 pub fn mark_ready(&self) -> bool {
87 self.tx.send_if_modified(|s| {
88 if matches!(*s, StartupState::Pending) {
89 *s = StartupState::Ready;
90 true
91 } else {
92 false
93 }
94 })
95 }
96
97 pub fn mark_failed(&self, err: String) {
99 self.tx.send_if_modified(|s| {
100 if matches!(*s, StartupState::Pending) {
101 *s = StartupState::Failed(err);
102 true
103 } else {
104 false
105 }
106 });
107 }
108}
109
110impl std::fmt::Debug for StartupSignal {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 f.debug_struct("StartupSignal")
113 .field("state", &self.tx.borrow())
114 .finish()
115 }
116}
117
118pub struct StartupReceiver {
125 rx: watch::Receiver<StartupState>,
126}
127
128impl StartupReceiver {
129 pub fn immediate() -> Self {
133 let (tx, rx) = watch::channel(StartupState::Ready);
134 let _ = tx;
137 Self { rx }
138 }
139
140 pub async fn await_ready(mut self) -> Result<(), CamelError> {
147 loop {
148 match &*self.rx.borrow() {
149 StartupState::Pending => {}
150 StartupState::Ready => return Ok(()),
151 StartupState::Failed(msg) => {
152 return Err(CamelError::RouteError(msg.clone()));
153 }
154 }
155 if self.rx.changed().await.is_err() {
156 return Err(CamelError::RouteError(
157 "consumer startup signal dropped without resolving".to_string(),
158 ));
159 }
160 }
161 }
162}
163
164impl std::fmt::Debug for StartupReceiver {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 f.debug_struct("StartupReceiver")
167 .field("state", &self.rx.borrow())
168 .finish()
169 }
170}
171
172#[derive(Clone)]
174pub struct ConsumerContext {
175 sender: mpsc::Sender<ExchangeEnvelope>,
176 cancel_token: CancellationToken,
177 route_id: String,
178 startup: StartupSignal,
179}
180
181impl ConsumerContext {
182 pub fn new(
192 sender: mpsc::Sender<ExchangeEnvelope>,
193 cancel_token: CancellationToken,
194 route_id: String,
195 ) -> Self {
196 let (startup, _unused_receiver) = StartupSignal::pair();
197 let _ = _unused_receiver;
201 Self {
202 sender,
203 cancel_token,
204 route_id,
205 startup,
206 }
207 }
208
209 pub fn with_startup(mut self, startup: StartupSignal) -> Self {
213 self.startup = startup;
214 self
215 }
216
217 pub fn startup_signal(&self) -> StartupSignal {
221 self.startup.clone()
222 }
223
224 pub fn mark_ready(&self) {
231 let _ = self.startup.mark_ready();
232 }
233
234 pub async fn cancelled(&self) {
237 self.cancel_token.cancelled().await
238 }
239
240 pub fn is_cancelled(&self) -> bool {
242 self.cancel_token.is_cancelled()
243 }
244
245 pub fn route_id(&self) -> &str {
251 &self.route_id
252 }
253
254 pub fn cancel_token(&self) -> CancellationToken {
259 self.cancel_token.clone()
260 }
261
262 pub fn sender(&self) -> mpsc::Sender<ExchangeEnvelope> {
268 self.sender.clone()
269 }
270
271 pub async fn send(&self, exchange: Exchange) -> Result<(), CamelError> {
273 self.sender
274 .send(ExchangeEnvelope {
275 exchange,
276 reply_tx: None,
277 })
278 .await
279 .map_err(|_| CamelError::ChannelClosed)
280 }
281
282 pub async fn send_and_wait(&self, exchange: Exchange) -> Result<Exchange, CamelError> {
287 let (reply_tx, reply_rx) = oneshot::channel();
288 self.sender
289 .send(ExchangeEnvelope {
290 exchange,
291 reply_tx: Some(reply_tx),
292 })
293 .await
294 .map_err(|_| CamelError::ChannelClosed)?;
295 reply_rx.await.map_err(|_| CamelError::ChannelClosed)?
296 }
297}
298
299pub struct SecurityContext {
305 pub policy: Arc<dyn SecurityPolicy>,
306 pub authenticator: Arc<dyn TokenAuthenticator>,
307 pub credential_sources: Vec<CredentialSource>,
308}
309
310impl SecurityContext {
311 pub fn new(
312 policy: impl SecurityPolicy + 'static,
313 authenticator: Arc<dyn TokenAuthenticator>,
314 ) -> Self {
315 Self {
316 policy: Arc::new(policy),
317 authenticator,
318 credential_sources: vec![CredentialSource::AuthorizationHeader],
319 }
320 }
321
322 pub fn from_arc(
323 policy: Arc<dyn SecurityPolicy>,
324 authenticator: Arc<dyn TokenAuthenticator>,
325 ) -> Self {
326 Self {
327 policy,
328 authenticator,
329 credential_sources: vec![CredentialSource::AuthorizationHeader],
330 }
331 }
332
333 pub fn with_credential_sources(mut self, sources: Vec<CredentialSource>) -> Self {
334 self.credential_sources = sources;
335 self
336 }
337}
338
339impl Clone for SecurityContext {
340 fn clone(&self) -> Self {
341 Self {
342 policy: Arc::clone(&self.policy),
343 authenticator: Arc::clone(&self.authenticator),
344 credential_sources: self.credential_sources.clone(),
345 }
346 }
347}
348
349impl std::fmt::Debug for SecurityContext {
350 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351 f.debug_struct("SecurityContext")
352 .field("policy", &"<SecurityPolicy>")
353 .field("authenticator", &"<TokenAuthenticator>")
354 .field("credential_sources", &self.credential_sources)
355 .finish()
356 }
357}
358
359#[derive(Debug, Clone, PartialEq, Eq)]
361pub enum ConcurrencyModel {
362 Sequential,
365 Concurrent { max: Option<usize> },
369}
370
371#[async_trait]
395pub trait Consumer: Send + Sync {
396 async fn start(&mut self, context: ConsumerContext) -> Result<(), CamelError>;
398
399 async fn stop(&mut self) -> Result<(), CamelError>;
404
405 async fn suspend(&self) -> Result<(), CamelError> {
409 Ok(())
410 }
411
412 async fn resume(&self) -> Result<(), CamelError> {
416 Ok(())
417 }
418
419 fn concurrency_model(&self) -> ConcurrencyModel {
428 ConcurrencyModel::Sequential
429 }
430
431 fn startup_mode(&self) -> ConsumerStartupMode {
446 ConsumerStartupMode::Immediate
447 }
448
449 fn background_task_handle(&mut self) -> Option<JoinHandle<Result<(), CamelError>>> {
461 None
462 }
463
464 fn set_security_context(&mut self, _ctx: SecurityContext) {}
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn consumer_context_exposes_route_id() {
479 let (tx, _rx) = mpsc::channel(1);
480 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "test-route".to_string());
481 assert_eq!(ctx.route_id(), "test-route");
482 }
483
484 #[tokio::test]
485 async fn test_consumer_context_cancelled() {
486 let (tx, _rx) = mpsc::channel(16);
487 let token = CancellationToken::new();
488 let ctx = ConsumerContext::new(tx, token.clone(), "test-route".to_string());
489
490 assert!(!ctx.is_cancelled());
491 token.cancel();
492 ctx.cancelled().await;
493 assert!(ctx.is_cancelled());
494 }
495
496 #[test]
497 fn test_concurrency_model_default_is_sequential() {
498 use super::ConcurrencyModel;
499
500 struct DummyConsumer;
501
502 #[async_trait::async_trait]
503 impl super::Consumer for DummyConsumer {
504 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
505 Ok(())
506 }
507 async fn stop(&mut self) -> Result<(), CamelError> {
508 Ok(())
509 }
510 }
511
512 let consumer = DummyConsumer;
513 assert_eq!(consumer.concurrency_model(), ConcurrencyModel::Sequential);
514 }
515
516 #[test]
517 fn test_concurrency_model_concurrent_override() {
518 use super::ConcurrencyModel;
519
520 struct ConcurrentConsumer;
521
522 #[async_trait::async_trait]
523 impl super::Consumer for ConcurrentConsumer {
524 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
525 Ok(())
526 }
527 async fn stop(&mut self) -> Result<(), CamelError> {
528 Ok(())
529 }
530 fn concurrency_model(&self) -> ConcurrencyModel {
531 ConcurrencyModel::Concurrent { max: Some(16) }
532 }
533 }
534
535 let consumer = ConcurrentConsumer;
536 assert_eq!(
537 consumer.concurrency_model(),
538 ConcurrencyModel::Concurrent { max: Some(16) }
539 );
540 }
541
542 #[test]
545 fn test_default_startup_mode_is_immediate() {
546 struct DummyConsumer;
547
548 #[async_trait::async_trait]
549 impl super::Consumer for DummyConsumer {
550 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
551 Ok(())
552 }
553 async fn stop(&mut self) -> Result<(), CamelError> {
554 Ok(())
555 }
556 }
557
558 let consumer = DummyConsumer;
559 assert_eq!(
560 consumer.startup_mode(),
561 super::ConsumerStartupMode::Immediate
562 );
563 }
564
565 #[test]
566 fn test_startup_mode_explicit_override() {
567 struct ExplicitConsumer;
568
569 #[async_trait::async_trait]
570 impl super::Consumer for ExplicitConsumer {
571 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
572 Ok(())
573 }
574 async fn stop(&mut self) -> Result<(), CamelError> {
575 Ok(())
576 }
577 fn startup_mode(&self) -> super::ConsumerStartupMode {
578 super::ConsumerStartupMode::Explicit
579 }
580 }
581
582 let consumer = ExplicitConsumer;
583 assert_eq!(
584 consumer.startup_mode(),
585 super::ConsumerStartupMode::Explicit
586 );
587 }
588
589 #[tokio::test]
590 async fn test_startup_signal_mark_ready_resolves_receiver_ok() {
591 let (signal, receiver) = StartupSignal::pair();
592 assert!(matches!(*receiver.rx.borrow(), StartupState::Pending));
594
595 signal.mark_ready();
597 let result = receiver.await_ready().await;
598 assert!(result.is_ok(), "expected Ok after mark_ready");
599 }
600
601 #[tokio::test]
602 async fn test_startup_signal_mark_failed_propagates_error() {
603 let (signal, receiver) = StartupSignal::pair();
604 signal.mark_failed("bind failed".to_string());
605 let err = receiver
606 .await_ready()
607 .await
608 .expect_err("expected Err after mark_failed");
609 match err {
610 CamelError::RouteError(msg) => assert!(msg.contains("bind failed")),
611 other => panic!("expected RouteError, got {other:?}"),
612 }
613 }
614
615 #[tokio::test]
616 async fn test_startup_signal_idempotent_first_wins() {
617 let (signal, receiver) = StartupSignal::pair();
618 signal.mark_ready();
619 signal.mark_failed("late failure".to_string());
621 let result = receiver.await_ready().await;
622 assert!(result.is_ok(), "first transition (Ready) wins");
623 }
624
625 #[tokio::test]
626 async fn test_startup_receiver_immediate_is_pre_resolved_ok() {
627 let receiver = StartupReceiver::immediate();
628 let result = receiver.await_ready().await;
629 assert!(result.is_ok(), "immediate receiver must resolve Ok");
630 }
631
632 #[tokio::test]
633 async fn test_consumer_context_mark_ready_drives_signal() {
634 let (tx, _rx) = mpsc::channel(1);
635 let ctx = ConsumerContext::new(
636 tx,
637 CancellationToken::new(),
638 "startup-test-route".to_string(),
639 );
640 let (signal, receiver) = StartupSignal::pair();
641 let ctx = ctx.with_startup(signal);
642 ctx.mark_ready();
643 let result = receiver.await_ready().await;
644 assert!(result.is_ok(), "ctx.mark_ready must resolve the receiver");
645 }
646
647 #[tokio::test]
648 async fn test_startup_receiver_dropped_sender_returns_err() {
649 let (_signal, receiver) = StartupSignal::pair();
652 drop(_signal);
653 let err = receiver
654 .await_ready()
655 .await
656 .expect_err("dropped signal must surface as Err");
657 match err {
658 CamelError::RouteError(msg) => assert!(msg.contains("dropped")),
659 other => panic!("expected RouteError, got {other:?}"),
660 }
661 }
662
663 #[tokio::test]
664 async fn test_consumer_default_suspend_resume() {
665 struct DummyConsumer;
666
667 #[async_trait::async_trait]
668 impl super::Consumer for DummyConsumer {
669 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
670 Ok(())
671 }
672 async fn stop(&mut self) -> Result<(), CamelError> {
673 Ok(())
674 }
675 }
676
677 let consumer = DummyConsumer;
678 assert!(consumer.suspend().await.is_ok());
679 assert!(consumer.resume().await.is_ok());
680 }
681
682 struct StubPolicy;
685
686 #[async_trait::async_trait]
687 impl SecurityPolicy for StubPolicy {
688 async fn evaluate(
689 &self,
690 _exchange: &mut Exchange,
691 ) -> Result<camel_api::security_policy::AuthorizationDecision, CamelError> {
692 Ok(camel_api::security_policy::AuthorizationDecision::Granted {
693 principal: camel_api::security_policy::Principal {
694 subject: "stub".into(),
695 issuer: "stub".into(),
696 audience: vec![],
697 scopes: vec![],
698 roles: vec![],
699 claims: serde_json::json!({}),
700 },
701 })
702 }
703 }
704
705 struct StubAuthenticator;
706
707 #[async_trait::async_trait]
708 impl camel_auth::TokenAuthenticator for StubAuthenticator {
709 async fn authenticate_bearer(
710 &self,
711 _token: &str,
712 ) -> Result<camel_api::security_policy::Principal, CamelError> {
713 Ok(camel_api::security_policy::Principal {
714 subject: "stub".into(),
715 issuer: "stub".into(),
716 audience: vec![],
717 scopes: vec![],
718 roles: vec![],
719 claims: serde_json::json!({}),
720 })
721 }
722 }
723
724 #[test]
725 fn test_security_context_new() {
726 let ctx = SecurityContext::new(StubPolicy, Arc::new(StubAuthenticator));
727 assert!(Arc::strong_count(&ctx.policy) == 1);
728 assert!(Arc::strong_count(&ctx.authenticator) == 1);
729 assert_eq!(
730 ctx.credential_sources,
731 vec![camel_auth::CredentialSource::AuthorizationHeader]
732 );
733 }
734
735 #[test]
736 fn test_security_context_from_arc() {
737 let policy: Arc<dyn SecurityPolicy> = Arc::new(StubPolicy);
738 let authenticator: Arc<dyn camel_auth::TokenAuthenticator> = Arc::new(StubAuthenticator);
739 let ctx = SecurityContext::from_arc(Arc::clone(&policy), Arc::clone(&authenticator));
740 assert!(Arc::ptr_eq(&ctx.policy, &policy));
741 assert!(Arc::ptr_eq(&ctx.authenticator, &authenticator));
742 assert_eq!(
743 ctx.credential_sources,
744 vec![camel_auth::CredentialSource::AuthorizationHeader]
745 );
746 }
747
748 #[test]
749 fn test_security_context_clone_independent() {
750 let ctx = SecurityContext::new(StubPolicy, Arc::new(StubAuthenticator));
751 let cloned = ctx.clone();
752 assert!(Arc::ptr_eq(&ctx.policy, &cloned.policy));
753 assert!(Arc::ptr_eq(&ctx.authenticator, &cloned.authenticator));
754 assert_eq!(ctx.credential_sources, cloned.credential_sources);
755 }
756
757 #[test]
758 fn test_security_context_debug_redacts_traits() {
759 let ctx = SecurityContext::new(StubPolicy, Arc::new(StubAuthenticator));
760 let debug_str = format!("{ctx:?}");
761 assert!(debug_str.contains("<SecurityPolicy>"));
762 assert!(debug_str.contains("<TokenAuthenticator>"));
763 assert!(debug_str.contains("credential_sources"));
764 }
765
766 #[test]
767 fn test_security_context_with_credential_sources() {
768 let ctx = SecurityContext::new(StubPolicy, Arc::new(StubAuthenticator))
769 .with_credential_sources(vec![
770 camel_auth::CredentialSource::Cookie {
771 name: "session".into(),
772 },
773 camel_auth::CredentialSource::AuthorizationHeader,
774 ]);
775 assert_eq!(ctx.credential_sources.len(), 2);
776 assert!(matches!(
777 &ctx.credential_sources[0],
778 camel_auth::CredentialSource::Cookie { .. }
779 ));
780 }
781}