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
117 assert!(
118 bytes.len() <= N as usize,
119 "Stored string exceeds BoundedString<{N}> bound"
120 );
121
122 let s = std::str::from_utf8(bytes)
123 .expect("Stored BoundedString is not valid UTF-8")
124 .to_string();
125
126 Self(s)
127 }
128}
129
130#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn create_within_bounds() {
140 let s = "hello".to_string();
141 let b = BoundedString16::new(s.clone());
142 assert_eq!(b.0, s);
143 }
144
145 #[test]
146 fn create_at_exact_limit() {
147 let s = "a".repeat(16);
148 let b = BoundedString16::new(s.clone());
149 assert_eq!(b.0, s);
150 }
151
152 #[test]
153 fn ordering_and_equality() {
154 let a = BoundedString16::new("abc".to_string());
155 let b = BoundedString16::new("abc".to_string());
156 let c = BoundedString16::new("def".to_string());
157
158 assert_eq!(a, b);
159 assert_ne!(a, c);
160 assert!(a < c); }
162
163 #[test]
164 fn try_new_is_fallible() {
165 let err = BoundedString16::try_new("a".repeat(17)).unwrap_err();
166 assert!(err.contains("BoundedString<16>"));
167 }
168}