use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock};
use cpex_core::delegation::HOOK_TOKEN_DELEGATE;
use cpex_core::hooks::{lookup_hook_metadata, HookPhase};
use cpex_core::manager::PluginManager;
use cpex_core::plugin::OnError;
use cpex_core::registry::HookEntry;
use apl_core::pipeline::Stage;
use apl_core::plugin_decl::{EffectivePlugin, PluginRegistry};
use apl_core::rules::{CompiledRoute, Effect};
#[derive(Clone)]
pub struct RoutePluginEntry {
pub plugin_name: String,
pub entries_by_hook: HashMap<String, HookEntry>,
}
impl RoutePluginEntry {
pub fn pick_entry(
&self,
requested_entity_type: Option<&str>,
requested_phase: HookPhase,
) -> Option<&HookEntry> {
self.entries_by_hook
.iter()
.find(|(hook_name, _)| {
lookup_hook_metadata(hook_name).matches(requested_entity_type, requested_phase)
})
.map(|(_, entry)| entry)
}
}
#[derive(Clone, Default)]
pub struct RouteDispatchPlan {
pub plugins: HashMap<String, RoutePluginEntry>,
pub delegation_entries: HashMap<String, HookEntry>,
}
impl RouteDispatchPlan {
pub async fn build(
route: &CompiledRoute,
registry: &PluginRegistry,
manager: &PluginManager,
) -> Self {
let mut plan = Self::default();
for name in collect_plugin_names(route) {
let eff = match EffectivePlugin::resolve(&name, registry, &route.plugin_overrides) {
Some(e) => e,
None => {
tracing::warn!(
plugin = %name,
route = %route.route_key,
"APL route references plugin not in `plugins:` block — skipping",
);
continue;
},
};
let override_block = route.plugin_overrides.get(&name);
let config_override = override_block.and_then(|o| o.config.as_ref());
let caps_override: Option<std::collections::HashSet<String>> = if matches!(
eff.capabilities,
apl_core::plugin_decl::CapsView::Override(_)
) {
Some(eff.capabilities.as_slice().iter().cloned().collect())
} else {
None
};
let on_error_override = override_block
.and_then(|o| o.on_error.as_deref())
.and_then(parse_on_error);
let entries = manager
.build_override_entries(
&name,
config_override,
caps_override.as_ref(),
on_error_override,
)
.await;
if entries.is_empty() {
tracing::warn!(
plugin = %name,
route = %route.route_key,
"APL plugin not resolvable (not registered, factory missing, \
or override construction failed) — skipping",
);
continue;
}
let mut entries_by_hook: HashMap<String, HookEntry> = HashMap::new();
for (hook_name, entry) in entries {
entries_by_hook.insert(hook_name, entry);
}
plan.plugins.insert(
name.clone(),
RoutePluginEntry {
plugin_name: name,
entries_by_hook,
},
);
}
for name in collect_delegate_plugin_names(route) {
let entries = manager
.build_override_entries(&name, None, None, None)
.await;
let delegate_entry = entries
.into_iter()
.find(|(hook_name, _)| hook_name == HOOK_TOKEN_DELEGATE);
if let Some((_, entry)) = delegate_entry {
plan.delegation_entries.insert(name, entry);
} else {
tracing::warn!(
plugin = %name,
route = %route.route_key,
"APL route references delegate plugin not registered under \
token.delegate hook — `delegate(...)` step will fail at dispatch",
);
}
}
plan
}
pub fn get(&self, plugin_name: &str) -> Option<&RoutePluginEntry> {
self.plugins.get(plugin_name)
}
pub fn resolve_plugin(manager: &PluginManager, plugin_name: &str) -> Option<RoutePluginEntry> {
let base_entries = manager.find_plugin_entries(plugin_name);
if base_entries.is_empty() {
return None;
}
let mut entries_by_hook: HashMap<String, HookEntry> = HashMap::new();
for (hook_name, entry) in base_entries {
entries_by_hook.insert(hook_name, entry);
}
Some(RoutePluginEntry {
plugin_name: plugin_name.to_string(),
entries_by_hook,
})
}
}
fn parse_on_error(s: &str) -> Option<OnError> {
match s.to_ascii_lowercase().as_str() {
"fail" => Some(OnError::Fail),
"ignore" => Some(OnError::Ignore),
"disable" => Some(OnError::Disable),
_ => None,
}
}
fn walk_effects<F: FnMut(&Effect)>(effects: &[Effect], visit: &mut F) {
for e in effects {
visit(e);
match e {
Effect::When { body, .. } => walk_effects(body, visit),
Effect::Sequential(inner) | Effect::Parallel(inner) => walk_effects(inner, visit),
Effect::Pdp {
on_allow, on_deny, ..
} => {
walk_effects(on_allow, visit);
walk_effects(on_deny, visit);
},
_ => {},
}
}
}
pub(crate) fn collect_delegate_plugin_names(route: &CompiledRoute) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
let mut visit = |e: &Effect| {
if let Effect::Delegate(ds) = e {
if seen.insert(ds.plugin_name.clone()) {
out.push(ds.plugin_name.clone());
}
}
};
walk_effects(&route.policy, &mut visit);
walk_effects(&route.post_policy, &mut visit);
out
}
pub(crate) fn collect_plugin_names(route: &CompiledRoute) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
let mut visit = |e: &Effect| {
if let Effect::Plugin { name } = e {
if seen.insert(name.clone()) {
out.push(name.clone());
}
}
};
walk_effects(&route.policy, &mut visit);
walk_effects(&route.post_policy, &mut visit);
for fr in route.args.iter().chain(route.result.iter()) {
for stage in &fr.pipeline.stages {
if let Stage::Plugin { name } = stage {
if seen.insert(name.clone()) {
out.push(name.clone());
}
}
}
}
out
}
pub(crate) fn route_capability_union(
route: &CompiledRoute,
registry: &PluginRegistry,
) -> std::collections::HashSet<String> {
let mut caps: std::collections::HashSet<String> = std::collections::HashSet::new();
for name in collect_plugin_names(route) {
if let Some(eff) = EffectivePlugin::resolve(&name, registry, &route.plugin_overrides) {
for cap in eff.capabilities.as_slice() {
caps.insert(cap.clone());
}
}
}
for name in collect_delegate_plugin_names(route) {
if let Some(eff) = EffectivePlugin::resolve(&name, registry, &route.plugin_overrides) {
for cap in eff.capabilities.as_slice() {
caps.insert(cap.clone());
}
}
}
caps
}
#[derive(Default)]
pub struct DispatchCache {
inner: RwLock<HashMap<String, (u64, Arc<RouteDispatchPlan>)>>,
}
impl DispatchCache {
pub fn new() -> Self {
Self::default()
}
pub async fn get_or_build(
&self,
route: &CompiledRoute,
registry: &PluginRegistry,
manager: &PluginManager,
) -> Arc<RouteDispatchPlan> {
let current_gen = manager.config_generation();
{
let r = self.inner.read().unwrap_or_else(|p| p.into_inner());
if let Some((stored_gen, plan)) = r.get(&route.route_key) {
if *stored_gen == current_gen {
return Arc::clone(plan);
}
}
}
let plan = Arc::new(RouteDispatchPlan::build(route, registry, manager).await);
let mut w = self.inner.write().unwrap_or_else(|p| p.into_inner());
w.insert(route.route_key.clone(), (current_gen, Arc::clone(&plan)));
plan
}
}