use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use apl_core::attributes::AttributeBag;
use apl_core::step::{PdpCall, PdpDecision, PdpDialect, PdpError, PdpResolver};
#[derive(Clone)]
pub struct PdpRouter {
resolvers: HashMap<PdpDialect, Arc<dyn PdpResolver>>,
}
impl PdpRouter {
pub fn new() -> Self {
Self {
resolvers: HashMap::new(),
}
}
pub fn register(&mut self, resolver: Arc<dyn PdpResolver>) -> &mut Self {
let dialect = resolver.dialect();
if self.resolvers.contains_key(&dialect) {
tracing::warn!(
dialect = ?dialect,
"PdpRouter: resolver for dialect already registered — keeping existing",
);
return self;
}
self.resolvers.insert(dialect, resolver);
self
}
pub fn replace(&mut self, resolver: Arc<dyn PdpResolver>) -> &mut Self {
let dialect = resolver.dialect();
self.resolvers.insert(dialect, resolver);
self
}
pub fn len(&self) -> usize {
self.resolvers.len()
}
pub fn is_empty(&self) -> bool {
self.resolvers.is_empty()
}
}
impl Default for PdpRouter {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl PdpResolver for PdpRouter {
fn dialect(&self) -> PdpDialect {
PdpDialect::Custom("router".to_string())
}
async fn evaluate(&self, call: &PdpCall, bag: &AttributeBag) -> Result<PdpDecision, PdpError> {
let resolver = self
.resolvers
.get(&call.dialect)
.ok_or_else(|| PdpError::NoResolver(call.dialect.clone()))?;
resolver.evaluate(call, bag).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use apl_core::evaluator::Decision;
struct FakePdp {
dialect: PdpDialect,
decision: Decision,
}
#[async_trait]
impl PdpResolver for FakePdp {
fn dialect(&self) -> PdpDialect {
self.dialect.clone()
}
async fn evaluate(
&self,
_call: &PdpCall,
_bag: &AttributeBag,
) -> Result<PdpDecision, PdpError> {
Ok(PdpDecision {
decision: self.decision.clone(),
diagnostics: Vec::new(),
})
}
}
#[tokio::test]
async fn routes_by_dialect() {
let mut router = PdpRouter::new();
router.register(Arc::new(FakePdp {
dialect: PdpDialect::Cedar,
decision: Decision::Allow,
}));
router.register(Arc::new(FakePdp {
dialect: PdpDialect::Opa,
decision: Decision::Deny {
reason: Some("opa says no".into()),
rule_source: "opa".into(),
},
}));
let bag = AttributeBag::default();
let cedar_call = PdpCall {
dialect: PdpDialect::Cedar,
args: serde_yaml::Value::Null,
};
let opa_call = PdpCall {
dialect: PdpDialect::Opa,
args: serde_yaml::Value::Null,
};
let cedar_res = router.evaluate(&cedar_call, &bag).await.unwrap();
assert!(matches!(cedar_res.decision, Decision::Allow));
let opa_res = router.evaluate(&opa_call, &bag).await.unwrap();
assert!(matches!(opa_res.decision, Decision::Deny { .. }));
}
#[tokio::test]
async fn missing_dialect_returns_no_resolver() {
let router = PdpRouter::new();
let bag = AttributeBag::default();
let call = PdpCall {
dialect: PdpDialect::Cedar,
args: serde_yaml::Value::Null,
};
let err = router.evaluate(&call, &bag).await.unwrap_err();
assert!(matches!(err, PdpError::NoResolver(_)));
}
#[tokio::test]
async fn duplicate_register_keeps_first() {
let mut router = PdpRouter::new();
router.register(Arc::new(FakePdp {
dialect: PdpDialect::Cedar,
decision: Decision::Allow,
}));
router.register(Arc::new(FakePdp {
dialect: PdpDialect::Cedar,
decision: Decision::Deny {
reason: Some("shouldn't fire".into()),
rule_source: "test".into(),
},
}));
let call = PdpCall {
dialect: PdpDialect::Cedar,
args: serde_yaml::Value::Null,
};
let res = router
.evaluate(&call, &AttributeBag::default())
.await
.unwrap();
assert!(matches!(res.decision, Decision::Allow));
}
}