1use alloc::string::String;
2use core::{
3 fmt::{self, Display},
4 ops::Deref,
5 str::FromStr,
6};
7use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
8
9use bytestring::ByteString;
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
25pub struct QueueGroup(ByteString);
26
27impl QueueGroup {
28 #[must_use]
34 pub fn from_static(value: &'static str) -> Self {
35 Self::try_from(ByteString::from_static(value)).expect("invalid QueueGroup")
36 }
37
38 #[must_use]
51 #[expect(
52 clippy::missing_panics_doc,
53 reason = "The queue group validation is only made in debug"
54 )]
55 pub fn from_dangerous_value(value: ByteString) -> Self {
56 if cfg!(debug_assertions)
57 && let Err(err) = validate_queue_group(&value)
58 {
59 panic!("QueueGroup {value:?} isn't valid {err:?}");
60 }
61 Self(value)
62 }
63
64 #[must_use]
65 pub fn as_str(&self) -> &str {
66 &self.0
67 }
68}
69
70impl Display for QueueGroup {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 Display::fmt(&self.0, f)
73 }
74}
75
76impl TryFrom<ByteString> for QueueGroup {
77 type Error = QueueGroupValidateError;
78
79 fn try_from(value: ByteString) -> Result<Self, Self::Error> {
80 validate_queue_group(&value)?;
81 Ok(Self::from_dangerous_value(value))
82 }
83}
84
85impl FromStr for QueueGroup {
86 type Err = QueueGroupValidateError;
87
88 fn from_str(value: &str) -> Result<Self, Self::Err> {
89 validate_queue_group(value)?;
90 Ok(Self::from_dangerous_value(value.into()))
91 }
92}
93
94impl TryFrom<String> for QueueGroup {
95 type Error = QueueGroupValidateError;
96
97 fn try_from(value: String) -> Result<Self, Self::Error> {
98 validate_queue_group(&value)?;
99 Ok(Self::from_dangerous_value(value.into()))
100 }
101}
102
103impl From<QueueGroup> for ByteString {
104 fn from(value: QueueGroup) -> Self {
105 value.0
106 }
107}
108
109impl AsRef<[u8]> for QueueGroup {
110 fn as_ref(&self) -> &[u8] {
111 self.as_str().as_bytes()
112 }
113}
114
115impl AsRef<str> for QueueGroup {
116 fn as_ref(&self) -> &str {
117 self.as_str()
118 }
119}
120
121impl Deref for QueueGroup {
122 type Target = str;
123
124 fn deref(&self) -> &Self::Target {
125 self.as_str()
126 }
127}
128
129impl Serialize for QueueGroup {
130 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
131 self.as_str().serialize(serializer)
132 }
133}
134
135impl<'de> Deserialize<'de> for QueueGroup {
136 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
137 let s = ByteString::deserialize(deserializer)?;
138 s.try_into().map_err(de::Error::custom)
139 }
140}
141
142#[derive(Debug, thiserror::Error)]
144#[cfg_attr(test, derive(PartialEq, Eq))]
145pub enum QueueGroupValidateError {
146 #[error("QueueGroup is empty")]
148 Empty,
149 #[error("QueueGroup contained an illegal character")]
151 IllegalCharacter,
152}
153
154fn validate_queue_group(queue_group: &str) -> Result<(), QueueGroupValidateError> {
155 if queue_group.is_empty() {
156 return Err(QueueGroupValidateError::Empty);
157 }
158
159 for b in queue_group.bytes() {
160 if b == b' ' || b == b'\t' || b == b'\r' || b == b'\n' {
166 return Err(QueueGroupValidateError::IllegalCharacter);
167 }
168 }
169
170 Ok(())
171}
172
173#[cfg(test)]
174mod tests {
175 use bytestring::ByteString;
176
177 use super::{QueueGroup, QueueGroupValidateError};
178
179 #[test]
180 fn valid_queue_groups() {
181 let queue_groups = ["importer", "importer.thing", "blablabla:itworks"];
182 for queue_group in queue_groups {
183 let q = QueueGroup::try_from(ByteString::from_static(queue_group)).unwrap();
184 assert_eq!(queue_group, q.as_str());
185 }
186 }
187
188 #[test]
189 fn invalid_queue_groups() {
190 let queue_groups = [
191 ("", QueueGroupValidateError::Empty),
192 ("importer ", QueueGroupValidateError::IllegalCharacter),
193 ("importer .thing", QueueGroupValidateError::IllegalCharacter),
194 (" importer", QueueGroupValidateError::IllegalCharacter),
195 ("importer.thing ", QueueGroupValidateError::IllegalCharacter),
196 (
197 "importer.thing.works ",
198 QueueGroupValidateError::IllegalCharacter,
199 ),
200 (
201 "importer.thing.works\r",
202 QueueGroupValidateError::IllegalCharacter,
203 ),
204 (
205 "importer.thing.works\t",
206 QueueGroupValidateError::IllegalCharacter,
207 ),
208 (
209 "importer.thi ng.works",
210 QueueGroupValidateError::IllegalCharacter,
211 ),
212 (
213 "importer.thi\tng.works",
214 QueueGroupValidateError::IllegalCharacter,
215 ),
216 (
217 "importer.thing .works",
218 QueueGroupValidateError::IllegalCharacter,
219 ),
220 (
221 "importer.thing\t.works",
222 QueueGroupValidateError::IllegalCharacter,
223 ),
224 (" ", QueueGroupValidateError::IllegalCharacter),
225 ("\t", QueueGroupValidateError::IllegalCharacter),
226 (
227 "importer.thing.works\n",
228 QueueGroupValidateError::IllegalCharacter,
229 ),
230 (
231 "importer.thi\rng.works",
232 QueueGroupValidateError::IllegalCharacter,
233 ),
234 (
235 "importer.thi\nng.works",
236 QueueGroupValidateError::IllegalCharacter,
237 ),
238 (
239 "importer.thing\r.works",
240 QueueGroupValidateError::IllegalCharacter,
241 ),
242 (
243 "importer.thing\n.works",
244 QueueGroupValidateError::IllegalCharacter,
245 ),
246 ("\r", QueueGroupValidateError::IllegalCharacter),
247 ("\n", QueueGroupValidateError::IllegalCharacter),
248 ];
249 for (queue_group, expected_err) in queue_groups {
250 let err = QueueGroup::try_from(ByteString::from_static(queue_group)).unwrap_err();
251 assert_eq!(expected_err, err);
252 }
253 }
254}