canic/types/
string.rs

1//!
2//! Bounded string wrappers that integrate with stable structures and enforce
3//! maximum lengths at construction time. These appear in configs and memory
4//! tables where size caps matter.
5//!
6
7use crate::impl_storable_bounded;
8use candid::CandidType;
9use derive_more::{Deref, DerefMut, Display};
10use serde::{Deserialize, Serialize};
11use std::convert::TryFrom;
12
13///
14/// BoundedString
15///
16/// String wrapper enforcing a compile-time maximum length, with serde and
17/// storage trait implementations.
18///
19
20#[derive(
21    CandidType,
22    Clone,
23    Debug,
24    Deref,
25    DerefMut,
26    Deserialize,
27    Display,
28    Eq,
29    Ord,
30    PartialEq,
31    PartialOrd,
32    Serialize,
33)]
34pub struct BoundedString<const N: u32>(pub String);
35
36#[allow(clippy::cast_possible_truncation)]
37impl<const N: u32> BoundedString<N> {
38    #[must_use]
39    pub fn new(s: impl Into<String>) -> Self {
40        let s: String = s.into();
41        let slen = s.len();
42
43        assert!(
44            slen as u32 <= N,
45            "String '{s}' too long for BoundedString<{N}> ({slen} bytes)",
46        );
47
48        Self(s)
49    }
50}
51
52impl<const N: u32> AsRef<str> for BoundedString<N> {
53    fn as_ref(&self) -> &str {
54        &self.0
55    }
56}
57
58pub type BoundedString8 = BoundedString<8>;
59pub type BoundedString16 = BoundedString<16>;
60pub type BoundedString32 = BoundedString<32>;
61pub type BoundedString64 = BoundedString<64>;
62pub type BoundedString128 = BoundedString<128>;
63pub type BoundedString256 = BoundedString<256>;
64
65impl_storable_bounded!(BoundedString8, 8, false);
66impl_storable_bounded!(BoundedString16, 16, false);
67impl_storable_bounded!(BoundedString32, 32, false);
68impl_storable_bounded!(BoundedString64, 64, false);
69impl_storable_bounded!(BoundedString128, 128, false);
70impl_storable_bounded!(BoundedString256, 256, false);
71
72// Fallible Into<String> back
73impl<const N: u32> From<BoundedString<N>> for String {
74    fn from(b: BoundedString<N>) -> Self {
75        b.0
76    }
77}
78
79impl<const N: u32> TryFrom<String> for BoundedString<N> {
80    type Error = String;
81
82    #[allow(clippy::cast_possible_truncation)]
83    fn try_from(value: String) -> Result<Self, Self::Error> {
84        if value.len() as u32 <= N {
85            Ok(Self(value))
86        } else {
87            Err(format!("String too long for BoundedString<{N}>"))
88        }
89    }
90}
91
92impl<const N: u32> TryFrom<&str> for BoundedString<N> {
93    type Error = String;
94
95    #[allow(clippy::cast_possible_truncation)]
96    fn try_from(value: &str) -> Result<Self, Self::Error> {
97        if value.len() as u32 <= N {
98            Ok(Self(value.to_string()))
99        } else {
100            Err(format!("String too long for BoundedString<{N}>"))
101        }
102    }
103}
104
105///
106/// TESTS
107///
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::{
113        cdk::structures::Storable,
114        serialize::{deserialize, serialize},
115    };
116
117    #[test]
118    fn create_within_bounds() {
119        let s = "hello".to_string();
120        let b = BoundedString16::new(s.clone());
121        assert_eq!(b.0, s);
122    }
123
124    #[test]
125    fn create_at_exact_limit() {
126        let s = "a".repeat(16);
127        let b = BoundedString16::new(s.clone());
128        assert_eq!(b.0, s);
129    }
130
131    #[test]
132    fn ordering_and_equality() {
133        let a = BoundedString16::new("abc".to_string());
134        let b = BoundedString16::new("abc".to_string());
135        let c = BoundedString16::new("def".to_string());
136
137        assert_eq!(a, b);
138        assert_ne!(a, c);
139        assert!(a < c); // "abc" < "def"
140    }
141
142    #[test]
143    fn serialize_and_deserialize_roundtrip() {
144        let original = BoundedString32::new("roundtrip test".to_string());
145
146        // to bytes
147        let bytes = serialize(&original).unwrap();
148
149        // back
150        let decoded: BoundedString32 = deserialize(&bytes).unwrap();
151
152        assert_eq!(original, decoded);
153    }
154
155    #[test]
156    fn storable_impl_to_bytes_and_from_bytes() {
157        let original = BoundedString64::new("hello world".to_string());
158
159        let bytes = serialize(&original).unwrap();
160        let cow = std::borrow::Cow::Owned(bytes);
161
162        // Storable trait methods
163        let stored = <BoundedString64 as Storable>::from_bytes(cow);
164
165        assert_eq!(original, stored);
166
167        let owned = original.clone().into_bytes();
168        assert_eq!(deserialize::<BoundedString64>(&owned).unwrap(), original);
169    }
170}