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