use std::collections::{BTreeMap, BTreeSet};
use super::super::configuration::ConfigurationError;
use super::{
CandidateInstance, CapabilityBinding, CapabilityCardinality, HostBinding, HostCatalog,
HostDefaultPlugin, HostPluginConfiguration, HostPluginRelease, HostSlot, PluginDescriptor,
PluginInstanceId, PluginInstancePlan, PluginInstanceSource, PluginRootInstance,
PluginRootResolutionError, PluginRootSnapshot, ResolvedApp, ResolvedPluginInstance, Value,
materialize_app, select_slot_candidates,
};
pub fn resolve_plugin_root(
host: &HostCatalog,
root: &PluginRootSnapshot,
) -> Result<ResolvedApp, PluginRootResolutionError> {
let slots = index_slots(&host.slots)?;
let releases = index_releases(&host.plugins, &root.releases)?;
validate_release_slots(&releases, &slots)?;
let defaults = index_defaults(&host.defaults)?;
let configurations = index_configurations(&host.configurations, &defaults, &releases)?;
let explicit = index_root_instances(&root.instances)?;
let disabled = index_disabled(&root.disabled)?;
validate_disabled(&defaults, &explicit, &disabled)?;
let candidates = build_candidates(&defaults, &configurations, &explicit, &disabled, &releases)?;
let selected = select_slot_candidates(&slots, candidates)?;
materialize_app(selected, &host.bindings, &host.execution_lanes)
}
fn index_slots(slots: &[HostSlot]) -> Result<BTreeMap<&str, &HostSlot>, PluginRootResolutionError> {
let mut indexed = BTreeMap::new();
for slot in slots {
if indexed.insert(slot.id.as_str(), slot).is_some() {
return Err(PluginRootResolutionError::DuplicateHostSlot(
slot.id.clone(),
));
}
}
Ok(indexed)
}
fn index_releases<'a>(
host: &'a [HostPluginRelease],
root: &'a [PluginDescriptor],
) -> Result<BTreeMap<&'a str, &'a PluginDescriptor>, PluginRootResolutionError> {
let mut indexed = BTreeMap::new();
let mut overrides = BTreeMap::new();
for release in host {
let id = release.descriptor.plugin_id();
if indexed.insert(id, &release.descriptor).is_some() {
return Err(PluginRootResolutionError::DuplicatePluginRelease(
id.to_owned(),
));
}
overrides.insert(id, release.allow_root_override);
}
let mut root_ids = BTreeSet::new();
for descriptor in root {
let id = descriptor.plugin_id();
if !root_ids.insert(id) {
return Err(PluginRootResolutionError::DuplicatePluginRelease(
id.to_owned(),
));
}
if indexed.contains_key(id) && !overrides.get(id).copied().unwrap_or(false) {
return Err(PluginRootResolutionError::RootReleaseOverrideDenied(
id.to_owned(),
));
}
indexed.insert(id, descriptor);
}
Ok(indexed)
}
fn validate_release_slots(
releases: &BTreeMap<&str, &PluginDescriptor>,
slots: &BTreeMap<&str, &HostSlot>,
) -> Result<(), PluginRootResolutionError> {
for descriptor in releases.values() {
if !slots.contains_key(descriptor.root_slot()) {
return Err(PluginRootResolutionError::UnknownRootSlot {
plugin_id: descriptor.plugin_id().to_owned(),
slot: descriptor.root_slot().to_owned(),
});
}
}
Ok(())
}
fn index_defaults(
defaults: &[HostDefaultPlugin],
) -> Result<BTreeMap<&PluginInstanceId, &HostDefaultPlugin>, PluginRootResolutionError> {
let mut indexed = BTreeMap::new();
for default in defaults {
if indexed.insert(&default.id, default).is_some() {
return Err(PluginRootResolutionError::DuplicateInstance(
default.id.clone(),
));
}
}
Ok(indexed)
}
fn index_configurations<'a>(
configurations: &'a [HostPluginConfiguration],
defaults: &BTreeMap<&PluginInstanceId, &HostDefaultPlugin>,
releases: &BTreeMap<&str, &PluginDescriptor>,
) -> Result<BTreeMap<&'a PluginInstanceId, &'a Value>, PluginRootResolutionError> {
let mut indexed = BTreeMap::new();
for configuration in configurations {
if !releases.contains_key(configuration.id.plugin_id()) {
return Err(PluginRootResolutionError::InvalidHostConfiguration(
format!("`{}` has no exact Plugin Release", configuration.id),
));
}
if defaults.contains_key(&configuration.id) {
return Err(PluginRootResolutionError::InvalidHostConfiguration(
format!(
"`{}` duplicates configuration owned by a Host default",
configuration.id
),
));
}
if indexed
.insert(&configuration.id, &configuration.configuration)
.is_some()
{
return Err(PluginRootResolutionError::InvalidHostConfiguration(
format!("duplicate configuration for `{}`", configuration.id),
));
}
}
Ok(indexed)
}
fn index_root_instances(
instances: &[PluginRootInstance],
) -> Result<BTreeMap<&PluginInstanceId, &PluginRootInstance>, PluginRootResolutionError> {
let mut indexed = BTreeMap::new();
for instance in instances {
if indexed.insert(&instance.id, instance).is_some() {
return Err(PluginRootResolutionError::DuplicateInstance(
instance.id.clone(),
));
}
}
Ok(indexed)
}
fn index_disabled(
disabled: &[PluginInstanceId],
) -> Result<BTreeSet<&PluginInstanceId>, PluginRootResolutionError> {
let mut indexed = BTreeSet::new();
for instance in disabled {
if !indexed.insert(instance) {
return Err(PluginRootResolutionError::DuplicateDisabledMarker(
instance.clone(),
));
}
}
Ok(indexed)
}
fn validate_disabled(
defaults: &BTreeMap<&PluginInstanceId, &HostDefaultPlugin>,
explicit: &BTreeMap<&PluginInstanceId, &PluginRootInstance>,
disabled: &BTreeSet<&PluginInstanceId>,
) -> Result<(), PluginRootResolutionError> {
for id in disabled {
if let Some(default) = defaults.get(id) {
if !default.disableable {
return Err(PluginRootResolutionError::RequiredInstanceDisabled(
(*id).clone(),
));
}
} else if !explicit.contains_key(id) {
return Err(PluginRootResolutionError::UnknownDisabledInstance(
(*id).clone(),
));
}
}
Ok(())
}
fn build_candidates<'a>(
defaults: &BTreeMap<&'a PluginInstanceId, &'a HostDefaultPlugin>,
configurations: &BTreeMap<&'a PluginInstanceId, &'a Value>,
explicit: &BTreeMap<&'a PluginInstanceId, &'a PluginRootInstance>,
disabled: &BTreeSet<&PluginInstanceId>,
releases: &BTreeMap<&'a str, &'a PluginDescriptor>,
) -> Result<Vec<CandidateInstance<'a>>, PluginRootResolutionError> {
let ids = defaults
.keys()
.chain(explicit.keys())
.copied()
.collect::<BTreeSet<_>>();
let mut candidates = Vec::new();
for id in ids {
if disabled.contains(id) {
continue;
}
let descriptor = releases
.get(id.plugin_id())
.copied()
.ok_or_else(|| PluginRootResolutionError::UnknownPlugin(id.clone()))?;
let host_default = defaults.get(id).copied();
let root_instance = explicit.get(id).copied();
let source = match (host_default, root_instance) {
(Some(_), Some(_)) => PluginInstanceSource::HostDefaultConfiguredByRoot,
(Some(_), None) => PluginInstanceSource::HostDefault,
(None, Some(_)) => PluginInstanceSource::PluginRoot,
(None, None) => unreachable!("candidate IDs come from default or explicit input"),
};
candidates.push(CandidateInstance {
id: id.clone(),
descriptor,
host_configuration: host_default
.map(|default| &default.configuration)
.or_else(|| configurations.get(id).copied()),
root_configuration: root_instance.map(|instance| &instance.configuration),
source,
});
}
Ok(candidates)
}
pub(super) fn derive_root_bindings(
instances: &[PluginInstancePlan],
resolved: &[ResolvedPluginInstance],
plan_slots: &BTreeMap<String, String>,
host_bindings: &[HostBinding],
) -> Result<Vec<CapabilityBinding>, PluginRootResolutionError> {
let ids_by_plan_key = resolved
.iter()
.map(|instance| (instance.plan_key.as_str(), &instance.id))
.collect::<BTreeMap<_, _>>();
let plan_keys_by_id = resolved
.iter()
.map(|instance| (&instance.id, instance.plan_key.as_str()))
.collect::<BTreeMap<_, _>>();
let indexed = index_host_bindings(host_bindings)?;
let mut consumed = BTreeSet::new();
let mut bindings = Vec::new();
for consumer in instances {
let Some(consumer_id) = ids_by_plan_key.get(consumer.instance_key()) else {
return Err(PluginRootResolutionError::InvalidResolvedApp(format!(
"missing Plugin identity for `{}`",
consumer.instance_key()
)));
};
for requirement in consumer.required_capabilities() {
let key = (*consumer_id, requirement.capability_id());
let host_binding = indexed.get(&key).copied();
if host_binding.is_some() {
consumed.insert(key);
}
let mut candidates = instances
.iter()
.filter(|provider| {
provider.provided_capabilities().iter().any(|endpoint| {
endpoint.capability_id() == requirement.capability_id()
&& endpoint.descriptor_version() == requirement.descriptor_version()
}) && host_binding.is_none_or(|binding| {
binding.provider_slot.as_ref().is_none_or(|slot| {
plan_slots.get(provider.instance_key()) == Some(slot)
}) && binding.provider_instance.as_ref().is_none_or(|id| {
plan_keys_by_id.get(id).copied() == Some(provider.instance_key())
}) && (binding.provider_instances.is_empty()
|| binding.provider_instances.iter().any(|id| {
plan_keys_by_id.get(id).copied() == Some(provider.instance_key())
}))
})
})
.collect::<Vec<_>>();
candidates.sort_by_key(|candidate| candidate.instance_key());
if let Some(binding) = host_binding
&& !binding.provider_instances.is_empty()
&& candidates.len() != binding.provider_instances.len()
{
return Err(PluginRootResolutionError::InvalidHostBinding(format!(
"attachment for `{consumer_id}` Capability `{}` does not resolve every selected provider Instance",
requirement.capability_id()
)));
}
let selected = match requirement.cardinality() {
CapabilityCardinality::Many => candidates,
CapabilityCardinality::One if candidates.len() == 1 => candidates,
CapabilityCardinality::Optional if candidates.len() <= 1 => candidates,
CapabilityCardinality::One if candidates.is_empty() => {
return Err(PluginRootResolutionError::MissingCapability {
consumer: (*consumer_id).clone(),
capability_id: requirement.capability_id().to_owned(),
descriptor_version: requirement.descriptor_version().to_owned(),
});
}
CapabilityCardinality::Optional | CapabilityCardinality::One => {
return Err(PluginRootResolutionError::AmbiguousCapability {
consumer: (*consumer_id).clone(),
capability_id: requirement.capability_id().to_owned(),
candidates: candidates
.iter()
.filter_map(|candidate| {
ids_by_plan_key
.get(candidate.instance_key())
.copied()
.cloned()
})
.collect(),
});
}
};
for provider in selected {
let mut binding = CapabilityBinding::new(
consumer.instance_key(),
requirement.capability_id(),
requirement.descriptor_version(),
provider.instance_key(),
);
if let Some(admission) = host_binding.and_then(HostBinding::admission) {
binding = binding.with_admission(admission);
}
bindings.push(binding);
}
}
}
validate_host_bindings_consumed(host_bindings, &plan_keys_by_id, &consumed)?;
Ok(bindings)
}
type IndexedHostBindings<'a> = BTreeMap<(&'a PluginInstanceId, &'a str), &'a HostBinding>;
fn index_host_bindings(
host_bindings: &[HostBinding],
) -> Result<IndexedHostBindings<'_>, PluginRootResolutionError> {
let mut indexed = BTreeMap::new();
for binding in host_bindings {
let key = (&binding.consumer, binding.capability_id.as_str());
if indexed.insert(key, binding).is_some() {
return Err(PluginRootResolutionError::InvalidHostBinding(format!(
"duplicate attachment for `{}` Capability `{}`",
binding.consumer, binding.capability_id
)));
}
let selectors = usize::from(binding.provider_slot.is_some())
+ usize::from(binding.provider_instance.is_some())
+ usize::from(!binding.provider_instances.is_empty());
if selectors != 1 {
return Err(PluginRootResolutionError::InvalidHostBinding(format!(
"attachment for `{}` must select exactly one provider Slot, Instance, or Instance set",
binding.consumer
)));
}
if binding
.provider_instances
.iter()
.collect::<BTreeSet<_>>()
.len()
!= binding.provider_instances.len()
{
return Err(PluginRootResolutionError::InvalidHostBinding(format!(
"attachment for `{}` contains a duplicate provider Instance",
binding.consumer
)));
}
}
Ok(indexed)
}
fn validate_host_bindings_consumed(
host_bindings: &[HostBinding],
plan_keys_by_id: &BTreeMap<&PluginInstanceId, &str>,
consumed: &BTreeSet<(&PluginInstanceId, &str)>,
) -> Result<(), PluginRootResolutionError> {
let Some(binding) = host_bindings.iter().find(|binding| {
plan_keys_by_id.contains_key(&binding.consumer)
&& !consumed.contains(&(&binding.consumer, binding.capability_id.as_str()))
}) else {
return Ok(());
};
let consumer_plan_key = plan_keys_by_id
.get(&binding.consumer)
.copied()
.unwrap_or("<missing>");
Err(PluginRootResolutionError::InvalidHostBinding(format!(
"attachment for `{consumer_plan_key}` Capability `{}` matches no requirement",
binding.capability_id
)))
}
pub(super) fn map_configuration_error(
instance: &PluginInstanceId,
error: ConfigurationError,
) -> PluginRootResolutionError {
PluginRootResolutionError::InvalidConfiguration {
instance: instance.clone(),
detail: error.detail,
}
}