nomograph-sysml-core 0.2.0

SysML v2 knowledge graph library -- parser, graph builder, queries, and rendering
Documentation
use std::collections::{HashMap, HashSet};

use crate::element::SysmlElement;
use crate::relationship::SysmlRelationship;

struct ImportEdge {
    target_namespace: String,
    wildcard: bool,
    segments: Vec<String>,
}

fn build_namespace_map(elements: &[SysmlElement]) -> HashMap<String, String> {
    let mut map = HashMap::new();
    for elem in elements {
        if !elem.qualified_name.contains("::")
            && (elem.kind == "package_definition" || elem.kind == "library_package")
        {
            map.insert(
                elem.qualified_name.clone(),
                elem.file_path.to_string_lossy().to_string(),
            );
        }
    }
    map
}

fn build_export_table(elements: &[SysmlElement]) -> HashMap<String, Vec<(String, String)>> {
    let mut by_file: HashMap<String, Vec<&SysmlElement>> = HashMap::new();
    for elem in elements {
        let file = elem.file_path.to_string_lossy().to_string();
        by_file.entry(file).or_default().push(elem);
    }

    let mut table = HashMap::new();
    for (file, elems) in &by_file {
        let ns = elems
            .iter()
            .find(|e| {
                !e.qualified_name.contains("::")
                    && (e.kind == "package_definition" || e.kind == "library_package")
            })
            .map(|e| e.qualified_name.as_str())
            .unwrap_or("");

        let mut exported = Vec::new();
        for elem in elems {
            let qname = &elem.qualified_name;
            let short = qname.rsplit("::").next().unwrap_or(qname);
            let is_top_level = if ns.is_empty() {
                !qname.contains("::")
            } else {
                let prefix = format!("{}::", ns);
                qname.starts_with(&prefix) && !qname[prefix.len()..].contains("::")
            };
            if is_top_level && qname.contains("::") {
                exported.push((short.to_string(), qname.clone()));
            }
        }
        table.insert(file.clone(), exported);
    }
    table
}

fn build_import_graph(relationships: &[SysmlRelationship]) -> HashMap<String, Vec<ImportEdge>> {
    let mut graph: HashMap<String, Vec<ImportEdge>> = HashMap::new();
    for rel in relationships {
        if rel.kind.eq_ignore_ascii_case("import") {
            if let Some(edge) = parse_import_target(&rel.target) {
                let file = rel.file_path.to_string_lossy().to_string();
                graph.entry(file).or_default().push(edge);
            }
        }
    }
    graph
}

fn parse_import_target(target: &str) -> Option<ImportEdge> {
    if target.is_empty() {
        return None;
    }
    let wildcard = target.ends_with("::*");
    let path = if wildcard {
        &target[..target.len() - 3]
    } else {
        target
    };
    let segments: Vec<String> = path.split("::").map(|s| s.to_string()).collect();
    if segments.is_empty() {
        return None;
    }
    Some(ImportEdge {
        target_namespace: segments[0].clone(),
        wildcard,
        segments,
    })
}

fn resolve_file_imports(
    file: &str,
    import_graph: &HashMap<String, Vec<ImportEdge>>,
    namespace_map: &HashMap<String, String>,
    exports: &HashMap<String, Vec<(String, String)>>,
    visible: &mut HashMap<String, String>,
    visited: &mut HashSet<String>,
    elements: &[SysmlElement],
) {
    if visited.contains(file) {
        return;
    }
    visited.insert(file.to_string());

    let edges = match import_graph.get(file) {
        Some(e) => e,
        None => return,
    };

    for edge in edges {
        let target_file = match namespace_map.get(&edge.target_namespace) {
            Some(f) => f.clone(),
            None => continue,
        };

        if edge.segments.len() == 1 && edge.wildcard {
            resolve_file_imports(
                &target_file,
                import_graph,
                namespace_map,
                exports,
                &mut HashMap::new(),
                visited,
                elements,
            );

            if let Some(target_exports) = exports.get(&target_file) {
                for (name, qname) in target_exports {
                    visible.insert(name.clone(), qname.clone());
                }
            }

            let mut transitive = HashMap::new();
            let mut tv = visited.clone();
            resolve_file_imports(
                &target_file,
                import_graph,
                namespace_map,
                exports,
                &mut transitive,
                &mut tv,
                elements,
            );
            for (name, qname) in transitive {
                visible.entry(name).or_insert(qname);
            }
        } else if edge.segments.len() == 1 {
            continue;
        } else {
            let remainder = &edge.segments[1..];
            resolve_multi_segment(
                &target_file,
                remainder,
                edge.wildcard,
                exports,
                elements,
                visible,
            );
        }
    }
}

