1use crate::orm::ChoiceField;
35use serde::{Deserialize, Deserializer, Serialize, Serializer};
36use sqlx::{Database, Decode, Encode, Postgres, Sqlite, Type};
37use std::fmt;
38use std::marker::PhantomData;
39use std::str::FromStr;
40
41#[derive(Debug, Clone, PartialEq, Eq, Default)]
47pub struct MultiChoice<E: ChoiceField> {
48 values: Vec<E>,
49}
50
51impl<E: ChoiceField> MultiChoice<E> {
52 pub const fn new() -> Self {
54 Self { values: Vec::new() }
55 }
56
57 pub fn from_vec(values: Vec<E>) -> Self {
60 Self { values }
61 }
62
63 pub fn as_slice(&self) -> &[E] {
65 &self.values
66 }
67
68 pub fn into_vec(self) -> Vec<E> {
70 self.values
71 }
72
73 pub fn len(&self) -> usize {
75 self.values.len()
76 }
77
78 pub fn is_empty(&self) -> bool {
80 self.values.is_empty()
81 }
82
83 pub fn push(&mut self, value: E) {
85 self.values.push(value);
86 }
87
88 pub fn contains(&self, value: &E) -> bool
90 where
91 E: PartialEq,
92 {
93 self.values.contains(value)
94 }
95
96 pub fn to_csv(&self) -> String {
99 let mut out = String::new();
100 for (i, v) in self.values.iter().enumerate() {
101 if i > 0 {
102 out.push(',');
103 }
104 out.push_str(v.as_str());
105 }
106 out
107 }
108
109 pub fn from_csv(s: &str) -> Result<Self, String> {
114 if s.is_empty() {
115 return Ok(Self::new());
116 }
117 let mut values = Vec::new();
118 for part in s.split(',') {
119 let part = part.trim();
120 if part.is_empty() {
121 continue;
122 }
123 match E::from_str_ok(part) {
124 Some(v) => values.push(v),
125 None => return Err(part.to_string()),
126 }
127 }
128 Ok(Self { values })
129 }
130}
131
132impl<E: ChoiceField> From<Vec<E>> for MultiChoice<E> {
133 fn from(values: Vec<E>) -> Self {
134 Self::from_vec(values)
135 }
136}
137
138impl<E: ChoiceField> FromIterator<E> for MultiChoice<E> {
139 fn from_iter<I: IntoIterator<Item = E>>(iter: I) -> Self {
140 Self {
141 values: iter.into_iter().collect(),
142 }
143 }
144}
145
146impl<E: ChoiceField> IntoIterator for MultiChoice<E> {
147 type Item = E;
148 type IntoIter = std::vec::IntoIter<E>;
149 fn into_iter(self) -> Self::IntoIter {
150 self.values.into_iter()
151 }
152}
153
154impl<'a, E: ChoiceField> IntoIterator for &'a MultiChoice<E> {
155 type Item = &'a E;
156 type IntoIter = std::slice::Iter<'a, E>;
157 fn into_iter(self) -> Self::IntoIter {
158 self.values.iter()
159 }
160}
161
162impl<E: ChoiceField> std::ops::Deref for MultiChoice<E> {
163 type Target = [E];
164 fn deref(&self) -> &Self::Target {
165 &self.values
166 }
167}
168
169impl<E: ChoiceField> fmt::Display for MultiChoice<E> {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 f.write_str(&self.to_csv())
172 }
173}
174
175impl<E: ChoiceField> FromStr for MultiChoice<E> {
176 type Err = String;
177 fn from_str(s: &str) -> Result<Self, Self::Err> {
178 Self::from_csv(s)
179 }
180}
181
182impl<E: ChoiceField> Serialize for MultiChoice<E> {
192 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
193 where
194 S: Serializer,
195 {
196 use serde::ser::SerializeSeq;
197 let mut seq = serializer.serialize_seq(Some(self.values.len()))?;
198 for v in &self.values {
199 seq.serialize_element(v.as_str())?;
200 }
201 seq.end()
202 }
203}
204
205impl<'de, E: ChoiceField> Deserialize<'de> for MultiChoice<E> {
211 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
212 where
213 D: Deserializer<'de>,
214 {
215 struct V<E>(PhantomData<E>);
216 impl<'de, E: ChoiceField> serde::de::Visitor<'de> for V<E> {
217 type Value = MultiChoice<E>;
218 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 f.write_str("a CSV string or a JSON array of choice strings")
220 }
221 fn visit_str<X: serde::de::Error>(self, s: &str) -> Result<Self::Value, X> {
222 MultiChoice::from_csv(s)
223 .map_err(|bad| X::custom(format!("unknown MultiChoice variant `{bad}`")))
224 }
225 fn visit_string<X: serde::de::Error>(self, s: String) -> Result<Self::Value, X> {
226 self.visit_str(&s)
227 }
228 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
229 where
230 A: serde::de::SeqAccess<'de>,
231 {
232 let mut values: Vec<E> = Vec::new();
233 while let Some(s) = seq.next_element::<String>()? {
234 match E::from_str_ok(&s) {
235 Some(v) => values.push(v),
236 None => {
237 return Err(serde::de::Error::custom(format!(
238 "unknown MultiChoice variant `{s}`"
239 )));
240 }
241 }
242 }
243 Ok(MultiChoice { values })
244 }
245 }
246 deserializer.deserialize_any(V::<E>(PhantomData))
247 }
248}
249
250impl<E: ChoiceField, DB: Database> Type<DB> for MultiChoice<E>
255where
256 String: Type<DB>,
257{
258 fn type_info() -> DB::TypeInfo {
259 <String as Type<DB>>::type_info()
260 }
261 fn compatible(ty: &DB::TypeInfo) -> bool {
262 <String as Type<DB>>::compatible(ty)
263 }
264}
265
266impl<'q, E: ChoiceField> Encode<'q, Sqlite> for MultiChoice<E> {
267 fn encode_by_ref(
268 &self,
269 buf: &mut <Sqlite as Database>::ArgumentBuffer<'q>,
270 ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
271 let csv = self.to_csv();
272 <String as Encode<'q, Sqlite>>::encode(csv, buf)
273 }
274}
275
276impl<'r, E: ChoiceField> Decode<'r, Sqlite> for MultiChoice<E> {
277 fn decode(value: <Sqlite as Database>::ValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
278 let s = <String as Decode<'r, Sqlite>>::decode(value)?;
279 MultiChoice::<E>::from_csv(&s).map_err(|bad| {
280 format!(
281 "unknown MultiChoice<{}> variant `{bad}`",
282 std::any::type_name::<E>()
283 )
284 .into()
285 })
286 }
287}
288
289impl<'q, E: ChoiceField> Encode<'q, Postgres> for MultiChoice<E> {
290 fn encode_by_ref(
291 &self,
292 buf: &mut <Postgres as Database>::ArgumentBuffer<'q>,
293 ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
294 let csv = self.to_csv();
295 <String as Encode<'q, Postgres>>::encode(csv, buf)
296 }
297}
298
299impl<'r, E: ChoiceField> Decode<'r, Postgres> for MultiChoice<E> {
300 fn decode(
301 value: <Postgres as Database>::ValueRef<'r>,
302 ) -> Result<Self, sqlx::error::BoxDynError> {
303 let s = <String as Decode<'r, Postgres>>::decode(value)?;
304 MultiChoice::<E>::from_csv(&s).map_err(|bad| {
305 format!(
306 "unknown MultiChoice<{}> variant `{bad}`",
307 std::any::type_name::<E>()
308 )
309 .into()
310 })
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use crate::orm::ChoiceField;
318
319 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
320 enum Tag {
321 Design,
322 Frontend,
323 Backend,
324 }
325
326 impl ChoiceField for Tag {
327 const VALUES: &'static [&'static str] = &["design", "frontend", "backend"];
328 const LABELS: &'static [&'static str] = &["Design", "Frontend", "Backend"];
329 fn as_str(&self) -> &'static str {
330 match self {
331 Tag::Design => "design",
332 Tag::Frontend => "frontend",
333 Tag::Backend => "backend",
334 }
335 }
336 fn from_str_ok(s: &str) -> Option<Self> {
337 match s {
338 "design" => Some(Tag::Design),
339 "frontend" => Some(Tag::Frontend),
340 "backend" => Some(Tag::Backend),
341 _ => None,
342 }
343 }
344 }
345
346 #[test]
347 fn csv_roundtrip() {
348 let mc: MultiChoice<Tag> = vec![Tag::Design, Tag::Backend].into();
349 assert_eq!(mc.to_csv(), "design,backend");
350 let parsed: MultiChoice<Tag> = MultiChoice::from_csv("design,backend").unwrap();
351 assert_eq!(parsed, mc);
352 }
353
354 #[test]
355 fn empty_csv_is_empty_selection() {
356 let mc: MultiChoice<Tag> = MultiChoice::from_csv("").unwrap();
357 assert!(mc.is_empty());
358 assert_eq!(mc.to_csv(), "");
359 }
360
361 #[test]
362 fn csv_trims_whitespace_and_skips_blanks() {
363 let mc: MultiChoice<Tag> = MultiChoice::from_csv(" design , , backend ").unwrap();
364 assert_eq!(mc.as_slice(), &[Tag::Design, Tag::Backend]);
365 }
366
367 #[test]
368 fn csv_rejects_unknown_variant() {
369 let err = MultiChoice::<Tag>::from_csv("design,bogus").unwrap_err();
370 assert_eq!(err, "bogus");
371 }
372
373 #[test]
374 fn serde_emits_json_array() {
375 let mc: MultiChoice<Tag> = vec![Tag::Design, Tag::Frontend].into();
376 let json = serde_json::to_string(&mc).unwrap();
377 assert_eq!(json, r#"["design","frontend"]"#);
378 }
379
380 #[test]
381 fn serde_accepts_json_array() {
382 let mc: MultiChoice<Tag> = serde_json::from_str(r#"["design","backend"]"#).unwrap();
383 assert_eq!(mc.as_slice(), &[Tag::Design, Tag::Backend]);
384 }
385
386 #[test]
387 fn serde_accepts_csv_string() {
388 let mc: MultiChoice<Tag> = serde_json::from_str(r#""design,backend""#).unwrap();
389 assert_eq!(mc.as_slice(), &[Tag::Design, Tag::Backend]);
390 }
391
392 #[test]
393 fn deref_to_slice() {
394 let mc: MultiChoice<Tag> = vec![Tag::Design].into();
395 let s: &[Tag] = &mc;
396 assert_eq!(s, &[Tag::Design]);
397 }
398}