Skip to main content

sdivi_core/input/
mod.rs

1//! Input structs for the pure-compute API.
2//!
3//! These are the types that WASM consumers and other embedders supply to the
4//! `compute_*` functions.  All are plain `serde` structs with no I/O, no
5//! tree-sitter, and no `std::time`.
6
7mod change_coupling_types;
8mod threshold_input;
9mod types;
10
11pub use change_coupling_types::{ChangeCouplingConfigInput, CoChangeEventInput};
12pub use edge_weight::{edge_weight_key, split_edge_weight_key};
13pub use threshold_input::{ThresholdOverrideInput, ThresholdsInput};
14pub use types::{
15    BoundaryDefInput, BoundarySpecInput, DependencyGraphInput, EdgeInput, LeidenConfigInput,
16    NodeInput, NormalizeNode, PatternInstanceInput, PatternLocationInput, PriorPartition,
17    QualityFunctionInput,
18};
19mod edge_weight;
20
21use crate::error::AnalysisError;
22
23/// Validates a node ID according to canonical rules.
24///
25/// A valid node ID is:
26/// - non-empty
27/// - uses forward slashes only (no backslashes)
28/// - no leading `./`
29/// - no trailing `/`
30/// - no absolute path component (no leading `/`)
31/// - no `..` components
32///
33/// # Errors
34///
35/// Returns [`AnalysisError::InvalidNodeId`] with the offending string on failure.
36///
37/// # Examples
38///
39/// ```rust
40/// use sdivi_core::input::validate_node_id;
41///
42/// assert!(validate_node_id("src/lib.rs").is_ok());
43/// assert!(validate_node_id("Cargo.toml").is_ok());
44/// assert!(validate_node_id("./foo").is_err());
45/// assert!(validate_node_id("foo/").is_err());
46/// assert!(validate_node_id("").is_err());
47/// assert!(validate_node_id("../foo").is_err());
48/// assert!(validate_node_id("/foo").is_err());
49/// ```
50pub fn validate_node_id(s: &str) -> Result<(), AnalysisError> {
51    if s.is_empty() {
52        return Err(AnalysisError::InvalidNodeId {
53            id: s.to_string(),
54            reason: "must not be empty".to_string(),
55        });
56    }
57    if s.contains('\\') {
58        return Err(AnalysisError::InvalidNodeId {
59            id: s.to_string(),
60            reason: "must use forward slashes only".to_string(),
61        });
62    }
63    if s.starts_with("./") {
64        return Err(AnalysisError::InvalidNodeId {
65            id: s.to_string(),
66            reason: "must not start with './'".to_string(),
67        });
68    }
69    if s.ends_with('/') {
70        return Err(AnalysisError::InvalidNodeId {
71            id: s.to_string(),
72            reason: "must not end with '/'".to_string(),
73        });
74    }
75    if s.starts_with('/') {
76        return Err(AnalysisError::InvalidNodeId {
77            id: s.to_string(),
78            reason: "must not be an absolute path".to_string(),
79        });
80    }
81    for component in s.split('/') {
82        if component == ".." {
83            return Err(AnalysisError::InvalidNodeId {
84                id: s.to_string(),
85                reason: "must not contain '..' components".to_string(),
86            });
87        }
88    }
89    Ok(())
90}