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 candid::CandidType;
8use canic_cdk::structures::{Storable, storable::Bound};
9use derive_more::{Deref, DerefMut, Display};
10use serde::{Deserialize, Serialize};
11use std::{borrow::Cow, convert::TryFrom};
12
13///
14/// BoundedString
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
64// Fallible Into<String> back
65impl<const N: u32> From<BoundedString<N>> for String {
66    fn from(b: BoundedString<N>) -> Self {
67        b.0
68    }
69}
70
71impl<const N: u32> TryFrom<String> for BoundedString<N> {
72    type Error = String;
73
74    #[allow(clippy::cast_possible_truncation)]
75    fn try_from(value: String) -> Result<Self, Self::Error> {
76        if value.len() as u32 <= N {
77            Ok(Self(value))
78        } else {
79            Err(format!("String too long for BoundedString<{N}>"))
80        }
81    }
82}
83
84impl<const N: u32> TryFrom<&str> for BoundedString<N> {
85    type Error = String;
86
87    #[allow(clippy::cast_possible_truncation)]
88    fn try_from(value: &str) -> Result<Self, Self::Error> {
89        if value.len() as u32 <= N {
90            Ok(Self(value.to_string()))
91        } else {
92            Err(format!("String too long for BoundedString<{N}>"))
93        }
94    }
95}
96
97impl<const N: u32> Storable for BoundedString<N> {
98    const BOUND: Bound = Bound::Bounded {
99        max_size: N,
100        is_fixed_size: false,
101    };
102
103    fn to_bytes(&self) -> Cow<'_, [u8]> {
104        Cow::Owned(self.0.as_bytes().to_vec())
105    }
106
107    fn into_bytes(self) -> Vec<u8> {
108        self.0.into_bytes()
109    }
110
111    fn from_bytes(bytes: Cow<[u8]>) -> Self {
112        let bytes = bytes.as_ref();
113
114        assert!(
115            bytes.len() <= N as usize,
116            "Stored string exceeds BoundedString<{N}> bound"
117        );
118
119        let s = String::from_utf8(bytes.to_vec()).expect("Stored BoundedString is not valid UTF-8");
120
121        Self(s)
122    }
123}
124
125///
126/// TESTS
127///
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn create_within_bounds() {
135        let s = "hello".to_string();
136        let b = BoundedString16::new(s.clone());
137        assert_eq!(b.0, s);
138    }
139
140    #[test]
141    fn create_at_exact_limit() {
142        let s = "a".repeat(16);
143        let b = BoundedString16::new(s.clone());
144        assert_eq!(b.0, s);
145    }
146
147    #[test]
148    fn ordering_and_equality() {
149        let a = BoundedString16::new("abc".to_string());
150        let b = BoundedString16::new("abc".to_string());
151        let c = BoundedString16::new("def".to_string());
152
153        assert_eq!(a, b);
154        assert_ne!(a, c);
155        assert!(a < c); // "abc" < "def"
156    }
157}