Skip to main content

nodedb_types/id/
collection.rs

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