fn resolve_multi_segment(
    file: &str,
    segments: &[String],
    wildcard: bool,
    exports: &HashMap<String, Vec<(String, String)>>,
    elements: &[SysmlElement],
    visible: &mut HashMap<String, String>,
) {
    let file_elements: Vec<&SysmlElement> = elements
        .iter()
        .filter(|e| e.file_path.to_string_lossy() == file)
        .collect();

    if segments.len() == 1 && !wildcard {
        let name = &segments[0];
        if let Some(file_exports) = exports.get(file) {
            for (ename, qname) in file_exports {
                if ename == name {
                    visible.insert(name.clone(), qname.clone());
                    return;
                }
            }
        }
    } else if segments.len() == 1 && wildcard {
        let container_name = &segments[0];
        for elem in &file_elements {
            let short = elem
                .qualified_name
                .rsplit("::")
                .next()
                .unwrap_or(&elem.qualified_name);
            if short == container_name {
                for member_qname in &elem.members {
                    let member_name = member_qname.rsplit("::").next().unwrap_or(member_qname);
                    visible.insert(member_name.to_string(), member_qname.clone());
                }
                return;
            }
        }
    } else if segments.len() >= 2 {
        let container_name = &segments[0];
        let sub_segments = &segments[1..];

        for elem in &file_elements {
            let short = elem
                .qualified_name
                .rsplit("::")
                .next()
                .unwrap_or(&elem.qualified_name);
            if short == container_name && sub_segments.len() == 1 && !wildcard {
                let target_name = &sub_segments[0];
                for member_qname in &elem.members {
                    let member_name = member_qname.rsplit("::").next().unwrap_or(member_qname);
                    if member_name == target_name {
                        visible.insert(target_name.clone(), member_qname.clone());
                        return;
                    }
                }
            }
        }
    }
}

fn qualify_reference(reference: &str, visible: &HashMap<String, String>) -> String {
    if reference.contains("::") {
        return reference.to_string();
    }
    visible
        .get(reference)
        .cloned()
        .unwrap_or_else(|| reference.to_string())
}

pub fn resolve_imports(elements: &[SysmlElement], relationships: &mut [SysmlRelationship]) {
    let namespace_map = build_namespace_map(elements);
    let exports = build_export_table(elements);
    let import_graph = build_import_graph(relationships);

    let mut files: HashSet<String> = HashSet::new();
    for elem in elements {
        files.insert(elem.file_path.to_string_lossy().to_string());
    }

    let mut resolved: HashMap<String, HashMap<String, String>> = HashMap::new();
    for file in &files {
        let mut visible = HashMap::new();
        let mut visited = HashSet::new();
        resolve_file_imports(
            file,
            &import_graph,
            &namespace_map,
            &exports,
            &mut visible,
            &mut visited,
            elements,
        );
        if !visible.is_empty() {
            resolved.insert(file.clone(), visible);
        }
    }

    for rel in relationships.iter_mut() {
        let file = rel.file_path.to_string_lossy().to_string();
        if let Some(visible) = resolved.get(&file) {
            rel.source = qualify_reference(&rel.source, visible);
            rel.target = qualify_reference(&rel.target, visible);
        }
    }
}