|
| 1 | +//! Containment hierarchy representation and generation |
| 2 | +//! |
| 3 | +//! This module provides data structures and functions for building and |
| 4 | +//! representing the physical containment hierarchy (folders, files, elements) |
| 5 | +//! of a requirements model. |
| 6 | +
|
| 7 | +use crate::element::{Element, ElementType}; |
| 8 | +use crate::error::ReqvireError; |
| 9 | +use crate::graph_registry::GraphRegistry; |
| 10 | +use serde::Serialize; |
| 11 | +use std::collections::BTreeMap; |
| 12 | +use std::path::Path; |
| 13 | + |
| 14 | +/// Represents a single element in the containment hierarchy |
| 15 | +#[derive(Debug, Clone, Serialize)] |
| 16 | +pub struct ContainmentElement { |
| 17 | + pub id: String, |
| 18 | + pub name: String, |
| 19 | + pub element_type: ElementType, |
| 20 | + pub file_path: String, |
| 21 | + pub identifier: String, |
| 22 | +} |
| 23 | + |
| 24 | +impl ContainmentElement { |
| 25 | + pub fn from_element(element: &Element) -> Self { |
| 26 | + ContainmentElement { |
| 27 | + id: element.id.clone(), |
| 28 | + name: element.name.clone(), |
| 29 | + element_type: element.element_type.clone(), |
| 30 | + file_path: element.file_path.clone(), |
| 31 | + identifier: element.identifier.clone(), |
| 32 | + } |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +/// Represents a file containing elements |
| 37 | +#[derive(Debug, Clone, Serialize)] |
| 38 | +pub struct ContainmentFile { |
| 39 | + pub path: String, |
| 40 | + pub name: String, |
| 41 | + pub elements: Vec<ContainmentElement>, |
| 42 | +} |
| 43 | + |
| 44 | +/// Represents a folder containing files and subfolders |
| 45 | +#[derive(Debug, Clone, Serialize)] |
| 46 | +pub struct ContainmentFolder { |
| 47 | + pub name: String, |
| 48 | + pub path: Vec<String>, |
| 49 | + pub files: Vec<ContainmentFile>, |
| 50 | + pub subfolders: Vec<ContainmentFolder>, |
| 51 | +} |
| 52 | + |
| 53 | +/// Root containment hierarchy structure |
| 54 | +#[derive(Debug, Clone, Serialize)] |
| 55 | +pub struct ContainmentHierarchy { |
| 56 | + pub root_folder: ContainmentFolder, |
| 57 | +} |
| 58 | + |
| 59 | +impl ContainmentHierarchy { |
| 60 | + /// Build containment hierarchy from a registry |
| 61 | + /// |
| 62 | + /// When `short` is true, shows only root elements (those without hierarchical parents). |
| 63 | + /// When `short` is false (default), shows all elements. |
| 64 | + pub fn build(registry: &GraphRegistry, short: bool) -> Result<Self, ReqvireError> { |
| 65 | + // Group elements by file |
| 66 | + let mut files_map: BTreeMap<String, Vec<&Element>> = BTreeMap::new(); |
| 67 | + for element in registry.get_all_elements() { |
| 68 | + files_map.entry(element.file_path.clone()) |
| 69 | + .or_insert_with(Vec::new) |
| 70 | + .push(element); |
| 71 | + } |
| 72 | + |
| 73 | + // Build elements map - filter if short mode, otherwise show all |
| 74 | + let mut elements_map: BTreeMap<String, Vec<ContainmentElement>> = BTreeMap::new(); |
| 75 | + for (file_path, elements) in files_map.iter() { |
| 76 | + let selected_elements: Vec<ContainmentElement> = if short { |
| 77 | + // Short mode: only top-level elements |
| 78 | + filter_top_level_elements(elements) |
| 79 | + .iter() |
| 80 | + .map(|e| ContainmentElement::from_element(e)) |
| 81 | + .collect() |
| 82 | + } else { |
| 83 | + // Full mode: all elements |
| 84 | + elements.iter() |
| 85 | + .map(|e| ContainmentElement::from_element(e)) |
| 86 | + .collect() |
| 87 | + }; |
| 88 | + elements_map.insert(file_path.clone(), selected_elements); |
| 89 | + } |
| 90 | + |
| 91 | + // Build folder structure |
| 92 | + let root_folder = build_folder_structure(&elements_map); |
| 93 | + |
| 94 | + Ok(ContainmentHierarchy { root_folder }) |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +/// Filter elements to show only top-level parents (those without hierarchical parents in same file) |
| 99 | +fn filter_top_level_elements<'a>(elements: &[&'a Element]) -> Vec<&'a Element> { |
| 100 | + use std::collections::HashSet; |
| 101 | + |
| 102 | + // Get hierarchical relation types (derivedFrom) |
| 103 | + let hierarchical_types = crate::relation::get_hierarchical_relation_types(); |
| 104 | + |
| 105 | + // Collect all element IDs (fragments) in this file |
| 106 | + let element_ids: HashSet<String> = elements.iter() |
| 107 | + .map(|e| e.id.clone()) |
| 108 | + .collect(); |
| 109 | + |
| 110 | + // Find elements that have derivedFrom relations pointing to elements in the same file |
| 111 | + let mut child_elements: HashSet<String> = HashSet::new(); |
| 112 | + for element in elements { |
| 113 | + for relation in &element.relations { |
| 114 | + if hierarchical_types.contains(&relation.relation_type.name) { |
| 115 | + // Check if the target element_id is in the same file |
| 116 | + if let Some(target_id) = &relation.target.element_id { |
| 117 | + if element_ids.contains(target_id) { |
| 118 | + // This element has a parent in the same file, so it's a child |
| 119 | + child_elements.insert(element.identifier.clone()); |
| 120 | + } |
| 121 | + } |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + // Return only elements that are NOT children (i.e., top-level) |
| 127 | + elements.iter() |
| 128 | + .filter(|e| !child_elements.contains(&e.identifier)) |
| 129 | + .copied() |
| 130 | + .collect() |
| 131 | +} |
| 132 | + |
| 133 | +/// Build folder structure from files map |
| 134 | +fn build_folder_structure(files_map: &BTreeMap<String, Vec<ContainmentElement>>) -> ContainmentFolder { |
| 135 | + // Build intermediate structure: folder_path -> files in that folder |
| 136 | + let mut folder_files: BTreeMap<Vec<String>, Vec<ContainmentFile>> = BTreeMap::new(); |
| 137 | + |
| 138 | + // Track all folder paths (including intermediate folders without direct files) |
| 139 | + let mut all_folder_paths: std::collections::HashSet<Vec<String>> = std::collections::HashSet::new(); |
| 140 | + |
| 141 | + for (file_path, elements) in files_map { |
| 142 | + let path = Path::new(file_path); |
| 143 | + let folder_path: Vec<String> = path.parent() |
| 144 | + .map(|p| p.components() |
| 145 | + .filter_map(|c| c.as_os_str().to_str()) |
| 146 | + .map(String::from) |
| 147 | + .collect()) |
| 148 | + .unwrap_or_default(); |
| 149 | + |
| 150 | + // Add all intermediate folder paths (e.g., for "a/b/c", add "", "a", "a/b", "a/b/c") |
| 151 | + for i in 0..=folder_path.len() { |
| 152 | + all_folder_paths.insert(folder_path[..i].to_vec()); |
| 153 | + } |
| 154 | + |
| 155 | + let file_name = path.file_name() |
| 156 | + .and_then(|n| n.to_str()) |
| 157 | + .unwrap_or("") |
| 158 | + .to_string(); |
| 159 | + |
| 160 | + let file = ContainmentFile { |
| 161 | + path: file_path.clone(), |
| 162 | + name: file_name, |
| 163 | + elements: elements.clone(), |
| 164 | + }; |
| 165 | + |
| 166 | + folder_files.entry(folder_path) |
| 167 | + .or_insert_with(Vec::new) |
| 168 | + .push(file); |
| 169 | + } |
| 170 | + |
| 171 | + // Build hierarchical folder structure using all folder paths |
| 172 | + build_folder_recursive(&[], &folder_files, &all_folder_paths) |
| 173 | +} |
| 174 | + |
| 175 | +/// Recursively build folder structure |
| 176 | +fn build_folder_recursive( |
| 177 | + current_path: &[String], |
| 178 | + folder_files: &BTreeMap<Vec<String>, Vec<ContainmentFile>>, |
| 179 | + all_folder_paths: &std::collections::HashSet<Vec<String>> |
| 180 | +) -> ContainmentFolder { |
| 181 | + let folder_name = current_path.last() |
| 182 | + .map(|s| s.clone()) |
| 183 | + .unwrap_or_else(|| "Reqvire root".to_string()); |
| 184 | + |
| 185 | + // Get files directly in this folder |
| 186 | + let files = folder_files.get(current_path) |
| 187 | + .cloned() |
| 188 | + .unwrap_or_default(); |
| 189 | + |
| 190 | + // Find all immediate subfolders (using all_folder_paths to include intermediate folders) |
| 191 | + let mut subfolders = Vec::new(); |
| 192 | + let current_depth = current_path.len(); |
| 193 | + |
| 194 | + let mut seen_subfolders: std::collections::HashSet<String> = std::collections::HashSet::new(); |
| 195 | + |
| 196 | + for folder_path in all_folder_paths.iter() { |
| 197 | + if folder_path.len() == current_depth + 1 { |
| 198 | + // Check if this is an immediate child |
| 199 | + let is_child = current_path.iter() |
| 200 | + .zip(folder_path.iter()) |
| 201 | + .all(|(a, b)| a == b); |
| 202 | + |
| 203 | + if is_child { |
| 204 | + if let Some(subfolder_name) = folder_path.last() { |
| 205 | + if seen_subfolders.insert(subfolder_name.clone()) { |
| 206 | + let subfolder = build_folder_recursive(folder_path, folder_files, all_folder_paths); |
| 207 | + subfolders.push(subfolder); |
| 208 | + } |
| 209 | + } |
| 210 | + } |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + // Sort subfolders for deterministic output |
| 215 | + subfolders.sort_by(|a, b| a.name.cmp(&b.name)); |
| 216 | + |
| 217 | + ContainmentFolder { |
| 218 | + name: folder_name, |
| 219 | + path: current_path.to_vec(), |
| 220 | + files, |
| 221 | + subfolders, |
| 222 | + } |
| 223 | +} |
0 commit comments