1use crate::{cdk::candid::CandidType, impl_storable_bounded};
8use derive_more::{Deref, DerefMut, Display};
9use serde::{Deserialize, Serialize};
10use std::convert::TryFrom;
11
12#[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
71impl<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#[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); }
140
141 #[test]
142 fn serialize_and_deserialize_roundtrip() {
143 let original = BoundedString32::new("roundtrip test".to_string());
144
145 let bytes = serialize(&original).unwrap();
147
148 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 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}