lenso-app-plan 0.3.2

Immutable application plans for the Lenso vNext runtime.
Documentation
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,
    }
}