microde-application 0.4.0

Composition and lifecycle runtime for modular Microde applications.
Documentation
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;

use crate::{Dependency, MicrodeError, ModuleInstanceId, Reference, RelationshipKind};

#[derive(Clone)]
pub(crate) struct ResolvedRelationship {
    pub(crate) owner: ModuleInstanceId,
    pub(crate) name: String,
    pub(crate) kind: RelationshipKind,
    pub(crate) value: Arc<dyn Any + Send + Sync>,
}

#[derive(Clone)]
pub struct SetupContext {
    owner: ModuleInstanceId,
    resolutions: Arc<HashMap<u64, ResolvedRelationship>>,
}

#[derive(Clone)]
pub struct RunContext {
    owner: ModuleInstanceId,
    resolutions: Arc<HashMap<u64, ResolvedRelationship>>,
}

impl SetupContext {
    pub(crate) fn new(
        owner: ModuleInstanceId,
        resolutions: Arc<HashMap<u64, ResolvedRelationship>>,
    ) -> Self {
        Self { owner, resolutions }
    }

    pub fn use_dependency<T>(&self, relationship: &Dependency<T>) -> Result<T, MicrodeError>
    where
        T: Clone + Send + Sync + 'static,
    {
        resolve(
            &self.owner,
            &self.resolutions,
            relationship.slot_id(),
            relationship.name(),
            Some(RelationshipKind::Dependency),
        )
    }
}

pub trait RunRelationship<T> {
    fn slot_id(&self) -> u64;
    fn name(&self) -> &str;
}

impl<T> RunRelationship<T> for Dependency<T> {
    fn slot_id(&self) -> u64 {
        self.slot_id()
    }
    fn name(&self) -> &str {
        self.name()
    }
}

impl<T> RunRelationship<T> for Reference<T> {
    fn slot_id(&self) -> u64 {
        self.slot_id()
    }
    fn name(&self) -> &str {
        self.name()
    }
}

impl RunContext {
    pub(crate) fn new(
        owner: ModuleInstanceId,
        resolutions: Arc<HashMap<u64, ResolvedRelationship>>,
    ) -> Self {
        Self { owner, resolutions }
    }

    pub fn use_relationship<T, Slot>(&self, relationship: &Slot) -> Result<T, MicrodeError>
    where
        T: Clone + Send + Sync + 'static,
        Slot: RunRelationship<T>,
    {
        resolve(
            &self.owner,
            &self.resolutions,
            relationship.slot_id(),
            relationship.name(),
            None,
        )
    }
}

fn resolve<T>(
    owner: &ModuleInstanceId,
    resolutions: &HashMap<u64, ResolvedRelationship>,
    slot_id: u64,
    name: &str,
    expected_kind: Option<RelationshipKind>,
) -> Result<T, MicrodeError>
where
    T: Clone + Send + Sync + 'static,
{
    let resolved = resolutions
        .get(&slot_id)
        .filter(|value| &value.owner == owner)
        .ok_or_else(|| {
            MicrodeError::new(format!(
                "relationship '{}.{}' is not resolved for this module",
                owner.as_str(),
                name
            ))
        })?;
    if expected_kind.is_some_and(|kind| resolved.kind != kind) {
        return Err(MicrodeError::new(format!(
            "relationship '{}.{}' is not available during setup",
            owner.as_str(),
            resolved.name
        )));
    }
    resolved.value.downcast_ref::<T>().cloned().ok_or_else(|| {
        MicrodeError::new(format!(
            "provider type mismatch for relationship '{}.{}'",
            owner.as_str(),
            resolved.name
        ))
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Port;

    fn resolved(
        owner: &str,
        name: &str,
        kind: RelationshipKind,
        value: Arc<dyn Any + Send + Sync>,
    ) -> ResolvedRelationship {
        ResolvedRelationship {
            owner: ModuleInstanceId::new(owner),
            name: name.to_owned(),
            kind,
            value,
        }
    }

    #[test]
    fn reports_unresolved_wrong_phase_and_provider_type_errors() {
        let dependency = Dependency::new("database", Port::<String>::new("database"));
        let mut values = HashMap::new();
        values.insert(
            dependency.slot_id(),
            resolved(
                "other",
                "database",
                RelationshipKind::Dependency,
                Arc::new("value".to_owned()),
            ),
        );
        let context = SetupContext::new(ModuleInstanceId::new("consumer"), Arc::new(values));
        assert_eq!(
            context.use_dependency(&dependency).unwrap_err().to_string(),
            "relationship 'consumer.database' is not resolved for this module"
        );

        let mut values = HashMap::new();
        values.insert(
            dependency.slot_id(),
            resolved(
                "consumer",
                "database",
                RelationshipKind::Reference,
                Arc::new("value".to_owned()),
            ),
        );
        let context = SetupContext::new(ModuleInstanceId::new("consumer"), Arc::new(values));
        assert_eq!(
            context.use_dependency(&dependency).unwrap_err().to_string(),
            "relationship 'consumer.database' is not available during setup"
        );

        let mut values = HashMap::new();
        values.insert(
            dependency.slot_id(),
            resolved(
                "consumer",
                "database",
                RelationshipKind::Dependency,
                Arc::new(7_u8),
            ),
        );
        let context = RunContext::new(ModuleInstanceId::new("consumer"), Arc::new(values));
        assert_eq!(
            context
                .use_relationship::<String, _>(&dependency)
                .unwrap_err()
                .to_string(),
            "provider type mismatch for relationship 'consumer.database'"
        );
    }
}