Skip to main content

canic_cdk/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::structures::{Storable, storable::Bound};
8use candid::CandidType;
9use serde::{Deserialize, Serialize};
10use std::{
11    borrow::Cow,
12    convert::TryFrom,
13    fmt::{self, Display},
14    ops::{Deref, DerefMut},
15};
16
17///
18/// BoundedString
19/// String wrapper enforcing a compile-time maximum length, with serde and
20/// storage trait implementations.
21///
22
23#[derive(CandidType, Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
24pub struct BoundedString<const N: u32>(pub String);
25
26#[expect(clippy::cast_possible_truncation)]
27impl<const N: u32> BoundedString<N> {
28    pub fn try_new(s: impl Into<String>) -> Result<Self, String> {
29        let s: String = s.into();
30
31        #[expect(clippy::cast_possible_truncation)]
32        if s.len() as u32 <= N {
33            Ok(Self(s))
34        } else {
35            Err(format!("String too long for BoundedString<{N}>"))
36        }
37    }
38
39    #[must_use]
40    pub fn new(s: impl Into<String>) -> Self {
41        let s: String = s.into();
42        let slen = s.len();
43
44        assert!(
45            slen as u32 <= N,
46            "String '{s}' too long for BoundedString<{N}> ({slen} bytes)",
47        );
48
49        Self(s)
50    }
51}
52
53impl<const N: u32> AsRef<str> for BoundedString<N> {
54    fn as_ref(&self) -> &str {
55        &self.0
56    }
57}
58
59impl<const N: u32> Deref for BoundedString<N> {
60    type Target = String;
61
62    // Expose the inner string for existing string-like call sites.
63    fn deref(&self) -> &Self::Target {
64        &self.0
65    }
66}
67
68impl<const N: u32> DerefMut for BoundedString<N> {
69    // Expose mutable string access while preserving the bounded wrapper.
70    fn deref_mut(&mut self) -> &mut Self::Target {
71        &mut self.0
72    }
73}
74
75impl<const N: u32> Display for BoundedString<N> {
76    // Render the bounded wrapper exactly like its inner string.
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        Display::fmt(&self.0, f)
79    }
80}
81
82pub type BoundedString8 = BoundedString<8>;
83pub type BoundedString16 = BoundedString<16>;
84pub type BoundedString32 = BoundedString<32>;
85pub type BoundedString64 = BoundedString<64>;
86pub type BoundedString128 = BoundedString<128>;
87pub type BoundedString256 = BoundedString<256>;
88
89// Fallible Into<String> back
90impl<const N: u32> From<BoundedString<N>> for String {
91    fn from(b: BoundedString<N>) -> Self {
92        b.0
93    }
94}
95
96impl<const N: u32> TryFrom<String> for BoundedString<N> {
97    type Error = String;
98
99    fn try_from(value: String) -> Result<Self, Self::Error> {
100        Self::try_new(value)
101    }
102}
103
104impl<const N: u32> TryFrom<&str> for BoundedString<N> {
105    type Error = String;
106
107    fn try_from(value: &str) -> Result<Self, Self::Error> {
108        Self::try_new(value)
109    }
110}
111
112impl<const N: u32> Storable for BoundedString<N> {
113    const BOUND: Bound = Bound::Bounded {
114        max_size: N,
115        is_fixed_size: false,
116    };
117
118    fn to_bytes(&self) -> Cow<'_, [u8]> {
119        Cow::Borrowed(self.0.as_bytes())
120    }
121
122    fn into_bytes(self) -> Vec<u8> {
123        self.0.into_bytes()
124    }
125
126    fn from_bytes(bytes: Cow<[u8]>) -> Self {
127        let bytes = bytes.as_ref();
128        let bytes = if bytes.len() > N as usize {
129            &bytes[..N as usize]
130        } else {
131            bytes
132        };
133
134        // Best-effort decode to avoid trapping on corrupted or migrated data.
135        let s = String::from_utf8_lossy(bytes).into_owned();
136
137        Self(s)
138    }
139}
140
141///
142/// TESTS
143///
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn create_within_bounds() {
151        let s = "hello".to_string();
152        let b = BoundedString16::new(s.clone());
153        assert_eq!(b.0, s);
154    }
155
156    #[test]
157    fn create_at_exact_limit() {
158        let s = "a".repeat(16);
159        let b = BoundedString16::new(s.clone());
160        assert_eq!(b.0, s);
161    }
162
163    #[test]
164    fn ordering_and_equality() {
165        let a = BoundedString16::new("abc".to_string());
166        let b = BoundedString16::new("abc".to_string());
167        let c = BoundedString16::new("def".to_string());
168
169        assert_eq!(a, b);
170        assert_ne!(a, c);
171        assert!(a < c); // "abc" < "def"
172    }
173
174    #[test]
175    fn try_new_is_fallible() {
176        let err = BoundedString16::try_new("a".repeat(17)).unwrap_err();
177        assert!(!err.is_empty());
178    }
179}