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::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#[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); }
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}