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 fn mark_failed(&self, err: String) {
244 self.startup.mark_failed(err);
245 }
246
247 pub async fn cancelled(&self) {
250 self.cancel_token.cancelled().await
251 }
252
253 pub fn is_cancelled(&self) -> bool {
255 self.cancel_token.is_cancelled()
256 }
257
258 pub fn route_id(&self) -> &str {
264 &self.route_id
265 }
266
267 pub fn cancel_token(&self) -> CancellationToken {
272 self.cancel_token.clone()
273 }
274
275 pub fn sender(&self) -> mpsc::Sender<ExchangeEnvelope> {
281 self.sender.clone()
282 }
283
284 pub async fn send(&self, exchange: Exchange) -> Result<(), CamelError> {
286 self.sender
287 .send(ExchangeEnvelope {
288 exchange,
289 reply_tx: None,
290 })
291 .await
292 .map_err(|_| CamelError::ChannelClosed)
293 }
294
295 pub async fn send_and_wait(&self, exchange: Exchange) -> Result<Exchange, CamelError> {
300 let (reply_tx, reply_rx) = oneshot::channel();
301 self.sender
302 .send(ExchangeEnvelope {
303 exchange,
304 reply_tx: Some(reply_tx),
305 })
306 .await
307 .map_err(|_| CamelError::ChannelClosed)?;
308 reply_rx.await.map_err(|_| CamelError::ChannelClosed)?
309 }
310}
311
312pub struct SecurityContext {
318 pub policy: Arc<dyn SecurityPolicy>,
319 pub authenticator: Arc<dyn TokenAuthenticator>,
320 pub credential_sources: Vec<CredentialSource>,
321}
322
323impl SecurityContext {
324 pub fn new(
325 policy: impl SecurityPolicy + 'static,
326 authenticator: Arc<dyn TokenAuthenticator>,
327 ) -> Self {
328 Self {
329 policy: Arc::new(policy),
330 authenticator,
331 credential_sources: vec![CredentialSource::AuthorizationHeader],
332 }
333 }
334
335 pub fn from_arc(
336 policy: Arc<dyn SecurityPolicy>,
337 authenticator: Arc<dyn TokenAuthenticator>,
338 ) -> Self {
339 Self {
340 policy,
341 authenticator,
342 credential_sources: vec![CredentialSource::AuthorizationHeader],
343 }
344 }
345
346 pub fn with_credential_sources(mut self, sources: Vec<CredentialSource>) -> Self {
347 self.credential_sources = sources;
348 self
349 }
350}
351
352impl Clone for SecurityContext {
353 fn clone(&self) -> Self {
354 Self {
355 policy: Arc::clone(&self.policy),
356 authenticator: Arc::clone(&self.authenticator),
357 credential_sources: self.credential_sources.clone(),
358 }
359 }
360}
361
362impl std::fmt::Debug for SecurityContext {
363 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364 f.debug_struct("SecurityContext")
365 .field("policy", &"<SecurityPolicy>")
366 .field("authenticator", &"<TokenAuthenticator>")
367 .field("credential_sources", &self.credential_sources)
368 .finish()
369 }
370}
371
372#[derive(Debug, Clone, PartialEq, Eq)]
374pub enum ConcurrencyModel {
375 Sequential,
378 Concurrent { max: Option<usize> },
382}
383
384#[async_trait]
408pub trait Consumer: Send + Sync {
409 async fn start(&mut self, context: ConsumerContext) -> Result<(), CamelError>;
411
412 async fn stop(&mut self) -> Result<(), CamelError>;
417
418 async fn suspend(&self) -> Result<(), CamelError> {
422 Ok(())
423 }
424
425 async fn resume(&self) -> Result<(), CamelError> {
429 Ok(())
430 }
431
432 fn concurrency_model(&self) -> ConcurrencyModel {
441 ConcurrencyModel::Sequential
442 }
443
444 fn startup_mode(&self) -> ConsumerStartupMode {
459 ConsumerStartupMode::Immediate
460 }
461
462 fn background_task_handle(&mut self) -> Option<JoinHandle<Result<(), CamelError>>> {
474 None
475 }
476
477 fn set_security_context(&mut self, _ctx: SecurityContext) {}
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489
490 #[test]
491 fn consumer_context_exposes_route_id() {
492 let (tx, _rx) = mpsc::channel(1);
493 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "test-route".to_string());
494 assert_eq!(ctx.route_id(), "test-route");
495 }
496
497 #[tokio::test]
498 async fn test_consumer_context_cancelled() {
499 let (tx, _rx) = mpsc::channel(16);
500 let token = CancellationToken::new();
501 let ctx = ConsumerContext::new(tx, token.clone(), "test-route".to_string());
502
503 assert!(!ctx.is_cancelled());
504 token.cancel();
505 ctx.cancelled().await;
506 assert!(ctx.is_cancelled());
507 }
508
509 #[test]
510 fn test_concurrency_model_default_is_sequential() {
511 use super::ConcurrencyModel;
512
513 struct DummyConsumer;
514
515 #[async_trait::async_trait]
516 impl super::Consumer for DummyConsumer {
517 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
518 Ok(())
519 }
520 async fn stop(&mut self) -> Result<(), CamelError> {
521 Ok(())
522 }
523 }
524
525 let consumer = DummyConsumer;
526 assert_eq!(consumer.concurrency_model(), ConcurrencyModel::Sequential);
527 }
528
529 #[test]
530 fn test_concurrency_model_concurrent_override() {
531 use super::ConcurrencyModel;
532
533 struct ConcurrentConsumer;
534
535 #[async_trait::async_trait]
536 impl super::Consumer for ConcurrentConsumer {
537 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
538 Ok(())
539 }
540 async fn stop(&mut self) -> Result<(), CamelError> {
541 Ok(())
542 }
543 fn concurrency_model(&self) -> ConcurrencyModel {
544 ConcurrencyModel::Concurrent { max: Some(16) }
545 }
546 }
547
548 let consumer = ConcurrentConsumer;
549 assert_eq!(
550 consumer.concurrency_model(),
551 ConcurrencyModel::Concurrent { max: Some(16) }
552 );
553 }
554
555 #[test]
558 fn test_default_startup_mode_is_immediate() {
559 struct DummyConsumer;
560
561 #[async_trait::async_trait]
562 impl super::Consumer for DummyConsumer {
563 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
564 Ok(())
565 }
566 async fn stop(&mut self) -> Result<(), CamelError> {
567 Ok(())
568 }
569 }
570
571 let consumer = DummyConsumer;
572 assert_eq!(
573 consumer.startup_mode(),
574 super::ConsumerStartupMode::Immediate
575 );
576 }
577
578 #[test]
579 fn test_startup_mode_explicit_override() {
580 struct ExplicitConsumer;
581
582 #[async_trait::async_trait]
583 impl super::Consumer for ExplicitConsumer {
584 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
585 Ok(())
586 }
587 async fn stop(&mut self) -> Result<(), CamelError> {
588 Ok(())
589 }
590 fn startup_mode(&self) -> super::ConsumerStartupMode {
591 super::ConsumerStartupMode::Explicit
592 }
593 }
594
595 let consumer = ExplicitConsumer;
596 assert_eq!(
597 consumer.startup_mode(),
598 super::ConsumerStartupMode::Explicit
599 );
600 }
601
602 #[tokio::test]
603 async fn test_startup_signal_mark_ready_resolves_receiver_ok() {
604 let (signal, receiver) = StartupSignal::pair();
605 assert!(matches!(*receiver.rx.borrow(), StartupState::Pending));
607
608 signal.mark_ready();
610 let result = receiver.await_ready().await;
611 assert!(result.is_ok(), "expected Ok after mark_ready");
612 }
613
614 #[tokio::test]
615 async fn test_startup_signal_mark_failed_propagates_error() {
616 let (signal, receiver) = StartupSignal::pair();
617 signal.mark_failed("bind failed".to_string());
618 let err = receiver
619 .await_ready()
620 .await
621 .expect_err("expected Err after mark_failed");
622 match err {
623 CamelError::RouteError(msg) => assert!(msg.contains("bind failed")),
624 other => panic!("expected RouteError, got {other:?}"),
625 }
626 }
627
628 #[tokio::test]
629 async fn test_startup_signal_idempotent_first_wins() {
630 let (signal, receiver) = StartupSignal::pair();
631 signal.mark_ready();
632 signal.mark_failed("late failure".to_string());
634 let result = receiver.await_ready().await;
635 assert!(result.is_ok(), "first transition (Ready) wins");
636 }
637
638 #[tokio::test]
639 async fn test_startup_receiver_immediate_is_pre_resolved_ok() {
640 let receiver = StartupReceiver::immediate();
641 let result = receiver.await_ready().await;
642 assert!(result.is_ok(), "immediate receiver must resolve Ok");
643 }
644
645 #[tokio::test]
646 async fn test_consumer_context_mark_ready_drives_signal() {
647 let (tx, _rx) = mpsc::channel(1);
648 let ctx = ConsumerContext::new(
649 tx,
650 CancellationToken::new(),
651 "startup-test-route".to_string(),
652 );
653 let (signal, receiver) = StartupSignal::pair();
654 let ctx = ctx.with_startup(signal);
655 ctx.mark_ready();
656 let result = receiver.await_ready().await;
657 assert!(result.is_ok(), "ctx.mark_ready must resolve the receiver");
658 }
659
660 #[tokio::test]
661 async fn test_consumer_context_mark_failed_drives_signal() {
662 let (tx, _rx) = mpsc::channel(1);
663 let ctx = ConsumerContext::new(
664 tx,
665 CancellationToken::new(),
666 "startup-fail-route".to_string(),
667 );
668 let (signal, receiver) = StartupSignal::pair();
669 let ctx = ctx.with_startup(signal);
670 ctx.mark_failed("assignment window elapsed".to_string());
671 let err = receiver
672 .await_ready()
673 .await
674 .expect_err("ctx.mark_failed must resolve the receiver as Err");
675 match err {
676 CamelError::RouteError(msg) => assert!(msg.contains("assignment window elapsed")),
677 other => panic!("expected RouteError, got {other:?}"),
678 }
679 }
680
681 #[tokio::test]
682 async fn test_startup_receiver_dropped_sender_returns_err() {
683 let (_signal, receiver) = StartupSignal::pair();
686 drop(_signal);
687 let err = receiver
688 .await_ready()
689 .await
690 .expect_err("dropped signal must surface as Err");
691 match err {
692 CamelError::RouteError(msg) => assert!(msg.contains("dropped")),
693 other => panic!("expected RouteError, got {other:?}"),
694 }
695 }
696
697 #[tokio::test]
698 async fn test_consumer_default_suspend_resume() {
699 struct DummyConsumer;
700
701 #[async_trait::async_trait]
702 impl super::Consumer for DummyConsumer {
703 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
704 Ok(())
705 }
706 async fn stop(&mut self) -> Result<(), CamelError> {
707 Ok(())
708 }
709 }
710
711 let consumer = DummyConsumer;
712 assert!(consumer.suspend().await.is_ok());
713 assert!(consumer.resume().await.is_ok());
714 }
715
716 struct StubPolicy;
719
720 #[async_trait::async_trait]
721 impl SecurityPolicy for StubPolicy {
722 async fn evaluate(
723 &self,
724 _exchange: &mut Exchange,
725 ) -> Result<camel_api::security_policy::AuthorizationDecision, CamelError> {
726 Ok(camel_api::security_policy::AuthorizationDecision::Granted {
727 principal: camel_api::security_policy::Principal {
728 subject: "stub".into(),
729 issuer: "stub".into(),
730 audience: vec![],
731 scopes: vec![],
732 roles: vec![],
733 claims: serde_json::json!({}),
734 },
735 })
736 }
737 }
738
739 struct StubAuthenticator;
740
741 #[async_trait::async_trait]
742 impl camel_auth::TokenAuthenticator for StubAuthenticator {
743 async fn authenticate_bearer(
744 &self,
745 _token: &str,
746 ) -> Result<camel_api::security_policy::Principal, CamelError> {
747 Ok(camel_api::security_policy::Principal {
748 subject: "stub".into(),
749 issuer: "stub".into(),
750 audience: vec![],
751 scopes: vec![],
752 roles: vec![],
753 claims: serde_json::json!({}),
754 })
755 }
756 }
757
758 #[test]
759 fn test_security_context_new() {
760 let ctx = SecurityContext::new(StubPolicy, Arc::new(StubAuthenticator));
761 assert!(Arc::strong_count(&ctx.policy) == 1);
762 assert!(Arc::strong_count(&ctx.authenticator) == 1);
763 assert_eq!(
764 ctx.credential_sources,
765 vec![camel_auth::CredentialSource::AuthorizationHeader]
766 );
767 }
768
769 #[test]
770 fn test_security_context_from_arc() {
771 let policy: Arc<dyn SecurityPolicy> = Arc::new(StubPolicy);
772 let authenticator: Arc<dyn camel_auth::TokenAuthenticator> = Arc::new(StubAuthenticator);
773 let ctx = SecurityContext::from_arc(Arc::clone(&policy), Arc::clone(&authenticator));
774 assert!(Arc::ptr_eq(&ctx.policy, &policy));
775 assert!(Arc::ptr_eq(&ctx.authenticator, &authenticator));
776 assert_eq!(
777 ctx.credential_sources,
778 vec![camel_auth::CredentialSource::AuthorizationHeader]
779 );
780 }
781
782 #[test]
783 fn test_security_context_clone_independent() {
784 let ctx = SecurityContext::new(StubPolicy, Arc::new(StubAuthenticator));
785 let cloned = ctx.clone();
786 assert!(Arc::ptr_eq(&ctx.policy, &cloned.policy));
787 assert!(Arc::ptr_eq(&ctx.authenticator, &cloned.authenticator));
788 assert_eq!(ctx.credential_sources, cloned.credential_sources);
789 }
790
791 #[test]
792 fn test_security_context_debug_redacts_traits() {
793 let ctx = SecurityContext::new(StubPolicy, Arc::new(StubAuthenticator));
794 let debug_str = format!("{ctx:?}");
795 assert!(debug_str.contains("<SecurityPolicy>"));
796 assert!(debug_str.contains("<TokenAuthenticator>"));
797 assert!(debug_str.contains("credential_sources"));
798 }
799
800 #[test]
801 fn test_security_context_with_credential_sources() {
802 let ctx = SecurityContext::new(StubPolicy, Arc::new(StubAuthenticator))
803 .with_credential_sources(vec![
804 camel_auth::CredentialSource::Cookie {
805 name: "session".into(),
806 },
807 camel_auth::CredentialSource::AuthorizationHeader,
808 ]);
809 assert_eq!(ctx.credential_sources.len(), 2);
810 assert!(matches!(
811 &ctx.credential_sources[0],
812 camel_auth::CredentialSource::Cookie { .. }
813 ));
814 }
815}