1use 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#[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
75impl<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::Borrowed(self.0.as_bytes())
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 let bytes = if bytes.len() > N as usize {
117 &bytes[..N as usize]
118 } else {
119 bytes
120 };
121
122 let s = String::from_utf8_lossy(bytes).into_owned();
124
125 Self(s)
126 }
127}
128
129#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn create_within_bounds() {
139 let s = "hello".to_string();
140 let b = BoundedString16::new(s.clone());
141 assert_eq!(b.0, s);
142 }
143
144 #[test]
145 fn create_at_exact_limit() {
146 let s = "a".repeat(16);
147 let b = BoundedString16::new(s.clone());
148 assert_eq!(b.0, s);
149 }
150
151 #[test]
152 fn ordering_and_equality() {
153 let a = BoundedString16::new("abc".to_string());
154 let b = BoundedString16::new("abc".to_string());
155 let c = BoundedString16::new("def".to_string());
156
157 assert_eq!(a, b);
158 assert_ne!(a, c);
159 assert!(a < c); }
161
162 #[test]
163 fn try_new_is_fallible() {
164 let err = BoundedString16::try_new("a".repeat(17)).unwrap_err();
165 assert!(err.contains("BoundedString<16>"));
166 }
167}