Skip to main content

nodedb_types/id/
node.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Graph node identifier.
4
5use std::fmt;
6
7use serde::{Deserialize, Serialize};
8
9use super::error::{IdError, validate};
10
11/// Identifies a graph node. Separate from `DocumentId` because graph nodes
12/// can exist independently of documents (e.g., concept nodes in a knowledge graph).
13#[derive(
14    Debug,
15    Clone,
16    PartialEq,
17    Eq,
18    Hash,
19    Serialize,
20    Deserialize,
21    rkyv::Archive,
22    rkyv::Serialize,
23    rkyv::Deserialize,
24    zerompk::ToMessagePack,
25    zerompk::FromMessagePack,
26)]
27pub struct NodeId(String);
28
29impl NodeId {
30    /// Construct a `NodeId`, validating the input string.
31    ///
32    /// Returns `Err(IdError)` if the string is empty, exceeds
33    /// [`ID_MAX_LEN`][super::error::ID_MAX_LEN] bytes, or contains a NUL byte.
34    pub fn try_new(id: impl Into<String>) -> Result<Self, IdError> {
35        let s = id.into();
36        validate(&s)?;
37        Ok(Self(s))
38    }
39
40    /// Construct without validation. Caller must guarantee the input was
41    /// already validated by `try_new` (or came from a previously-validated
42    /// source like deserialized wire bytes from a NodeDB server).
43    pub fn from_validated(id: String) -> Self {
44        Self(id)
45    }
46
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50}
51
52impl fmt::Display for NodeId {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        f.write_str(&self.0)
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::super::error::ID_MAX_LEN;
61    use super::*;
62
63    #[test]
64    fn try_new_accepts_valid() {
65        let n = NodeId::try_new("concept:rust").expect("valid");
66        assert_eq!(n.as_str(), "concept:rust");
67    }
68
69    #[test]
70    fn try_new_rejects_empty() {
71        assert_eq!(NodeId::try_new(""), Err(IdError::Empty));
72    }
73
74    #[test]
75    fn try_new_rejects_too_long() {
76        let long = "x".repeat(ID_MAX_LEN + 1);
77        assert!(matches!(
78            NodeId::try_new(long),
79            Err(IdError::TooLong { .. })
80        ));
81    }
82
83    #[test]
84    fn try_new_rejects_nul() {
85        assert_eq!(NodeId::try_new("ab\0cd"), Err(IdError::ContainsNul));
86    }
87
88    #[test]
89    fn try_new_accepts_max_length() {
90        let exact = "a".repeat(ID_MAX_LEN);
91        assert!(NodeId::try_new(exact).is_ok());
92    }
93
94    #[test]
95    fn try_new_accepts_unicode() {
96        assert!(NodeId::try_new("节点:rust").is_ok());
97    }
98
99    #[test]
100    fn from_validated_does_not_validate() {
101        let oversized = "z".repeat(ID_MAX_LEN * 2);
102        let n = NodeId::from_validated(oversized.clone());
103        assert_eq!(n.as_str(), oversized);
104    }
105}