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)]
38#[non_exhaustive]
39pub enum ConsumerStartupMode {
40 #[default]
43 Immediate,
44 Explicit,
47}
48
49#[derive(Clone, Debug)]
51enum StartupState {
52 Pending,
54 Ready,
56 Failed(String),
58}
59
60#[derive(Clone)]
67pub struct StartupSignal {
68 tx: watch::Sender<StartupState>,
69}
70
71impl StartupSignal {
72 pub fn pair() -> (Self, StartupReceiver) {
74 let (tx, rx) = watch::channel(StartupState::Pending);
75 (Self { tx }, StartupReceiver { rx })
76 }
77
78 pub fn mark_ready(&self) -> bool {
88 self.tx.send_if_modified(|s| {
89 if matches!(*s, StartupState::Pending) {
90 *s = StartupState::Ready;
91 true
92 } else {
93 false
94 }
95 })
96 }
97
98 pub fn mark_failed(&self, err: String) {
100 self.tx.send_if_modified(|s| {
101 if matches!(*s, StartupState::Pending) {
102 *s = StartupState::Failed(err);
103 true
104 } else {
105 false
106 }
107 });
108 }
109}
110
111impl std::fmt::Debug for StartupSignal {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 f.debug_struct("StartupSignal")
114 .field("state", &self.tx.borrow())
115 .finish()
116 }
117}
118
119pub struct StartupReceiver {
126 rx: watch::Receiver<StartupState>,
127}
128
129impl StartupReceiver {
130 pub fn immediate() -> Self {
134 let (tx, rx) = watch::channel(StartupState::Ready);
135 let _ = tx;
138 Self { rx }
139 }
140
141 pub async fn await_ready(mut self) -> Result<(), CamelError> {
148 loop {
149 match &*self.rx.borrow() {
150 StartupState::Pending => {}
151 StartupState::Ready => return Ok(()),
152 StartupState::Failed(msg) => {
153 return Err(CamelError::RouteError(msg.clone()));
154 }
155 }
156 if self.rx.changed().await.is_err() {
157 return Err(CamelError::RouteError(
158 "consumer startup signal dropped without resolving".to_string(),
159 ));
160 }
161 }
162 }
163}
164
165impl std::fmt::Debug for StartupReceiver {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 f.debug_struct("StartupReceiver")
168 .field("state", &self.rx.borrow())
169 .finish()
170 }
171}
172
173#[derive(Clone)]
175pub struct ConsumerContext {
176 sender: mpsc::Sender<ExchangeEnvelope>,
177 cancel_token: CancellationToken,
178 route_id: String,
179 startup: StartupSignal,
180}
181
182impl ConsumerContext {
183 pub fn new(
193 sender: mpsc::Sender<ExchangeEnvelope>,
194 cancel_token: CancellationToken,
195 route_id: String,
196 ) -> Self {
197 let (startup, _unused_receiver) = StartupSignal::pair();
198 let _ = _unused_receiver;
202 Self {
203 sender,
204 cancel_token,
205 route_id,
206 startup,
207 }
208 }
209
210 pub fn with_startup(mut self, startup: StartupSignal) -> Self {
214 self.startup = startup;
215 self
216 }
217
218 pub fn startup_signal(&self) -> StartupSignal {
222 self.startup.clone()
223 }
224
225 pub fn mark_ready(&self) {
232 let _ = self.startup.mark_ready();
233 }
234
235 pub fn mark_failed(&self, err: String) {
245 self.startup.mark_failed(err);
246 }
247
248 pub async fn cancelled(&self) {
251 self.cancel_token.cancelled().await
252 }
253
254 pub fn is_cancelled(&self) -> bool {
256 self.cancel_token.is_cancelled()
257 }
258
259 pub fn route_id(&self) -> &str {
265 &self.route_id
266 }
267
268 pub fn cancel_token(&self) -> CancellationToken {
273 self.cancel_token.clone()
274 }
275
276 pub fn sender(&self) -> mpsc::Sender<ExchangeEnvelope> {
282 self.sender.clone()
283 }
284
285 pub async fn send(&self, exchange: Exchange) -> Result<(), CamelError> {
287 self.sender
288 .send(ExchangeEnvelope {
289 exchange,
290 reply_tx: None,
291 })
292 .await
293 .map_err(|_| CamelError::ChannelClosed)
294 }
295
296 pub async fn send_and_wait(&self, exchange: Exchange) -> Result<Exchange, CamelError> {
301 let (reply_tx, reply_rx) = oneshot::channel();
302 self.sender
303 .send(ExchangeEnvelope {
304 exchange,
305 reply_tx: Some(reply_tx),
306 })
307 .await
308 .map_err(|_| CamelError::ChannelClosed)?;
309 reply_rx.await.map_err(|_| CamelError::ChannelClosed)?
310 }
311}
312
313pub struct SecurityContext {
319 pub policy: Arc<dyn SecurityPolicy>,
320 pub authenticator: Arc<dyn TokenAuthenticator>,
321 pub credential_sources: Vec<CredentialSource>,
322}
323
324impl SecurityContext {
325 pub fn new(
326 policy: impl SecurityPolicy + 'static,
327 authenticator: Arc<dyn TokenAuthenticator>,
328 ) -> Self {
329 Self {
330 policy: Arc::new(policy),
331 authenticator,
332 credential_sources: vec![CredentialSource::AuthorizationHeader],
333 }
334 }
335
336 pub fn from_arc(
337 policy: Arc<dyn SecurityPolicy>,
338 authenticator: Arc<dyn TokenAuthenticator>,
339 ) -> Self {
340 Self {
341 policy,
342 authenticator,
343 credential_sources: vec![CredentialSource::AuthorizationHeader],
344 }
345 }
346
347 pub fn with_credential_sources(mut self, sources: Vec<CredentialSource>) -> Self {
348 self.credential_sources = sources;
349 self
350 }
351}
352
353impl Clone for SecurityContext {
354 fn clone(&self) -> Self {
355 Self {
356 policy: Arc::clone(&self.policy),
357 authenticator: Arc::clone(&self.authenticator),
358 credential_sources: self.credential_sources.clone(),
359 }
360 }
361}
362
363impl std::fmt::Debug for SecurityContext {
364 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365 f.debug_struct("SecurityContext")
366 .field("policy", &"<SecurityPolicy>")
367 .field("authenticator", &"<TokenAuthenticator>")
368 .field("credential_sources", &self.credential_sources)
369 .finish()
370 }
371}
372
373#[derive(Debug, Clone, PartialEq, Eq)]
375#[non_exhaustive]
376pub enum ConcurrencyModel {
377 Sequential,
380 Concurrent { max: Option<usize> },
384}
385
386#[async_trait]
410pub trait Consumer: Send + Sync {
411 async fn start(&mut self, context: ConsumerContext) -> Result<(), CamelError>;
413
414 async fn stop(&mut self) -> Result<(), CamelError>;
419
420 async fn suspend(&self) -> Result<(), CamelError> {
424 Ok(())
425 }
426
427 async fn resume(&self) -> Result<(), CamelError> {
431 Ok(())
432 }
433
434 fn concurrency_model(&self) -> ConcurrencyModel {
443 ConcurrencyModel::Sequential
444 }
445
446 fn startup_mode(&self) -> ConsumerStartupMode {
461 ConsumerStartupMode::Immediate
462 }
463
464 fn background_task_handle(&mut self) -> Option<JoinHandle<Result<(), CamelError>>> {
476 None
477 }
478
479 fn set_security_context(&mut self, _ctx: SecurityContext) {}
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491
492 #[test]
493 fn consumer_context_exposes_route_id() {
494 let (tx, _rx) = mpsc::channel(1);
495 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "test-route".to_string());
496 assert_eq!(ctx.route_id(), "test-route");
497 }
498
499 #[tokio::test]
500 async fn test_consumer_context_cancelled() {
501 let (tx, _rx) = mpsc::channel(16);
502 let token = CancellationToken::new();
503 let ctx = ConsumerContext::new(tx, token.clone(), "test-route".to_string());
504
505 assert!(!ctx.is_cancelled());
506 token.cancel();
507 ctx.cancelled().await;
508 assert!(ctx.is_cancelled());
509 }
510
511 #[test]
512 fn test_concurrency_model_default_is_sequential() {
513 use super::ConcurrencyModel;
514
515 struct DummyConsumer;
516
517 #[async_trait::async_trait]
518 impl super::Consumer for DummyConsumer {
519 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
520 Ok(())
521 }
522 async fn stop(&mut self) -> Result<(), CamelError> {
523 Ok(())
524 }
525 }
526
527 let consumer = DummyConsumer;
528 assert_eq!(consumer.concurrency_model(), ConcurrencyModel::Sequential);
529 }
530
531 #[test]
532 fn test_concurrency_model_concurrent_override() {
533 use super::ConcurrencyModel;
534
535 struct ConcurrentConsumer;
536
537 #[async_trait::async_trait]
538 impl super::Consumer for ConcurrentConsumer {
539 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
540 Ok(())
541 }
542 async fn stop(&mut self) -> Result<(), CamelError> {
543 Ok(())
544 }
545 fn concurrency_model(&self) -> ConcurrencyModel {
546 ConcurrencyModel::Concurrent { max: Some(16) }
547 }
548 }
549
550 let consumer = ConcurrentConsumer;
551 assert_eq!(
552 consumer.concurrency_model(),
553 ConcurrencyModel::Concurrent { max: Some(16) }
554 );
555 }
556
557 #[test]
560 fn test_default_startup_mode_is_immediate() {
561 struct DummyConsumer;
562
563 #[async_trait::async_trait]
564 impl super::Consumer for DummyConsumer {
565 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
566 Ok(())
567 }
568 async fn stop(&mut self) -> Result<(), CamelError> {
569 Ok(())
570 }
571 }
572
573 let consumer = DummyConsumer;
574 assert_eq!(
575 consumer.startup_mode(),
576 super::ConsumerStartupMode::Immediate
577 );
578 }
579
580 #[test]
581 fn test_startup_mode_explicit_override() {
582 struct ExplicitConsumer;
583
584 #[async_trait::async_trait]
585 impl super::Consumer for ExplicitConsumer {
586 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
587 Ok(())
588 }
589 async fn stop(&mut self) -> Result<(), CamelError> {
590 Ok(())
591 }
592 fn startup_mode(&self) -> super::ConsumerStartupMode {
593 super::ConsumerStartupMode::Explicit
594 }
595 }
596
597 let consumer = ExplicitConsumer;
598 assert_eq!(
599 consumer.startup_mode(),
600 super::ConsumerStartupMode::Explicit
601 );
602 }
603
604 #[tokio::test]
605 async fn test_startup_signal_mark_ready_resolves_receiver_ok() {
606 let (signal, receiver) = StartupSignal::pair();
607 assert!(matches!(*receiver.rx.borrow(), StartupState::Pending));
609
610 signal.mark_ready();
612 let result = receiver.await_ready().await;
613 assert!(result.is_ok(), "expected Ok after mark_ready");
614 }
615
616 #[tokio::test]
617 async fn test_startup_signal_mark_failed_propagates_error() {
618 let (signal, receiver) = StartupSignal::pair();
619 signal.mark_failed("bind failed".to_string());
620 let err = receiver
621 .await_ready()
622 .await
623 .expect_err("expected Err after mark_failed");
624 match err {
625 CamelError::RouteError(msg) => assert!(msg.contains("bind failed")),
626 other => panic!("expected RouteError, got {other:?}"),
627 }
628 }
629
630 #[tokio::test]
631 async fn test_startup_signal_idempotent_first_wins() {
632 let (signal, receiver) = StartupSignal::pair();
633 signal.mark_ready();
634 signal.mark_failed("late failure".to_string());
636 let result = receiver.await_ready().await;
637 assert!(result.is_ok(), "first transition (Ready) wins");
638 }
639
640 #[tokio::test]
641 async fn test_startup_receiver_immediate_is_pre_resolved_ok() {
642 let receiver = StartupReceiver::immediate();
643 let result = receiver.await_ready().await;
644 assert!(result.is_ok(), "immediate receiver must resolve Ok");
645 }
646
647 #[tokio::test]
648 async fn test_consumer_context_mark_ready_drives_signal() {
649 let (tx, _rx) = mpsc::channel(1);
650 let ctx = ConsumerContext::new(
651 tx,
652 CancellationToken::new(),
653 "startup-test-route".to_string(),
654 );
655 let (signal, receiver) = StartupSignal::pair();
656 let ctx = ctx.with_startup(signal);
657 ctx.mark_ready();
658 let result = receiver.await_ready().await;
659 assert!(result.is_ok(), "ctx.mark_ready must resolve the receiver");
660 }
661
662 #[tokio::test]
663 async fn test_consumer_context_mark_failed_drives_signal() {
664 let (tx, _rx) = mpsc::channel(1);
665 let ctx = ConsumerContext::new(
666 tx,
667 CancellationToken::new(),
668 "startup-fail-route".to_string(),
669 );
670 let (signal, receiver) = StartupSignal::pair();
671 let ctx = ctx.with_startup(signal);
672 ctx.mark_failed("assignment window elapsed".to_string());
673 let err = receiver
674 .await_ready()
675 .await
676 .expect_err("ctx.mark_failed must resolve the receiver as Err");
677 match err {
678 CamelError::RouteError(msg) => assert!(msg.contains("assignment window elapsed")),
679 other => panic!("expected RouteError, got {other:?}"),
680 }
681 }
682
683 #[tokio::test]
684 async fn test_startup_receiver_dropped_sender_returns_err() {
685 let (_signal, receiver) = StartupSignal::pair();
688 drop(_signal);
689 let err = receiver
690 .await_ready()
691 .await
692 .expect_err("dropped signal must surface as Err");
693 match err {
694 CamelError::RouteError(msg) => assert!(msg.contains("dropped")),
695 other => panic!("expected RouteError, got {other:?}"),
696 }
697 }
698
699 #[tokio::test]
700 async fn test_consumer_default_suspend_resume() {
701 struct DummyConsumer;
702
703 #[async_trait::async_trait]
704 impl super::Consumer for DummyConsumer {
705 async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
706 Ok(())
707 }
708 async fn stop(&mut self) -> Result<(), CamelError> {
709 Ok(())
710 }
711 }
712
713 let consumer = DummyConsumer;
714 assert!(consumer.suspend().await.is_ok());
715 assert!(consumer.resume().await.is_ok());
716 }
717
718 struct StubPolicy;
721
722 #[async_trait::async_trait]
723 impl SecurityPolicy for StubPolicy {
724 async fn evaluate(
725 &self,
726 _exchange: &mut Exchange,
727 ) -> Result<camel_api::security_policy::AuthorizationDecision, CamelError> {
728 Ok(camel_api::security_policy::AuthorizationDecision::Granted {
729 principal: camel_api::security_policy::Principal {
730 subject: "stub".into(),
731 issuer: "stub".into(),
732 audience: vec![],
733 scopes: vec![],
734 roles: vec![],
735 claims: serde_json::json!({}),
736 },
737 })
738 }
739 }
740
741 struct StubAuthenticator;
742
743 #[async_trait::async_trait]
744 impl camel_auth::TokenAuthenticator for StubAuthenticator {
745 async fn authenticate_bearer(
746 &self,
747 _token: &str,
748 ) -> Result<camel_api::security_policy::Principal, CamelError> {
749 Ok(camel_api::security_policy::Principal {
750 subject: "stub".into(),
751 issuer: "stub".into(),
752 audience: vec![],
753 scopes: vec![],
754 roles: vec![],
755 claims: serde_json::json!({}),
756 })
757 }
758 }
759
760 #[test]
761 fn test_security_context_new() {
762 let ctx = SecurityContext::new(StubPolicy, Arc::new(StubAuthenticator));
763 assert!(Arc::strong_count(&ctx.policy) == 1);
764 assert!(Arc::strong_count(&ctx.authenticator) == 1);
765 assert_eq!(
766 ctx.credential_sources,
767 vec![camel_auth::CredentialSource::AuthorizationHeader]
768 );
769 }
770
771 #[test]
772 fn test_security_context_from_arc() {
773 let policy: Arc<dyn SecurityPolicy> = Arc::new(StubPolicy);
774 let authenticator: Arc<dyn camel_auth::TokenAuthenticator> = Arc::new(StubAuthenticator);
775 let ctx = SecurityContext::from_arc(Arc::clone(&policy), Arc::clone(&authenticator));
776 assert!(Arc::ptr_eq(&ctx.policy, &policy));
777 assert!(Arc::ptr_eq(&ctx.authenticator, &authenticator));
778 assert_eq!(
779 ctx.credential_sources,
780 vec![camel_auth::CredentialSource::AuthorizationHeader]
781 );
782 }
783
784 #[test]
785 fn test_security_context_clone_independent() {
786 let ctx = SecurityContext::new(StubPolicy, Arc::new(StubAuthenticator));
787 let cloned = ctx.clone();
788 assert!(Arc::ptr_eq(&ctx.policy, &cloned.policy));
789 assert!(Arc::ptr_eq(&ctx.authenticator, &cloned.authenticator));
790 assert_eq!(ctx.credential_sources, cloned.credential_sources);
791 }
792
793 #[test]
794 fn test_security_context_debug_redacts_traits() {
795 let ctx = SecurityContext::new(StubPolicy, Arc::new(StubAuthenticator));
796 let debug_str = format!("{ctx:?}");
797 assert!(debug_str.contains("<SecurityPolicy>"));
798 assert!(debug_str.contains("<TokenAuthenticator>"));
799 assert!(debug_str.contains("credential_sources"));
800 }
801
802 #[test]
803 fn test_security_context_with_credential_sources() {
804 let ctx = SecurityContext::new(StubPolicy, Arc::new(StubAuthenticator))
805 .with_credential_sources(vec![
806 camel_auth::CredentialSource::Cookie {
807 name: "session".into(),
808 },
809 camel_auth::CredentialSource::AuthorizationHeader,
810 ]);
811 assert_eq!(ctx.credential_sources.len(), 2);
812 assert!(matches!(
813 &ctx.credential_sources[0],
814 camel_auth::CredentialSource::Cookie { .. }
815 ));
816 }
817}