spacetimedb_commitlog/
varchar.rs1use std::ops::Deref;
2
3#[derive(Clone, Debug, Eq, Hash, PartialEq)]
7#[repr(transparent)]
8pub struct Varchar<const N: usize> {
9 inner: String,
12}
13
14impl<const N: usize> Varchar<N> {
15 #[allow(clippy::should_implement_trait)]
18 pub fn from_str(s: &str) -> Option<Self> {
19 (s.len() <= N).then(|| Self { inner: s.into() })
20 }
21
22 pub fn from_str_truncate(s: &str) -> Self {
29 Self::from_str(s).unwrap_or_else(|| {
30 let mut s = s.to_owned();
31 while s.len() > N {
32 s.pop().unwrap();
33 }
34 Self { inner: s }
35 })
36 }
37
38 pub fn from_string(s: String) -> Option<Self> {
41 (s.len() <= N).then_some(Self { inner: s })
42 }
43
44 pub fn from_string_truncate(s: String) -> Self {
50 if s.len() <= N {
51 Self { inner: s }
52 } else {
53 let mut s = s;
54 while s.len() > N {
55 s.pop().unwrap();
56 }
57 Self { inner: s }
58 }
59 }
60
61 pub fn into_inner(self) -> String {
63 self.into()
64 }
65
66 pub fn as_str(&self) -> &str {
68 self
69 }
70}
71
72impl<const N: usize> Deref for Varchar<N> {
73 type Target = str;
74
75 fn deref(&self) -> &Self::Target {
76 &self.inner
77 }
78}
79
80impl<const N: usize> From<Varchar<N>> for String {
81 fn from(value: Varchar<N>) -> Self {
82 value.inner
83 }
84}
85
86#[cfg(feature = "serde")]
87impl<const N: usize> serde::Serialize for Varchar<N> {
88 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
89 where
90 S: serde::Serializer,
91 {
92 serializer.serialize_str(self)
93 }
94}
95
96#[cfg(feature = "serde")]
97impl<'de, const N: usize> serde::Deserialize<'de> for Varchar<N> {
98 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
99 where
100 D: serde::Deserializer<'de>,
101 {
102 let s = String::deserialize(deserializer)?;
103 let len = s.len();
104 Self::from_string(s)
105 .ok_or_else(|| serde::de::Error::custom(format!("input string too long: {len} max-len={N}")))
106 }
107}
108
109#[cfg(test)]
110pub(crate) mod tests {
111 use super::*;
112 use proptest::prelude::*;
113
114 impl<const N: usize> Arbitrary for Varchar<N> {
115 type Strategy = BoxedStrategy<Varchar<N>>;
116 type Parameters = ();
117
118 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
119 use proptest::char;
120 use proptest::collection::vec;
121
122 vec(char::ranges(char::DEFAULT_PREFERRED_RANGES.into()), 0..N)
123 .prop_map(|chars| {
124 let inner = chars.into_iter().fold(String::with_capacity(N), |mut s, c| {
125 if s.len() + c.len_utf8() <= N {
126 s.push(c);
127 }
128 s
129 });
130 Varchar { inner }
131 })
132 .boxed()
133 }
134 }
135
136 proptest! {
137 #[test]
138 fn prop_varchar_generator_does_not_break_invariant(varchar in any::<Varchar<255>>()) {
139 assert!(varchar.len() <= 255);
140 }
141
142 #[test]
143 fn prop_rejects_long(s in "\\w{33,}") {
144 assert!(Varchar::<32>::from_string(s).is_none());
145 }
146
147 #[test]
148 fn prop_accepts_short(s in "[[:ascii:]]{0,32}") {
149 assert_eq!(s.as_str(), Varchar::<32>::from_str(&s).unwrap().as_str())
150 }
151
152 #[test]
153 fn prop_truncate(s in "[[:ascii:]]{33,}") {
154 let vc = Varchar::<32>::from_string_truncate(s);
155 assert_eq!(32, vc.len());
156 }
157
158 #[test]
159 fn prop_truncate_n_on_char_boundary(s in "[[:ascii:]]{31}") {
160 let mut t = s.clone();
161 t.push('ß');
162 let vc = Varchar::<32>::from_string_truncate(t);
163 assert_eq!(*vc, s);
164 }
165 }
166}