Skip to main content

selene_graph/graph_types/
endpoint.rs

1//! Edge endpoint definitions for closed graph types.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7/// Edge endpoint definition.
8///
9/// `OneOf` enumerates a sorted, deduplicated set of distinct node-type indices.
10/// Construct it via [`EdgeEndpointDef::one_of`] to enforce the structural
11/// invariants (sorted, deduplicated, length >= 2; singleton collapses to
12/// [`EdgeEndpointDef::NodeType`]). Direct struct construction is permitted for
13/// rkyv/serde decode paths but is rejected by
14/// [`super::GraphTypeDef::validate_ref`] as defense in depth.
15#[derive(
16    Clone,
17    Debug,
18    Deserialize,
19    Eq,
20    Hash,
21    PartialEq,
22    rkyv::Archive,
23    rkyv::Deserialize,
24    rkyv::Serialize,
25    Serialize,
26)]
27pub enum EdgeEndpointDef {
28    /// Accept any declared node type at this endpoint.
29    Any,
30    /// Require one concrete node-type index.
31    NodeType(u32),
32    /// Accept any node type drawn from a sorted, deduplicated, length >= 2 set
33    /// of concrete node-type indices.
34    OneOf(Vec<u32>),
35}
36
37impl EdgeEndpointDef {
38    /// Construct an endpoint accepting `indices`, canonicalized.
39    ///
40    /// The input is sorted ascending and deduplicated. A single resulting
41    /// index collapses to [`EdgeEndpointDef::NodeType`] so that `Eq`/`Hash`
42    /// and rkyv byte encoding remain stable across construction paths.
43    ///
44    /// # Panics
45    ///
46    /// Panics when the resulting set is empty; zero-label endpoints are a
47    /// caller bug and the upstream resolver must reject them before reaching
48    /// this constructor.
49    #[must_use]
50    pub fn one_of(indices: impl IntoIterator<Item = u32>) -> Self {
51        let mut buf: Vec<u32> = indices.into_iter().collect();
52        buf.sort_unstable();
53        buf.dedup();
54        assert!(
55            !buf.is_empty(),
56            "EdgeEndpointDef::one_of called with empty index set"
57        );
58        match buf.len() {
59            1 => Self::NodeType(buf[0]),
60            _ => Self::OneOf(buf),
61        }
62    }
63
64    /// Return true when this endpoint accepts the observed node-type index.
65    #[must_use]
66    pub fn matches_node_type(&self, node_type: u32) -> bool {
67        match self {
68            Self::Any => true,
69            Self::NodeType(expected) => *expected == node_type,
70            Self::OneOf(indices) => indices.binary_search(&node_type).is_ok(),
71        }
72    }
73
74    /// Return true when two endpoint declarations can match a common node type.
75    #[must_use]
76    pub fn overlaps(&self, other: &Self) -> bool {
77        match (self, other) {
78            (Self::Any, _) | (_, Self::Any) => true,
79            (Self::NodeType(left), Self::NodeType(right)) => left == right,
80            (Self::NodeType(index), Self::OneOf(indices))
81            | (Self::OneOf(indices), Self::NodeType(index)) => indices.binary_search(index).is_ok(),
82            (Self::OneOf(left), Self::OneOf(right)) => sorted_slices_intersect(left, right),
83        }
84    }
85
86    /// Return the concrete node-type index when this endpoint resolves to
87    /// exactly one node type.
88    ///
89    /// Returns `None` for [`EdgeEndpointDef::Any`] and
90    /// [`EdgeEndpointDef::OneOf`]; callers that need to enumerate the
91    /// candidate set should match the variant explicitly.
92    #[must_use]
93    pub const fn node_type_index(&self) -> Option<u32> {
94        match self {
95            Self::Any | Self::OneOf(_) => None,
96            Self::NodeType(index) => Some(*index),
97        }
98    }
99}
100
101impl fmt::Display for EdgeEndpointDef {
102    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            Self::Any => formatter.write_str("Any"),
105            Self::NodeType(index) => write!(formatter, "{index}"),
106            Self::OneOf(indices) => {
107                formatter.write_str("OneOf(")?;
108                for (position, index) in indices.iter().enumerate() {
109                    if position > 0 {
110                        formatter.write_str(", ")?;
111                    }
112                    write!(formatter, "{index}")?;
113                }
114                formatter.write_str(")")
115            }
116        }
117    }
118}
119
120fn sorted_slices_intersect(left: &[u32], right: &[u32]) -> bool {
121    let (mut i, mut j) = (0, 0);
122    while i < left.len() && j < right.len() {
123        match left[i].cmp(&right[j]) {
124            std::cmp::Ordering::Less => i += 1,
125            std::cmp::Ordering::Greater => j += 1,
126            std::cmp::Ordering::Equal => return true,
127        }
128    }
129    false
130}