Skip to content

Commit 143372e

Browse files
author
Test User
committed
Code improvements.
1 parent 59d104f commit 143372e

File tree

97 files changed

+15850
-9619
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+15850
-9619
lines changed

core/src/crud.rs

Lines changed: 91 additions & 464 deletions
Large diffs are not rendered by default.

core/src/graph_registry.rs

Lines changed: 309 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2228,6 +2228,239 @@ impl GraphRegistry {
22282228
Ok(relations)
22292229
}
22302230

2231+
/// Adds a relation to an element with full validation and target resolution
2232+
/// This is the comprehensive method used by CRUD operations
2233+
///
2234+
/// # Arguments
2235+
/// * `source_id` - Source element identifier
2236+
/// * `target` - Target (element name, URL, or file path)
2237+
/// * `relation_type` - Relation type name
2238+
/// * `git_root` - Git root path for file resolution
2239+
///
2240+
/// # Returns
2241+
/// Modified file path
2242+
pub fn add_element_relation_full(
2243+
&mut self,
2244+
source_id: &str,
2245+
target: &str,
2246+
relation_type: &str,
2247+
git_root: &std::path::Path,
2248+
) -> Result<String, ReqvireError> {
2249+
use crate::relation::{RELATION_TYPES, Relation, RelationTarget, LinkType};
2250+
use std::path::PathBuf;
2251+
2252+
// Validate source element exists
2253+
if !self.nodes.contains_key(source_id) {
2254+
return Err(ReqvireError::ElementNotFound(
2255+
format!("Source element '{}' not found", source_id)
2256+
));
2257+
}
2258+
2259+
// Validate relation type
2260+
if !RELATION_TYPES.contains_key(relation_type) {
2261+
return Err(ReqvireError::UnsupportedRelationType(
2262+
format!("Invalid relation type '{}'. Valid types: {}",
2263+
relation_type, crate::relation::supported_relation_types_list())
2264+
));
2265+
}
2266+
2267+
// Get source element info
2268+
let source_node = self.nodes.get(source_id).unwrap();
2269+
let source_name = source_node.element.name.clone();
2270+
let source_file_path = source_node.element.file_path.clone();
2271+
let source_type = source_node.element.element_type.clone();
2272+
2273+
// Determine target type: element name, external URL, or internal path
2274+
let is_external_url = crate::utils::is_external_url(target);
2275+
let is_internal_path = !is_external_url && (
2276+
target.ends_with(".md") ||
2277+
target.contains('/') ||
2278+
git_root.join(target).exists()
2279+
);
2280+
2281+
// Resolve target and create relation components
2282+
let (target_display_name, relation_target_link, target_id_for_check, element_id_opt) =
2283+
if is_external_url {
2284+
// External URL - use as-is
2285+
(target.to_string(), LinkType::ExternalUrl(target.to_string()), target.to_string(), None)
2286+
} else if is_internal_path {
2287+
// Internal file path
2288+
let source_folder = crate::utils::get_parent_dir(&source_file_path);
2289+
2290+
// Calculate relative path from source file to target
2291+
let target_path = PathBuf::from(target);
2292+
let relative_path = pathdiff::diff_paths(&target_path, &source_folder)
2293+
.unwrap_or_else(|| target_path.clone());
2294+
2295+
// Extract filename for display name
2296+
let display = target_path.file_name()
2297+
.map(|n| n.to_string_lossy().to_string())
2298+
.unwrap_or_else(|| target.to_string());
2299+
2300+
(display, LinkType::InternalPath(relative_path), target.to_string(), None)
2301+
} else {
2302+
// Element name - resolve to get identifier
2303+
let target_element = self.get_element_by_name(target)
2304+
.ok_or_else(|| ReqvireError::ElementNotFound(
2305+
format!("Target element '{}' not found", target)
2306+
))?;
2307+
2308+
let target_id = target_element.identifier.clone();
2309+
let target_display_name = target_element.name.clone();
2310+
let target_file_path = target_element.file_path.clone();
2311+
let target_type = target_element.element_type.clone();
2312+
2313+
// Validate type compatibility for element-to-element relations
2314+
// TODO: Add relation-type-specific validation here
2315+
// For now, just check that we're not linking incompatible types
2316+
if !source_type.is_merge_compatible(&target_type) {
2317+
// Note: This is a simplified check - we may want more nuanced validation
2318+
// based on the specific relation type
2319+
log::warn!(
2320+
"Adding relation '{}' between potentially incompatible types: {} ({}) -> {} ({})",
2321+
relation_type, source_name, source_type.as_str(),
2322+
target_display_name, target_type.as_str()
2323+
);
2324+
}
2325+
2326+
// Calculate relative identifier from source element's file to target element
2327+
let source_folder = crate::utils::get_parent_dir(&source_file_path);
2328+
2329+
let relation_target = if source_file_path == target_file_path {
2330+
// Same file - use just the fragment
2331+
let (_path, fragment_opt) = crate::utils::extract_path_and_fragment(&target_id);
2332+
let fragment = fragment_opt.unwrap_or(&target_id);
2333+
LinkType::Identifier(format!("#{}", fragment))
2334+
} else {
2335+
// Different files - calculate relative path
2336+
let relative_id = crate::utils::to_relative_identifier(
2337+
&target_id,
2338+
&source_folder,
2339+
true
2340+
).unwrap_or_else(|_| target_id.clone());
2341+
LinkType::Identifier(relative_id)
2342+
};
2343+
2344+
// Extract element ID (fragment) for change tracking
2345+
let (_path, fragment_opt) = crate::utils::extract_path_and_fragment(&target_id);
2346+
let element_id = fragment_opt.map(|s| s.to_string());
2347+
2348+
(target_display_name, relation_target, target_id, element_id)
2349+
};
2350+
2351+
// Get source node again (mutable this time)
2352+
let source_node = self.nodes.get(source_id).unwrap();
2353+
2354+
// Validate: Check if relation already exists (idempotent)
2355+
let relation_exists = source_node.element.relations.iter().any(|r| {
2356+
r.user_created &&
2357+
r.relation_type.name == relation_type &&
2358+
r.target.link.as_str() == target_id_for_check
2359+
});
2360+
2361+
if relation_exists {
2362+
return Err(ReqvireError::RelationError(
2363+
format!("Relation '{}' from '{}' to '{}' already exists",
2364+
relation_type, source_name, target)
2365+
));
2366+
}
2367+
2368+
// Validate: Check for cross-section duplicate (target in Attachments)
2369+
let in_attachments = source_node.element.attachments.iter().any(|a| {
2370+
a.target.as_str() == target_id_for_check
2371+
});
2372+
2373+
if in_attachments {
2374+
return Err(ReqvireError::CrossSectionDuplicate(
2375+
format!("Target '{}' already exists in Attachments of '{}'. Cannot add to Relations.",
2376+
target, source_name)
2377+
));
2378+
}
2379+
2380+
// Create the relation
2381+
let relation_type_info = RELATION_TYPES.get(relation_type).unwrap();
2382+
let relation = Relation {
2383+
relation_type: relation_type_info,
2384+
target: RelationTarget {
2385+
text: target_display_name,
2386+
link: relation_target_link,
2387+
element_id: element_id_opt,
2388+
},
2389+
user_created: true,
2390+
};
2391+
2392+
// Add relation to source element
2393+
let source_node = self.nodes.get_mut(source_id).unwrap();
2394+
source_node.element.relations.push(relation);
2395+
2396+
// Mark file as modified
2397+
let file_path = source_node.element.file_path.clone();
2398+
self.modified_files.insert(file_path.clone());
2399+
2400+
Ok(file_path)
2401+
}
2402+
2403+
/// Removes a relation from an element with full target resolution
2404+
/// This is the comprehensive method used by CRUD operations
2405+
///
2406+
/// # Arguments
2407+
/// * `source_id` - Source element identifier
2408+
/// * `target` - Target (element name, URL, or file path)
2409+
///
2410+
/// # Returns
2411+
/// Tuple of (modified file path, relation type, target display name) or None if no relation found
2412+
pub fn remove_element_relation_full(
2413+
&mut self,
2414+
source_id: &str,
2415+
target: &str,
2416+
) -> Result<Option<(String, String, String)>, ReqvireError> {
2417+
// Validate source element exists
2418+
if !self.nodes.contains_key(source_id) {
2419+
return Err(ReqvireError::ElementNotFound(
2420+
format!("Source element '{}' not found", source_id)
2421+
));
2422+
}
2423+
2424+
let source_node = self.nodes.get(source_id).unwrap();
2425+
let source_file_path = source_node.element.file_path.clone();
2426+
2427+
// Try to resolve target as element name first
2428+
let target_id_to_find = if let Some(target_element) = self.get_element_by_name(target) {
2429+
target_element.identifier.clone()
2430+
} else {
2431+
// Not an element name - could be URL or path
2432+
target.to_string()
2433+
};
2434+
2435+
// Find matching relation
2436+
let relation_match = source_node.element.relations.iter()
2437+
.find(|r| {
2438+
r.user_created && r.target.link.as_str() == target_id_to_find
2439+
})
2440+
.cloned(); // Clone to avoid borrow issues
2441+
2442+
if let Some(relation) = relation_match {
2443+
let relation_type = relation.relation_type.name.to_string();
2444+
let target_display_name = relation.target.text.clone();
2445+
2446+
// Remove the relation
2447+
let source_node = self.nodes.get_mut(source_id).unwrap();
2448+
source_node.element.relations.retain(|r| {
2449+
!(r.user_created &&
2450+
r.relation_type.name == relation_type &&
2451+
r.target.link.as_str() == target_id_to_find)
2452+
});
2453+
2454+
// Mark file as modified
2455+
self.modified_files.insert(source_file_path.clone());
2456+
2457+
Ok(Some((source_file_path, relation_type, target_display_name)))
2458+
} else {
2459+
// No relation found - could be an attachment (handled by crud layer)
2460+
Ok(None)
2461+
}
2462+
}
2463+
22312464
/// Gets statistics about the graph
22322465
pub fn get_graph_stats(&self) -> (usize, usize) {
22332466
let element_count = self.nodes.len();
@@ -2321,6 +2554,41 @@ impl GraphRegistry {
23212554
Ok(new_element)
23222555
}
23232556

2557+
/// Check if removing an element would orphan any children
2558+
///
2559+
/// Returns a sorted list of child element names that would be orphaned
2560+
fn check_for_orphaned_children(&self, element_id: &str) -> Result<Vec<String>, ReqvireError> {
2561+
let mut orphaned_children: Vec<String> = Vec::new();
2562+
let hierarchical_types = crate::relation::get_hierarchical_relation_types();
2563+
2564+
for (_child_id, child_node) in &self.nodes {
2565+
// Count how many hierarchical parent relations this child has to the element being deleted
2566+
let mut parents_to_target = 0;
2567+
let mut total_parents = 0;
2568+
2569+
for rel in &child_node.element.relations {
2570+
let target_id = match &rel.target.link {
2571+
crate::relation::LinkType::Identifier(id) => id.as_str(),
2572+
_ => continue, // Skip external links
2573+
};
2574+
if hierarchical_types.contains(&rel.relation_type.name) {
2575+
total_parents += 1;
2576+
if target_id == element_id {
2577+
parents_to_target += 1;
2578+
}
2579+
}
2580+
}
2581+
2582+
// If child only has hierarchical parent relations to the target element, it will be orphaned
2583+
if parents_to_target > 0 && total_parents == parents_to_target {
2584+
orphaned_children.push(child_node.element.name.clone());
2585+
}
2586+
}
2587+
2588+
orphaned_children.sort();
2589+
Ok(orphaned_children)
2590+
}
2591+
23242592
/// Enhanced remove element that tracks modifications and performs cleanup
23252593
pub fn remove_element_with_cleanup(&mut self, element_id: &str) -> Result<Vec<String>, ReqvireError> {
23262594
if !self.nodes.contains_key(element_id) {
@@ -2331,8 +2599,25 @@ impl GraphRegistry {
23312599

23322600
// Get element info before removal
23332601
let element = &self.nodes.get(element_id).unwrap().element;
2602+
let element_name = element.name.clone();
23342603
let file_path = element.file_path.clone();
23352604

2605+
// Validate: Check for orphaned children before removal
2606+
let orphaned_children = self.check_for_orphaned_children(element_id)?;
2607+
if !orphaned_children.is_empty() {
2608+
return Err(ReqvireError::InvalidOperation(
2609+
format!(
2610+
"Cannot delete '{}' because it has {} child element(s) with parent hierarchical relations that would become orphaned: {}.\n\n\
2611+
To proceed, either:\n\
2612+
1. Delete the child elements first, or\n\
2613+
2. Update the child elements to link to a different parent element",
2614+
element_name,
2615+
orphaned_children.len(),
2616+
orphaned_children.join(", ")
2617+
)
2618+
));
2619+
}
2620+
23362621
// Track all files that will be modified
23372622
let mut modified_files = vec![file_path.clone()];
23382623

@@ -2485,6 +2770,11 @@ impl GraphRegistry {
24852770
));
24862771
}
24872772

2773+
// Get target element data first (needed for validation)
2774+
let target_node = self.nodes.get(target_id).unwrap();
2775+
let target_name = target_node.element.name.clone();
2776+
let target_type = target_node.element.element_type.clone();
2777+
24882778
// Validate all sources exist and collect their data
24892779
let mut source_data: Vec<(String, String, String, Vec<crate::relation::Relation>, Vec<crate::element::Attachment>)> = Vec::new();
24902780
for source_id in source_ids {
@@ -2494,6 +2784,24 @@ impl GraphRegistry {
24942784
))?;
24952785

24962786
let source_element = &source_node.element;
2787+
2788+
// Validate: Check if source would merge into itself
2789+
if source_id == target_id {
2790+
return Err(ReqvireError::InvalidOperation(
2791+
"Cannot merge element into itself".to_string()
2792+
));
2793+
}
2794+
2795+
// Validate: Check type compatibility
2796+
if !target_type.is_merge_compatible(&source_element.element_type) {
2797+
return Err(ReqvireError::MergeTypeMismatch(format!(
2798+
"Cannot merge '{}' ({}) into '{}' ({}): type mismatch. \
2799+
Elements must be in the same category (requirement/verification/refinement/other).",
2800+
source_element.name, source_element.element_type.as_str(),
2801+
target_name, target_type.as_str()
2802+
)));
2803+
}
2804+
24972805
source_data.push((
24982806
source_id.clone(),
24992807
source_element.name.clone(),
@@ -2506,7 +2814,7 @@ impl GraphRegistry {
25062814
));
25072815
}
25082816

2509-
// Get target element data
2817+
// Re-get target element data (needed after validation)
25102818
let target_node = self.nodes.get(target_id).unwrap();
25112819
let target_file_path = target_node.element.file_path.clone();
25122820
let mut merged_content = String::new();

core/src/utils.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,16 @@ pub fn extract_path_and_fragment(identifier: &str) -> (&str, Option<&str>) {
328328
("", Some(identifier))
329329
}
330330
}
331+
332+
/// Gets the parent directory of a file path.
333+
/// Returns the parent directory as a PathBuf, or an empty PathBuf if the path has no parent.
334+
/// This is commonly used when calculating file-relative paths in markdown.
335+
pub fn get_parent_dir(file_path: &str) -> PathBuf {
336+
PathBuf::from(file_path).parent()
337+
.map(|p| p.to_path_buf())
338+
.unwrap_or_default()
339+
}
340+
331341
/// Normalizes a fragment identifier according to GitHub's rules:
332342
/// - Convert to lowercase
333343
/// - Replace spaces with hyphens

0 commit comments

Comments
 (0)