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    pub fn try_new(s: impl Into<String>) -> Result<Self, String> {
38        let s: String = s.into();
39
40        #[allow(clippy::cast_possible_truncation)]
41        if s.len() as u32 <= N {
42            Ok(Self(s))
43        } else {
44            Err(format!("String too long for BoundedString<{N}>"))
45        }
46    }
47
48    #[must_use]
49    pub fn new(s: impl Into<String>) -> Self {
50        let s: String = s.into();
51        let slen = s.len();
52
53        assert!(
54            slen as u32 <= N,
55            "String '{s}' too long for BoundedString<{N}> ({slen} bytes)",
56        );
57
58        Self(s)
59    }
60}
61
62impl<const N: u32> AsRef<str> for BoundedString<N> {
63    fn as_ref(&self) -> &str {
64        &self.0
65    }
66}
67
68pub type BoundedString8 = BoundedString<8>;
69pub type BoundedString16 = BoundedString<16>;
70pub type BoundedString32 = BoundedString<32>;
71pub type BoundedString64 = BoundedString<64>;
72pub type BoundedString128 = BoundedString<128>;
73pub type BoundedString256 = BoundedString<256>;
74
75// Fallible Into<String> back
76impl<const N: u32> From<BoundedString<N>> for String {
77    fn from(b: BoundedString<N>) -> Self {
78        b.0
79    }
80}
81
82impl<const N: u32> TryFrom<String> for BoundedString<N> {
83    type Error = String;
84
85    #[allow(clippy::cast_possible_truncation)]
86    fn try_from(value: String) -> Result<Self, Self::Error> {
87        Self::try_new(value)
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        Self::try_new(value)
97    }
98}
99
100impl<const N: u32> Storable for BoundedString<N> {
101    const BOUND: Bound = Bound::Bounded {
102        max_size: N,
103        is_fixed_size: false,
104    };
105
106    fn to_bytes(&self) -> Cow<'_, [u8]> {
107        Cow::Owned(self.0.as_bytes().to_vec())
108    }
109
110    fn into_bytes(self) -> Vec<u8> {
111        self.0.into_bytes()
112    }
113
114    fn from_bytes(bytes: Cow<[u8]>) -> Self {
115        let bytes = bytes.as_ref();
116
117        assert!(
118            bytes.len() <= N as usize,
119            "Stored string exceeds BoundedString<{N}> bound"
120        );
121
122        let s = String::from_utf8(bytes.to_vec()).expect("Stored BoundedString is not valid UTF-8");
123
124        Self(s)
125    }
126}
127
128///
129/// TESTS
130///
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn create_within_bounds() {
138        let s = "hello".to_string();
139        let b = BoundedString16::new(s.clone());
140        assert_eq!(b.0, s);
141    }
142
143    #[test]
144    fn create_at_exact_limit() {
145        let s = "a".repeat(16);
146        let b = BoundedString16::new(s.clone());
147        assert_eq!(b.0, s);
148    }
149
150    #[test]
151    fn ordering_and_equality() {
152        let a = BoundedString16::new("abc".to_string());
153        let b = BoundedString16::new("abc".to_string());
154        let c = BoundedString16::new("def".to_string());
155
156        assert_eq!(a, b);
157        assert_ne!(a, c);
158        assert!(a < c); // "abc" < "def"
159    }
160
161    #[test]
162    fn try_new_is_fallible() {
163        let err = BoundedString16::try_new("a".repeat(17)).unwrap_err();
164        assert!(err.contains("BoundedString<16>"));
165    }
166}