link-cli 0.2.6

A CLI tool and reusable library for links manipulation backed by a LiNo-notation doublet storage engine.
Documentation
use anyhow::Result;
use std::collections::HashSet;

use crate::error::LinkError;
use crate::link::Link;
use crate::lino_link::LinoLink;
use crate::named_type_links::NamedTypeLinks;

pub(crate) struct LinkReferenceValidator {
    trace: bool,
    auto_create_missing_references: bool,
}

#[derive(Debug, Default)]
struct LinkReferencePlan {
    numeric_ids_to_be_created: HashSet<u32>,
    names_to_be_created: HashSet<String>,
    missing_references: Vec<MissingLinkReference>,
    missing_reference_keys: HashSet<String>,
}

impl LinkReferencePlan {
    fn add_missing_reference(&mut self, reference: MissingLinkReference) {
        let key = reference.key();
        if self.missing_reference_keys.insert(key) {
            self.missing_references.push(reference);
        }
    }
}

#[derive(Debug, Clone)]
struct MissingLinkReference {
    identifier: String,
    pattern_type: &'static str,
    numeric_id: Option<u32>,
}

impl MissingLinkReference {
    fn key(&self) -> String {
        self.numeric_id
            .map(|id| format!("id:{id}"))
            .unwrap_or_else(|| format!("name:{}", self.identifier))
    }
}

impl LinkReferenceValidator {
    pub(crate) fn new(trace: bool, auto_create_missing_references: bool) -> Self {
        Self {
            trace,
            auto_create_missing_references,
        }
    }

    pub(crate) fn validate_links_exist_or_will_be_created(
        &self,
        storage: &mut impl NamedTypeLinks,
        restriction_patterns: &[LinoLink],
        substitution_patterns: &[LinoLink],
    ) -> Result<Vec<Link>> {
        self.trace_msg("[ValidateLinksExistOrWillBeCreated] Starting validation");

        let mut plan = self.build_link_reference_plan(storage, substitution_patterns);
        self.trace_msg(&format!(
            "[ValidateLinksExistOrWillBeCreated] Numeric links to be created: {:?}",
            plan.numeric_ids_to_be_created
        ));
        self.trace_msg(&format!(
            "[ValidateLinksExistOrWillBeCreated] Named links to be created: {:?}",
            plan.names_to_be_created
        ));

        self.collect_missing_references(
            storage,
            &mut plan,
            restriction_patterns,
            false,
            "restriction",
        )?;
        self.collect_missing_references(
            storage,
            &mut plan,
            substitution_patterns,
            true,
            "substitution",
        )?;

        if plan.missing_references.is_empty() {
            self.trace_msg("[ValidateLinksExistOrWillBeCreated] Validation completed");
            return Ok(vec![]);
        }

        if !self.auto_create_missing_references {
            let missing = &plan.missing_references[0];
            return Err(LinkError::QueryError(format!(
                "Invalid reference to non-existent link '{}' in {} pattern. Link '{}' does not exist and will not be created by this operation. Use --auto-create-missing-references to create missing references as point links.",
                missing.identifier, missing.pattern_type, missing.identifier
            ))
            .into());
        }

        let created = self.auto_create_missing_references(storage, &plan.missing_references)?;
        self.trace_msg("[ValidateLinksExistOrWillBeCreated] Validation completed");
        Ok(created)
    }

    fn build_link_reference_plan(
        &self,
        storage: &mut impl NamedTypeLinks,
        substitution_patterns: &[LinoLink],
    ) -> LinkReferencePlan {
        let mut plan = LinkReferencePlan::default();
        let mut reserved_numeric_ids = HashSet::new();

        for pattern in substitution_patterns {
            self.collect_explicit_definitions(pattern, &mut plan, &mut reserved_numeric_ids);
        }

        for pattern in substitution_patterns {
            self.collect_implicit_definitions(
                storage,
                pattern,
                &mut plan,
                &mut reserved_numeric_ids,
            );
        }

        plan
    }

    fn collect_explicit_definitions(
        &self,
        pattern: &LinoLink,
        plan: &mut LinkReferencePlan,
        reserved_numeric_ids: &mut HashSet<u32>,
    ) {
        if Self::is_composite_lino(pattern) {
            if let Some(identifier) = Self::concrete_identifier(pattern.id.as_deref()) {
                if let Ok(link_id) = identifier.parse::<u32>() {
                    plan.numeric_ids_to_be_created.insert(link_id);
                    reserved_numeric_ids.insert(link_id);
                } else {
                    plan.names_to_be_created.insert(identifier);
                }
            }
        }

        if let Some(values) = &pattern.values {
            for sub_pattern in values {
                self.collect_explicit_definitions(sub_pattern, plan, reserved_numeric_ids);
            }
        }
    }

    fn collect_implicit_definitions(
        &self,
        storage: &mut impl NamedTypeLinks,
        pattern: &LinoLink,
        plan: &mut LinkReferencePlan,
        reserved_numeric_ids: &mut HashSet<u32>,
    ) {
        if let Some(values) = &pattern.values {
            for sub_pattern in values {
                self.collect_implicit_definitions(storage, sub_pattern, plan, reserved_numeric_ids);
            }
        }

        if Self::is_composite_lino(pattern)
            && Self::concrete_identifier(pattern.id.as_deref()).is_none()
        {
            let next_id = Self::next_available_link_id(storage, reserved_numeric_ids);
            reserved_numeric_ids.insert(next_id);
            plan.numeric_ids_to_be_created.insert(next_id);
        }
    }

