kmp-domain 0.1.1

Domain model of the Kernel Memory Protocol: aggregates, value objects, repositories and projections, with no IO
Documentation
use std::collections::BTreeSet;

use crate::MemoryDimensionIdentity;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DimensionSelectionMode {
    All,
    Only,
    Except,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DimensionScopeMode {
    CurrentAbout,
    Abouts,
    AllAbouts,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DimensionSelection {
    mode: DimensionSelectionMode,
    dimensions: BTreeSet<String>,
    scope_mode: DimensionScopeMode,
    abouts: BTreeSet<String>,
    scope_ids: BTreeSet<String>,
}

impl DimensionSelection {
    pub fn all() -> Self {
        Self {
            mode: DimensionSelectionMode::All,
            dimensions: BTreeSet::new(),
            scope_mode: DimensionScopeMode::CurrentAbout,
            abouts: BTreeSet::new(),
            scope_ids: BTreeSet::new(),
        }
    }

    pub fn only(values: impl IntoIterator<Item = impl Into<String>>) -> Self {
        Self {
            mode: DimensionSelectionMode::Only,
            dimensions: normalize_dimensions(values),
            scope_mode: DimensionScopeMode::CurrentAbout,
            abouts: BTreeSet::new(),
            scope_ids: BTreeSet::new(),
        }
    }

    pub fn except(values: impl IntoIterator<Item = impl Into<String>>) -> Self {
        Self {
            mode: DimensionSelectionMode::Except,
            dimensions: normalize_dimensions(values),
            scope_mode: DimensionScopeMode::CurrentAbout,
            abouts: BTreeSet::new(),
            scope_ids: BTreeSet::new(),
        }
    }

    pub fn mode(&self) -> DimensionSelectionMode {
        self.mode
    }

    pub fn dimensions(&self) -> &BTreeSet<String> {
        &self.dimensions
    }

    pub fn scope_mode(&self) -> DimensionScopeMode {
        self.scope_mode
    }

    pub fn abouts(&self) -> &BTreeSet<String> {
        &self.abouts
    }

    pub fn scope_ids(&self) -> &BTreeSet<String> {
        &self.scope_ids
    }

    pub fn with_scope_ids(
        mut self,
        scope_ids: impl IntoIterator<Item = impl Into<String>>,
    ) -> Self {
        self.scope_ids = normalize_dimensions(scope_ids);
        self
    }

    pub fn with_current_about_scope(mut self) -> Self {
        self.scope_mode = DimensionScopeMode::CurrentAbout;
        self.abouts.clear();
        self
    }

    pub fn with_about_scope(mut self, abouts: impl IntoIterator<Item = impl Into<String>>) -> Self {
        self.scope_mode = DimensionScopeMode::Abouts;
        self.abouts = normalize_dimensions(abouts);
        self
    }

    pub fn with_all_about_scope(mut self) -> Self {
        self.scope_mode = DimensionScopeMode::AllAbouts;
        self.abouts.clear();
        self
    }

    pub fn resolve_current_about(&self, current_about: &str) -> Self {
        if self.scope_mode != DimensionScopeMode::CurrentAbout {
            return self.clone();
        }
        self.clone().with_about_scope([current_about.to_string()])
    }

    pub fn includes(&self, dimension: &str) -> bool {
        self.includes_dimension(dimension)
    }

    pub fn includes_dimension(&self, dimension: &str) -> bool {
        match self.mode {
            DimensionSelectionMode::All => true,
            DimensionSelectionMode::Only => self.dimensions.contains(dimension),
            DimensionSelectionMode::Except => !self.dimensions.contains(dimension),
        }
    }

    pub fn includes_coordinate(&self, dimension: &str, scope_id: &str) -> bool {
        self.includes_dimension(dimension)
            && self.includes_scope(scope_id)
            && self.includes_dimension_scope(scope_id)
    }

    pub fn includes_scope(&self, scope_id: &str) -> bool {
        match self.scope_mode {
            DimensionScopeMode::AllAbouts => true,
            DimensionScopeMode::CurrentAbout => true,
            DimensionScopeMode::Abouts => MemoryDimensionIdentity::parse(scope_id)
                .map(|identity| self.abouts.contains(identity.about()))
                .unwrap_or(false),
        }
    }

    pub fn includes_dimension_scope(&self, scope_id: &str) -> bool {
        if self.scope_ids.is_empty() {
            return true;
        }
        let scope_id = scope_id.trim();
        self.scope_ids.contains(scope_id)
            || MemoryDimensionIdentity::parse(scope_id)
                .map(|identity| self.scope_ids.contains(identity.dimension_id()))
                .unwrap_or(false)
    }
}

impl Default for DimensionSelection {
    fn default() -> Self {
        Self::all()
    }
}

fn normalize_dimensions(values: impl IntoIterator<Item = impl Into<String>>) -> BTreeSet<String> {
    values
        .into_iter()
        .map(Into::into)
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
        .collect()
}

#[cfg(test)]
mod tests {
    use super::DimensionSelection;

    #[test]
    fn selection_filters_dimensions() {
        let only = DimensionSelection::only(["conversation", "entity", " "]);
        assert!(only.includes("conversation"));
        assert!(!only.includes("benchmark_record"));

        let except = DimensionSelection::except(["entity"]);
        assert!(except.includes("conversation"));
        assert!(!except.includes("entity"));
    }

    #[test]
    fn selection_filters_about_scope_when_resolved() {
        let current = DimensionSelection::only(["timeline"]).resolve_current_about("question:a");
        assert!(current.includes_coordinate("timeline", "about:question:a:dimension:timeline"));
        assert!(!current.includes_coordinate("timeline", "about:question:b:dimension:timeline"));

        let all = DimensionSelection::only(["timeline"]).with_all_about_scope();
        assert!(all.includes_coordinate("timeline", "about:question:b:dimension:timeline"));
    }

    #[test]
    fn selection_filters_exact_dimension_scope_ids() {
        let local = DimensionSelection::only(["conversation"])
            .resolve_current_about("question:a")
            .with_scope_ids(["conversation:alpha"]);
        assert!(local.includes_coordinate(
            "conversation",
            "about:question:a:dimension:conversation:alpha"
        ));
        assert!(!local.includes_coordinate(
            "conversation",
            "about:question:a:dimension:conversation:beta"
        ));
        assert!(
            !local.includes_coordinate("topic", "about:question:a:dimension:conversation:alpha")
        );

        let namespaced = DimensionSelection::all()
            .with_scope_ids(["about:question:a:dimension:conversation:alpha"]);
        assert!(namespaced.includes_coordinate(
            "conversation",
            "about:question:a:dimension:conversation:alpha"
        ));
        assert!(!namespaced.includes_coordinate(
            "conversation",
            "about:question:b:dimension:conversation:alpha"
        ));
    }
}