use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::{mpsc, oneshot, watch};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use camel_api::security_policy::SecurityPolicy;
use camel_api::{CamelError, Exchange};
use camel_auth::CredentialSource;
pub struct ExchangeEnvelope {
pub exchange: Exchange,
pub reply_tx: Option<oneshot::Sender<Result<Exchange, CamelError>>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ConsumerStartupMode {
#[default]
Immediate,
Explicit,
}
#[derive(Clone, Debug)]
enum StartupState {
Pending,
Ready,
Failed(String),
}
#[derive(Clone)]
pub struct StartupSignal {
tx: watch::Sender<StartupState>,
}
impl StartupSignal {
pub fn pair() -> (Self, StartupReceiver) {
let (tx, rx) = watch::channel(StartupState::Pending);
(Self { tx }, StartupReceiver { rx })
}
pub fn mark_ready(&self) -> bool {
self.tx.send_if_modified(|s| {
if matches!(*s, StartupState::Pending) {
*s = StartupState::Ready;
true
} else {
false
}
})
}
pub fn mark_failed(&self, err: String) {
self.tx.send_if_modified(|s| {
if matches!(*s, StartupState::Pending) {
*s = StartupState::Failed(err);
true
} else {
false
}
});
}
}
impl std::fmt::Debug for StartupSignal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StartupSignal")
.field("state", &self.tx.borrow())
.finish()
}
}
pub struct StartupReceiver {
rx: watch::Receiver<StartupState>,
}
impl StartupReceiver {
pub fn immediate() -> Self {
let (tx, rx) = watch::channel(StartupState::Ready);
let _ = tx;
Self { rx }
}
pub async fn await_ready(mut self) -> Result<(), CamelError> {
loop {
match &*self.rx.borrow() {
StartupState::Pending => {}
StartupState::Ready => return Ok(()),
StartupState::Failed(msg) => {
return Err(CamelError::RouteError(msg.clone()));
}
}
if self.rx.changed().await.is_err() {
return Err(CamelError::RouteError(
"consumer startup signal dropped without resolving".to_string(),
));
}
}
}
}
impl std::fmt::Debug for StartupReceiver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StartupReceiver")
.field("state", &self.rx.borrow())
.finish()
}
}
#[derive(Clone)]
pub struct ConsumerContext {
sender: mpsc::Sender<ExchangeEnvelope>,
cancel_token: CancellationToken,
route_id: String,
startup: StartupSignal,
}
impl ConsumerContext {
pub fn new(
sender: mpsc::Sender<ExchangeEnvelope>,
cancel_token: CancellationToken,
route_id: String,
) -> Self {
let (startup, _unused_receiver) = StartupSignal::pair();
let _ = _unused_receiver;
Self {
sender,
cancel_token,
route_id,
startup,
}
}
pub fn with_startup(mut self, startup: StartupSignal) -> Self {
self.startup = startup;
self
}
pub fn startup_signal(&self) -> StartupSignal {
self.startup.clone()
}
pub fn mark_ready(&self) {
let _ = self.startup.mark_ready();
}
pub fn mark_failed(&self, err: String) {
self.startup.mark_failed(err);
}
pub async fn cancelled(&self) {
self.cancel_token.cancelled().await
}
pub fn is_cancelled(&self) -> bool {
self.cancel_token.is_cancelled()
}
pub fn route_id(&self) -> &str {
&self.route_id
}
pub fn cancel_token(&self) -> CancellationToken {
self.cancel_token.clone()
}
pub fn sender(&self) -> mpsc::Sender<ExchangeEnvelope> {
self.sender.clone()
}
pub async fn send(&self, exchange: Exchange) -> Result<(), CamelError> {
self.sender
.send(ExchangeEnvelope {
exchange,
reply_tx: None,
})
.await
.map_err(|_| CamelError::ChannelClosed)
}
pub async fn send_and_wait(&self, exchange: Exchange) -> Result<Exchange, CamelError> {
let (reply_tx, reply_rx) = oneshot::channel();
self.sender
.send(ExchangeEnvelope {
exchange,
reply_tx: Some(reply_tx),
})
.await
.map_err(|_| CamelError::ChannelClosed)?;
reply_rx.await.map_err(|_| CamelError::ChannelClosed)?
}
}
pub struct SecurityContext {
pub policy: Option<Arc<dyn SecurityPolicy>>,
pub credential_sources: Vec<CredentialSource>,
pub plan: Option<camel_api::security_policy::RouteSecurityPlan>,
pub providers: Option<std::sync::Arc<camel_auth::ProviderRegistry>>,
}
impl SecurityContext {
pub fn new(policy: impl SecurityPolicy + 'static) -> Self {
Self {
policy: Some(Arc::new(policy)),
credential_sources: vec![CredentialSource::AuthorizationHeader],
plan: None,
providers: None,
}
}
pub fn from_arc(policy: Arc<dyn SecurityPolicy>) -> Self {
Self {
policy: Some(policy),
credential_sources: vec![CredentialSource::AuthorizationHeader],
plan: None,
providers: None,
}
}
pub fn from_plan(plan: camel_api::security_policy::RouteSecurityPlan) -> Self {
Self {
policy: None,
credential_sources: Vec::new(),
plan: Some(plan),
providers: None,
}
}
pub fn with_credential_sources(mut self, sources: Vec<CredentialSource>) -> Self {
self.credential_sources = sources;
self
}
pub fn with_plan(mut self, plan: camel_api::security_policy::RouteSecurityPlan) -> Self {
self.plan = Some(plan);
self
}
pub fn with_providers(
mut self,
providers: std::sync::Arc<camel_auth::ProviderRegistry>,
) -> Self {
self.providers = Some(providers);
self
}
}
impl Clone for SecurityContext {
fn clone(&self) -> Self {
Self {
policy: self.policy.clone(),
credential_sources: self.credential_sources.clone(),
plan: self.plan.clone(),
providers: self.providers.as_ref().map(Arc::clone),
}
}
}
impl std::fmt::Debug for SecurityContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecurityContext")
.field("policy", &self.policy.as_ref().map(|_| "<SecurityPolicy>"))
.field("credential_sources", &self.credential_sources)
.field("plan", &self.plan)
.field(
"providers",
&self.providers.as_ref().map(|_| "<ProviderRegistry>"),
)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConcurrencyModel {
Sequential,
Concurrent { max: Option<usize> },
}
#[async_trait]
pub trait Consumer: Send + Sync {
async fn start(&mut self, context: ConsumerContext) -> Result<(), CamelError>;
async fn stop(&mut self) -> Result<(), CamelError>;
async fn suspend(&self) -> Result<(), CamelError> {
Ok(())
}
async fn resume(&self) -> Result<(), CamelError> {
Ok(())
}
fn concurrency_model(&self) -> ConcurrencyModel {
ConcurrencyModel::Sequential
}
fn startup_mode(&self) -> ConsumerStartupMode {
ConsumerStartupMode::Immediate
}
fn background_task_handle(&mut self) -> Option<JoinHandle<Result<(), CamelError>>> {
None
}
fn set_security_context(&mut self, _ctx: SecurityContext) {}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn consumer_context_exposes_route_id() {
let (tx, _rx) = mpsc::channel(1);
let ctx = ConsumerContext::new(tx, CancellationToken::new(), "test-route".to_string());
assert_eq!(ctx.route_id(), "test-route");
}
#[tokio::test]
async fn test_consumer_context_cancelled() {
let (tx, _rx) = mpsc::channel(16);
let token = CancellationToken::new();
let ctx = ConsumerContext::new(tx, token.clone(), "test-route".to_string());
assert!(!ctx.is_cancelled());
token.cancel();
ctx.cancelled().await;
assert!(ctx.is_cancelled());
}
#[test]
fn test_concurrency_model_default_is_sequential() {
use super::ConcurrencyModel;
struct DummyConsumer;
#[async_trait::async_trait]
impl super::Consumer for DummyConsumer {
async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
Ok(())
}
async fn stop(&mut self) -> Result<(), CamelError> {
Ok(())
}
}
let consumer = DummyConsumer;
assert_eq!(consumer.concurrency_model(), ConcurrencyModel::Sequential);
}
#[test]
fn test_concurrency_model_concurrent_override() {
use super::ConcurrencyModel;
struct ConcurrentConsumer;
#[async_trait::async_trait]
impl super::Consumer for ConcurrentConsumer {
async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
Ok(())
}
async fn stop(&mut self) -> Result<(), CamelError> {
Ok(())
}
fn concurrency_model(&self) -> ConcurrencyModel {
ConcurrencyModel::Concurrent { max: Some(16) }
}
}
let consumer = ConcurrentConsumer;
assert_eq!(
consumer.concurrency_model(),
ConcurrencyModel::Concurrent { max: Some(16) }
);
}
#[test]
fn test_default_startup_mode_is_immediate() {
struct DummyConsumer;
#[async_trait::async_trait]
impl super::Consumer for DummyConsumer {
async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
Ok(())
}
async fn stop(&mut self) -> Result<(), CamelError> {
Ok(())
}
}
let consumer = DummyConsumer;
assert_eq!(
consumer.startup_mode(),
super::ConsumerStartupMode::Immediate
);
}
#[test]
fn test_startup_mode_explicit_override() {
struct ExplicitConsumer;
#[async_trait::async_trait]
impl super::Consumer for ExplicitConsumer {
async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
Ok(())
}
async fn stop(&mut self) -> Result<(), CamelError> {
Ok(())
}
fn startup_mode(&self) -> super::ConsumerStartupMode {
super::ConsumerStartupMode::Explicit
}
}
let consumer = ExplicitConsumer;
assert_eq!(
consumer.startup_mode(),
super::ConsumerStartupMode::Explicit
);
}
#[tokio::test]
async fn test_startup_signal_mark_ready_resolves_receiver_ok() {
let (signal, receiver) = StartupSignal::pair();
assert!(matches!(*receiver.rx.borrow(), StartupState::Pending));
signal.mark_ready();
let result = receiver.await_ready().await;
assert!(result.is_ok(), "expected Ok after mark_ready");
}
#[tokio::test]
async fn test_startup_signal_mark_failed_propagates_error() {
let (signal, receiver) = StartupSignal::pair();
signal.mark_failed("bind failed".to_string());
let err = receiver
.await_ready()
.await
.expect_err("expected Err after mark_failed");
match err {
CamelError::RouteError(msg) => assert!(msg.contains("bind failed")),
other => panic!("expected RouteError, got {other:?}"),
}
}
#[tokio::test]
async fn test_startup_signal_idempotent_first_wins() {
let (signal, receiver) = StartupSignal::pair();
signal.mark_ready();
signal.mark_failed("late failure".to_string());
let result = receiver.await_ready().await;
assert!(result.is_ok(), "first transition (Ready) wins");
}
#[tokio::test]
async fn test_startup_receiver_immediate_is_pre_resolved_ok() {
let receiver = StartupReceiver::immediate();
let result = receiver.await_ready().await;
assert!(result.is_ok(), "immediate receiver must resolve Ok");
}
#[tokio::test]
async fn test_consumer_context_mark_ready_drives_signal() {
let (tx, _rx) = mpsc::channel(1);
let ctx = ConsumerContext::new(
tx,
CancellationToken::new(),
"startup-test-route".to_string(),
);
let (signal, receiver) = StartupSignal::pair();
let ctx = ctx.with_startup(signal);
ctx.mark_ready();
let result = receiver.await_ready().await;
assert!(result.is_ok(), "ctx.mark_ready must resolve the receiver");
}
#[tokio::test]
async fn test_consumer_context_mark_failed_drives_signal() {
let (tx, _rx) = mpsc::channel(1);
let ctx = ConsumerContext::new(
tx,
CancellationToken::new(),
"startup-fail-route".to_string(),
);
let (signal, receiver) = StartupSignal::pair();
let ctx = ctx.with_startup(signal);
ctx.mark_failed("assignment window elapsed".to_string());
let err = receiver
.await_ready()
.await
.expect_err("ctx.mark_failed must resolve the receiver as Err");
match err {
CamelError::RouteError(msg) => assert!(msg.contains("assignment window elapsed")),
other => panic!("expected RouteError, got {other:?}"),
}
}
#[tokio::test]
async fn test_startup_receiver_dropped_sender_returns_err() {
let (_signal, receiver) = StartupSignal::pair();
drop(_signal);
let err = receiver
.await_ready()
.await
.expect_err("dropped signal must surface as Err");
match err {
CamelError::RouteError(msg) => assert!(msg.contains("dropped")),
other => panic!("expected RouteError, got {other:?}"),
}
}
#[tokio::test]
async fn test_consumer_default_suspend_resume() {
struct DummyConsumer;
#[async_trait::async_trait]
impl super::Consumer for DummyConsumer {
async fn start(&mut self, _ctx: super::ConsumerContext) -> Result<(), CamelError> {
Ok(())
}
async fn stop(&mut self) -> Result<(), CamelError> {
Ok(())
}
}
let consumer = DummyConsumer;
assert!(consumer.suspend().await.is_ok());
assert!(consumer.resume().await.is_ok());
}
struct StubPolicy;
#[async_trait::async_trait]
impl SecurityPolicy for StubPolicy {
async fn evaluate(
&self,
_exchange: &mut Exchange,
_auth: &camel_api::security_policy::AuthContext<'_>,
) -> Result<camel_api::security_policy::AuthorizationDecision, CamelError> {
Ok(camel_api::security_policy::AuthorizationDecision::Granted {
principal: camel_api::security_policy::Principal {
subject: "stub".into(),
issuer: "stub".into(),
audience: vec![],
scopes: vec![],
roles: vec![],
claims: serde_json::json!({}),
},
})
}
}
struct StubAuthenticator;
#[async_trait::async_trait]
impl camel_auth::TokenAuthenticator for StubAuthenticator {
async fn authenticate_bearer(
&self,
_token: &str,
) -> Result<camel_api::security_policy::Principal, CamelError> {
Ok(camel_api::security_policy::Principal {
subject: "stub".into(),
issuer: "stub".into(),
audience: vec![],
scopes: vec![],
roles: vec![],
claims: serde_json::json!({}),
})
}
}
#[test]
fn test_security_context_new() {
let ctx = SecurityContext::new(StubPolicy);
assert!(Arc::strong_count(ctx.policy.as_ref().expect("new sets policy")) == 1);
assert_eq!(
ctx.credential_sources,
vec![camel_auth::CredentialSource::AuthorizationHeader]
);
}
#[test]
fn test_security_context_from_arc() {
let policy: Arc<dyn SecurityPolicy> = Arc::new(StubPolicy);
let ctx = SecurityContext::from_arc(Arc::clone(&policy));
assert!(Arc::ptr_eq(
ctx.policy.as_ref().expect("from_arc sets policy"),
&policy
));
assert_eq!(
ctx.credential_sources,
vec![camel_auth::CredentialSource::AuthorizationHeader]
);
}
#[test]
fn test_security_context_clone_independent() {
let ctx = SecurityContext::new(StubPolicy);
let cloned = ctx.clone();
assert!(Arc::ptr_eq(
ctx.policy.as_ref().expect("source policy"),
cloned.policy.as_ref().expect("cloned policy"),
));
assert_eq!(ctx.credential_sources, cloned.credential_sources);
}
#[test]
fn test_security_context_debug_redacts_traits() {
let ctx = SecurityContext::new(StubPolicy);
let debug_str = format!("{ctx:?}");
assert!(debug_str.contains("<SecurityPolicy>"));
assert!(debug_str.contains("credential_sources"));
}
#[test]
fn test_security_context_with_credential_sources() {
let ctx = SecurityContext::new(StubPolicy).with_credential_sources(vec![
camel_auth::CredentialSource::Cookie {
name: "session".into(),
},
camel_auth::CredentialSource::AuthorizationHeader,
]);
assert_eq!(ctx.credential_sources.len(), 2);
assert!(matches!(
&ctx.credential_sources[0],
camel_auth::CredentialSource::Cookie { .. }
));
}
#[test]
fn security_context_holds_plan_and_providers() {
use camel_api::security_policy::{AccessMode, RouteSecurityPlan, TransportId};
let plan = RouteSecurityPlan {
access_mode: AccessMode::Public,
provider_ref: None,
transport: TransportId::Http,
credential_sources: vec![],
audience_binding: None,
};
let registry = camel_auth::ProviderRegistry::new();
registry.register(
"default",
camel_auth::ProviderEntry {
authenticator: Arc::new(StubAuthenticator),
audience_binding: None,
},
);
let ctx = SecurityContext::new(StubPolicy)
.with_plan(plan)
.with_providers(Arc::new(registry));
let ctx_plan = ctx.plan.as_ref().expect("plan must be attached");
assert!(matches!(ctx_plan.access_mode, AccessMode::Public));
assert_eq!(ctx_plan.transport, TransportId::Http);
let providers = ctx.providers.as_ref().expect("providers must be attached");
let resolved = providers.resolve("default").expect("provider must resolve");
assert!(resolved.audience_binding.is_none());
}
#[test]
fn security_context_authenticator_deleted_source_scan() {
let src = include_str!("consumer.rs");
let needle = format!("pub {}authenticator", "");
assert!(
!src.contains(&needle),
"SecurityContext.authenticator must stay deleted: field found in consumer.rs"
);
}
}