canic_cdk/types/
string.rs1use 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#[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 fn deref(&self) -> &Self::Target {
64 &self.0
65 }
66}
67
68impl<const N: u32> DerefMut for BoundedString<N> {
69 fn deref_mut(&mut self) -> &mut Self::Target {
71 &mut self.0
72 }
73}
74
75impl<const N: u32> Display for BoundedString<N> {
76 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
89impl<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 let s = String::from_utf8_lossy(bytes).into_owned();
136
137 Self(s)
138 }
139}
140
141#[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); }
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}