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 #[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<const N: u32> From<BoundedString<N>> for String {
66 fn from(b: BoundedString<N>) -> Self {
67 b.0
68 }
69}
70
71impl<const N: u32> TryFrom<String> for BoundedString<N> {
72 type Error = String;
73
74 #[allow(clippy::cast_possible_truncation)]
75 fn try_from(value: String) -> Result<Self, Self::Error> {
76 if value.len() as u32 <= N {
77 Ok(Self(value))
78 } else {
79 Err(format!("String too long for BoundedString<{N}>"))
80 }
81 }
82}
83
84impl<const N: u32> TryFrom<&str> for BoundedString<N> {
85 type Error = String;
86
87 #[allow(clippy::cast_possible_truncation)]
88 fn try_from(value: &str) -> Result<Self, Self::Error> {
89 if value.len() as u32 <= N {
90 Ok(Self(value.to_string()))
91 } else {
92 Err(format!("String too long for BoundedString<{N}>"))
93 }
94 }
95}
96
97impl<const N: u32> Storable for BoundedString<N> {
98 const BOUND: Bound = Bound::Bounded {
99 max_size: N,
100 is_fixed_size: false,
101 };
102
103 fn to_bytes(&self) -> Cow<'_, [u8]> {
104 Cow::Owned(self.0.as_bytes().to_vec())
105 }
106
107 fn into_bytes(self) -> Vec<u8> {
108 self.0.into_bytes()
109 }
110
111 fn from_bytes(bytes: Cow<[u8]>) -> Self {
112 let bytes = bytes.as_ref();
113
114 assert!(
115 bytes.len() <= N as usize,
116 "Stored string exceeds BoundedString<{N}> bound"
117 );
118
119 let s = String::from_utf8(bytes.to_vec()).expect("Stored BoundedString is not valid UTF-8");
120
121 Self(s)
122 }
123}
124
125#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn create_within_bounds() {
135 let s = "hello".to_string();
136 let b = BoundedString16::new(s.clone());
137 assert_eq!(b.0, s);
138 }
139
140 #[test]
141 fn create_at_exact_limit() {
142 let s = "a".repeat(16);
143 let b = BoundedString16::new(s.clone());
144 assert_eq!(b.0, s);
145 }
146
147 #[test]
148 fn ordering_and_equality() {
149 let a = BoundedString16::new("abc".to_string());
150 let b = BoundedString16::new("abc".to_string());
151 let c = BoundedString16::new("def".to_string());
152
153 assert_eq!(a, b);
154 assert_ne!(a, c);
155 assert!(a < c); }
157}