    fn next_available_link_id(
        storage: &mut impl NamedTypeLinks,
        reserved_numeric_ids: &HashSet<u32>,
    ) -> u32 {
        let mut next_id = 1;
        while storage.exists(next_id) || reserved_numeric_ids.contains(&next_id) {
            next_id += 1;
        }
        next_id
    }

    fn collect_missing_references(
        &self,
        storage: &mut impl NamedTypeLinks,
        plan: &mut LinkReferencePlan,
        patterns: &[LinoLink],
        is_substitution: bool,
        pattern_type: &'static str,
    ) -> Result<()> {
        for pattern in patterns {
            self.collect_missing_references_in_pattern(
                storage,
                plan,
                pattern,
                is_substitution,
                pattern_type,
            )?;
        }
        Ok(())
    }

    fn collect_missing_references_in_pattern(
        &self,
        storage: &mut impl NamedTypeLinks,
        plan: &mut LinkReferencePlan,
        pattern: &LinoLink,
        is_substitution: bool,
        pattern_type: &'static str,
    ) -> Result<()> {
        let pattern_id_is_definition = is_substitution
            && Self::is_composite_lino(pattern)
            && Self::concrete_identifier(pattern.id.as_deref()).is_some();

        if !pattern_id_is_definition {
            if let Some(identifier) = Self::concrete_identifier(pattern.id.as_deref()) {
                self.validate_reference_identifier(storage, plan, &identifier, pattern_type)?;
            }
        }

        if let Some(values) = &pattern.values {
            for sub_pattern in values {
                self.collect_missing_references_in_pattern(
                    storage,
                    plan,
                    sub_pattern,
                    is_substitution,
                    pattern_type,
                )?;
            }
        }
        Ok(())
    }

    fn validate_reference_identifier(
        &self,
        storage: &mut impl NamedTypeLinks,
        plan: &mut LinkReferencePlan,
        identifier: &str,
        pattern_type: &'static str,
    ) -> Result<()> {
        if let Ok(link_id) = identifier.parse::<u32>() {
            if !storage.exists(link_id) && !plan.numeric_ids_to_be_created.contains(&link_id) {
                plan.add_missing_reference(MissingLinkReference {
                    identifier: identifier.to_string(),
                    pattern_type,
                    numeric_id: Some(link_id),
                });
                return Ok(());
            }
            self.trace_msg(&format!(
                "[ValidateReferencesInPattern] Link {link_id} reference validated in {pattern_type} pattern"
            ));
            return Ok(());
        }

        if storage.get_by_name(identifier)?.is_none()
            && !plan.names_to_be_created.contains(identifier)
        {
            plan.add_missing_reference(MissingLinkReference {
                identifier: identifier.to_string(),
                pattern_type,
                numeric_id: None,
            });
            return Ok(());
        }

        self.trace_msg(&format!(
            "[ValidateReferencesInPattern] Named link '{identifier}' reference validated in {pattern_type} pattern"
        ));
        Ok(())
    }

    fn auto_create_missing_references(
        &self,
        storage: &mut impl NamedTypeLinks,
        missing_references: &[MissingLinkReference],
    ) -> Result<Vec<Link>> {
        let mut created = Vec::new();
        let mut numeric_references = missing_references
            .iter()
            .filter_map(|reference| reference.numeric_id)
            .collect::<Vec<_>>();
        numeric_references.sort_unstable();
        numeric_references.dedup();

        for link_id in numeric_references {
            if storage.exists(link_id) {
                continue;
            }

            self.trace_msg(&format!(
                "[ValidateLinksExistOrWillBeCreated] Auto-creating missing numeric reference {link_id} as point link."
            ));
            storage.try_ensure_created(link_id)?;
            storage.update(link_id, link_id, link_id)?;
            if let Some(link) = storage.get_link(link_id) {
                created.push(link);
            }
        }

        let mut named_references = missing_references
            .iter()
            .filter(|reference| reference.numeric_id.is_none())
            .map(|reference| reference.identifier.clone())
            .collect::<Vec<_>>();
        named_references.sort();
        named_references.dedup();

        for name in named_references {
            if storage.get_by_name(&name)?.is_some() {
                continue;
            }

            self.trace_msg(&format!(
                "[ValidateLinksExistOrWillBeCreated] Auto-creating missing named reference '{name}' as point link."
            ));
            let link_id = storage.get_or_create_named(&name)?;
            if let Some(link) = storage.get_link(link_id) {
                created.push(link);
            }
        }

        Ok(created)
    }

    fn is_composite_lino(lino_link: &LinoLink) -> bool {
        lino_link.values_count() == 2
    }

    fn concrete_identifier(id: Option<&str>) -> Option<String> {
        let identifier = id?.trim_end_matches(':');
        if identifier.is_empty() || identifier == "*" || identifier.starts_with('$') {
            None
        } else {
            Some(identifier.to_string())
        }
    }

    fn trace_msg(&self, msg: &str) {
        if self.trace {
            eprintln!("{}", msg);
        }
    }
}