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