Skip to main content

nodedb_types/id/
shape.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Shape subscription identifier.
4
5use std::borrow::Borrow;
6use std::fmt;
7
8use serde::{Deserialize, Serialize};
9
10use super::error::{IdError, validate};
11
12/// Identifies a shape subscription (globally unique per Origin).
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)]
25pub struct ShapeId(String);
26
27impl ShapeId {
28    /// Construct a `ShapeId`, validating the input string.
29    ///
30    /// Returns `Err(IdError)` if the string is empty, exceeds
31    /// [`ID_MAX_LEN`][super::error::ID_MAX_LEN] bytes, or contains a NUL byte.
32    pub fn try_new(id: impl Into<String>) -> Result<Self, IdError> {
33        let s = id.into();
34        validate(&s)?;
35        Ok(Self(s))
36    }
37
38    /// Construct without validation. Caller must guarantee the input was
39    /// already validated by `try_new` (or came from a previously-validated
40    /// source like deserialized wire bytes from a NodeDB server).
41    pub fn from_validated(id: String) -> Self {
42        Self(id)
43    }
44
45    pub fn as_str(&self) -> &str {
46        &self.0
47    }
48}
49
50impl fmt::Display for ShapeId {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        f.write_str(&self.0)
53    }
54}
55
56impl Borrow<str> for ShapeId {
57    fn borrow(&self) -> &str {
58        &self.0
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::super::error::ID_MAX_LEN;
65    use super::*;
66
67    #[test]
68    fn try_new_accepts_valid() {
69        let s = ShapeId::try_new("shape-001").expect("valid");
70        assert_eq!(s.as_str(), "shape-001");
71    }
72
73    #[test]
74    fn try_new_rejects_empty() {
75        assert_eq!(ShapeId::try_new(""), Err(IdError::Empty));
76    }
77
78    #[test]
79    fn try_new_rejects_too_long() {
80        let long = "x".repeat(ID_MAX_LEN + 1);
81        assert!(matches!(
82            ShapeId::try_new(long),
83            Err(IdError::TooLong { .. })
84        ));
85    }
86
87    #[test]
88    fn try_new_rejects_nul() {
89        assert_eq!(ShapeId::try_new("ab\0cd"), Err(IdError::ContainsNul));
90    }
91
92    #[test]
93    fn try_new_accepts_max_length() {
94        let exact = "a".repeat(ID_MAX_LEN);
95        assert!(ShapeId::try_new(exact).is_ok());
96    }
97
98    #[test]
99    fn try_new_accepts_unicode() {
100        assert!(ShapeId::try_new("形状:001").is_ok());
101    }
102
103    #[test]
104    fn from_validated_does_not_validate() {
105        let oversized = "z".repeat(ID_MAX_LEN * 2);
106        let s = ShapeId::from_validated(oversized.clone());
107        assert_eq!(s.as_str(), oversized);
108    }
109}