Skip to main content

connect_user_value/
lib.rs

1use std::borrow::Cow;
2use std::path::PathBuf;
3use std::sync::Arc;
4use std::time::Duration;
5
6use arrow_array::RecordBatch;
7use arrow_array::array::ArrayRef;
8use arrow_array::array::StringArray;
9use arrow_cast::display::ArrayFormatter;
10use bevy_math::DVec2;
11use chrono::DateTime;
12use chrono::NaiveDateTime;
13use chrono::NaiveTime;
14use chrono::Utc;
15use serde::Deserialize;
16use serde::Deserializer;
17use serde::Serialize;
18use serde::Serializer;
19use serde::de::Error;
20use tracing::info;
21use tracing::warn;
22
23#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Default)]
24#[serde(untagged)]
25pub enum UserValue {
26    #[default]
27    #[serde(serialize_with = "serde::ser::Serializer::serialize_none")]
28    None,
29    // NOTE: this variant must be placed before 'Text' in order for it to deserialize correctly.
30    DateTime(NaiveDateTime),
31    Time(NaiveTime),
32    Duration(Duration),
33    Text(String),
34    Number(f64),
35    Bool(bool),
36    Files(Vec<PathBuf>),
37    Notes(Vec<Note>),
38    Table(TableData),
39    GeoPos(GeoPos),
40    PlotRegions(Vec<PlotRegion>),
41}
42
43#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
44pub struct Note {
45    timestamp: DateTime<Utc>,
46    pub text: String,
47}
48
49impl Note {
50    pub fn new(text: impl Into<String>) -> Self {
51        Self::with_timestamp(Utc::now(), text.into())
52    }
53    pub fn with_timestamp(timestamp: DateTime<Utc>, text: impl Into<String>) -> Self {
54        Self {
55            timestamp,
56            text: text.into(),
57        }
58    }
59
60    pub fn preview(&self) -> Cow<'_, str> {
61        if let Some((i, _)) = self.text.char_indices().nth(8) {
62            Cow::Borrowed(&self.text[..i])
63        } else {
64            Cow::Owned(self.timestamp.with_timezone(&chrono::Local).to_string())
65        }
66    }
67}
68
69#[derive(Debug, Clone, PartialEq)]
70pub struct TableData {
71    pub batch: RecordBatch,
72}
73
74#[derive(Deserialize, Serialize, Debug, Clone)]
75struct JsonTableData {
76    #[serde(default)]
77    pub columns: Vec<String>,
78    #[serde(deserialize_with = "deserialize_string_array", default)]
79    pub data: Vec<Vec<String>>,
80}
81
82impl<'w> Deserialize<'w> for TableData {
83    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
84    where
85        D: Deserializer<'w>,
86    {
87        let data = JsonTableData::deserialize(deserializer)?;
88
89        info!(
90            "Parsed table data: {} columns, {} rows",
91            data.columns.len(),
92            data.data.len()
93        );
94
95        if data.columns.len() != data.data.len() {
96            warn!(
97                "number of columns does not match shape of data: {} != {}",
98                data.columns.len(),
99                data.data.len()
100            );
101        }
102
103        let columns = std::iter::zip(data.data, data.columns)
104            .map(|(column, colname)| (colname, Arc::new(StringArray::from(column)) as ArrayRef));
105
106        let batch = RecordBatch::try_from_iter(columns).map_err(D::Error::custom)?;
107        Ok(Self { batch })
108    }
109}
110
111impl Serialize for TableData {
112    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
113    where
114        S: serde::Serializer,
115    {
116        let columns = self
117            .batch
118            .schema_ref()
119            .fields()
120            .iter()
121            .map(|field| field.name().clone())
122            .collect();
123        let data = self
124            .batch
125            .columns()
126            .iter()
127            .filter_map(|col| {
128                let Ok(formatter) = ArrayFormatter::try_new(col, &Default::default()) else {
129                    return None;
130                };
131                Some(
132                    (0..col.len())
133                        .map(|i| formatter.value(i).to_string())
134                        .collect::<Vec<_>>(),
135                )
136            })
137            .collect();
138        let value = JsonTableData { columns, data };
139        value.serialize(serializer)
140    }
141}
142
143pub fn deserialize_string_array<'de, D>(deserializer: D) -> Result<Vec<Vec<String>>, D::Error>
144where
145    D: Deserializer<'de>,
146{
147    let raw_data: Vec<Vec<Option<String>>> = Vec::deserialize(deserializer)?;
148    Ok(raw_data
149        .into_iter()
150        .map(|row| {
151            row.into_iter()
152                .map(|cell| cell.unwrap_or_default())
153                .collect()
154        })
155        .collect())
156}
157
158#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
159pub struct PlotRegion {
160    pub start_timestamp: f64,
161    pub end_timestamp: f64,
162    pub color: Color,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq)]
166pub struct Color(ecolor::Color32);
167
168impl Color {
169    pub const fn as_egui_color(self) -> ecolor::Color32 {
170        self.0
171    }
172}
173
174impl From<Color> for ecolor::Color32 {
175    fn from(value: Color) -> Self {
176        value.as_egui_color()
177    }
178}
179
180impl<'de> Deserialize<'de> for Color {
181    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
182    where
183        D: Deserializer<'de>,
184    {
185        let input = String::deserialize(deserializer)?;
186        let hex = input.trim().to_lowercase();
187
188        if !hex.starts_with('#') {
189            return Err(D::Error::custom(
190                "hex colors must start with the `#` character",
191            ));
192        }
193
194        let color = match ecolor::Color32::from_hex(&hex) {
195            Ok(color) => color,
196            Err(ecolor::ParseHexColorError::MissingHash) => {
197                // Technically unreachable here
198                return Err(D::Error::custom("hex color is missing `#` prefix"));
199            }
200            Err(ecolor::ParseHexColorError::InvalidLength) => {
201                return Err(D::Error::custom(
202                    "hex color has invalid length — must be 7 or 9 characters (e.g., `#RRGGBB` or `#RRGGBBAA`)",
203                ));
204            }
205            Err(ecolor::ParseHexColorError::InvalidInt(e)) => {
206                return Err(D::Error::custom(format!("failed to parse hex digits: {e}")));
207            }
208        };
209
210        Ok(Color(color))
211    }
212}
213
214impl Serialize for Color {
215    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
216    where
217        S: Serializer,
218    {
219        let hex_string = format!(
220            "#{:02x}{:02x}{:02x}{:02x}",
221            self.0.r(),
222            self.0.g(),
223            self.0.b(),
224            self.0.a()
225        );
226        serializer.serialize_str(&hex_string)
227    }
228}
229
230#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Default)]
231pub struct GeoPos {
232    pub latitude: f64,
233    pub longitude: f64,
234}
235
236impl From<DVec2> for GeoPos {
237    fn from(value: DVec2) -> Self {
238        Self {
239            latitude: value.y,
240            longitude: value.x,
241        }
242    }
243}
244
245impl From<GeoPos> for DVec2 {
246    fn from(value: GeoPos) -> Self {
247        Self {
248            x: value.longitude,
249            y: value.latitude,
250        }
251    }
252}
253
254impl std::fmt::Display for GeoPos {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        write!(f, "{},{}", self.latitude, self.longitude)
257    }
258}
259
260const DATE_TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S";
261const TIME_FORMAT: &str = "%H:%M:%S";
262
263impl UserValue {
264    pub fn string_mut(&mut self) -> &mut String {
265        match *self {
266            Self::None => self.insert_string(String::new()),
267            Self::DateTime(ref value) => self.insert_string(format!("{value:?}")),
268            Self::Time(ref value) => self.insert_string(format!("{value:?}")),
269            Self::Duration(ref value) => self.insert_string(format!("{value:?}")),
270            Self::Text(ref mut value) => value,
271            Self::Number(value) => self.insert_string(value.to_string()),
272            Self::Bool(value) => self.insert_string(value.to_string()),
273            Self::Files(ref files) => {
274                self.insert_string(fold_str(files.iter().map(|p| p.to_string_lossy())))
275            }
276            Self::Notes(ref notes) => self.insert_string(fold_str(notes.iter().map(|n| &n.text))),
277            Self::Table(ref table) => {
278                self.insert_string(serde_json::to_string(table).unwrap_or_else(|e| e.to_string()))
279            }
280            Self::GeoPos(ref geo_pos) => self.insert_string(geo_pos.to_string()),
281            Self::PlotRegions(ref regions) => {
282                self.insert_string(serde_json::to_string(regions).unwrap_or_else(|e| e.to_string()))
283            }
284        }
285    }
286
287    pub fn number_mut(&mut self, fallback: f64) -> &mut f64 {
288        match *self {
289            Self::Number(ref mut value) => value,
290            Self::Text(ref value) => {
291                let value = value.parse::<f64>().unwrap_or(fallback);
292                self.insert_number(value)
293            }
294            Self::Bool(value) => self.insert_number(if value { 1.0 } else { 0.0 }),
295            Self::None
296            | Self::Files(_)
297            | Self::Notes(_)
298            | Self::Table(_)
299            | Self::PlotRegions(_)
300            | Self::DateTime(_)
301            | Self::Time(_)
302            | Self::Duration(_)
303            | Self::GeoPos(_) => self.insert_number(fallback),
304        }
305    }
306
307    pub fn bool_mut(&mut self, fallback: bool) -> &mut bool {
308        match *self {
309            Self::Bool(ref mut value) => value,
310            Self::Text(ref value) => {
311                let value = value.to_lowercase().parse::<bool>().unwrap_or(fallback);
312                self.insert_bool(value)
313            }
314            Self::Number(value) => self.insert_bool(value != 0.0),
315            Self::None
316            | Self::Files(_)
317            | Self::Notes(_)
318            | Self::Table(_)
319            | Self::PlotRegions(_)
320            | Self::DateTime(_)
321            | Self::Time(_)
322            | Self::Duration(_)
323            | Self::GeoPos(_) => self.insert_bool(fallback),
324        }
325    }
326
327    pub fn files_mut(&mut self, fallback: impl FnOnce() -> Vec<PathBuf>) -> &mut Vec<PathBuf> {
328        match *self {
329            Self::Files(ref mut files) => files,
330            _ => self.insert_files(fallback()),
331        }
332    }
333
334    pub fn notes_mut(&mut self, fallback: impl FnOnce() -> Vec<Note>) -> &mut Vec<Note> {
335        match *self {
336            Self::Notes(ref mut notes) => notes,
337            _ => self.insert_notes(fallback()),
338        }
339    }
340
341    pub fn table_mut(&mut self, fallback: impl FnOnce() -> TableData) -> &mut TableData {
342        match *self {
343            Self::Table(ref mut table) => table,
344            _ => self.insert_table(fallback()),
345        }
346    }
347
348    pub fn geo_pos_mut(&mut self, fallback: impl FnOnce() -> GeoPos) -> &mut GeoPos {
349        match *self {
350            Self::GeoPos(ref mut geo_pos) => geo_pos,
351            _ => self.insert_geo_pos(fallback()),
352        }
353    }
354
355    pub fn as_bool(&self) -> Option<bool> {
356        match *self {
357            Self::Bool(value) => Some(value),
358            Self::Text(ref value) => value.to_lowercase().parse::<bool>().ok(),
359            Self::Number(value) => Some(value != 0.0),
360            Self::None
361            | Self::Files(_)
362            | Self::Notes(_)
363            | Self::Table(_)
364            | Self::PlotRegions(_)
365            | Self::DateTime(_)
366            | Self::Time(_)
367            | Self::Duration(_)
368            | Self::GeoPos(_) => None,
369        }
370    }
371
372    pub fn as_table(&self) -> Option<&TableData> {
373        match *self {
374            Self::Table(ref value) => Some(value),
375            _ => None,
376        }
377    }
378
379    pub fn as_str(&self) -> Option<Cow<'_, str>> {
380        match *self {
381            Self::Text(ref text) => Some(Cow::Borrowed(text)),
382            Self::None => Some(Cow::Borrowed("")),
383            Self::Bool(value) => Some(Cow::Owned(value.to_string())),
384            Self::Number(value) => Some(Cow::Owned(value.to_string())),
385            Self::DateTime(value) => Some(Cow::Owned(value.to_string())),
386            Self::Time(value) => Some(Cow::Owned(value.to_string())),
387            Self::Duration(value) => Some(Cow::Owned(format!("{value:?}"))),
388            Self::GeoPos(value) => Some(Cow::Owned(value.to_string())),
389            Self::Files(_) | Self::Notes(_) | Self::Table(_) | Self::PlotRegions(_) => None,
390        }
391    }
392
393    pub fn as_date_time(&self) -> Option<NaiveDateTime> {
394        match *self {
395            Self::DateTime(value) => Some(value),
396            Self::Text(ref text) => NaiveDateTime::parse_from_str(text, DATE_TIME_FORMAT).ok(),
397            Self::Number(timestamp) => chrono::DateTime::from_timestamp(timestamp as i64, 0)
398                .map(|dt| dt.with_timezone(&chrono::Local).naive_local()),
399            _ => None,
400        }
401    }
402
403    pub fn as_time(&self) -> Option<NaiveTime> {
404        match *self {
405            Self::Time(value) => Some(value),
406            Self::Text(ref text) => NaiveTime::parse_from_str(text, TIME_FORMAT).ok(),
407            Self::Number(timestamp) => chrono::DateTime::from_timestamp(timestamp as i64, 0)
408                .map(|dt| dt.with_timezone(&chrono::Local).naive_local().time()),
409            _ => None,
410        }
411    }
412
413    pub fn as_geo_pos(&self) -> Option<GeoPos> {
414        match *self {
415            Self::GeoPos(value) => Some(value),
416            _ => None,
417        }
418    }
419
420    pub fn as_plot_regions(&self) -> Option<&[PlotRegion]> {
421        match *self {
422            Self::PlotRegions(ref regions) => Some(regions),
423            _ => None,
424        }
425    }
426
427    pub fn insert_string(&mut self, value: String) -> &mut String {
428        *self = Self::Text(value);
429        match self {
430            Self::Text(value) => value,
431            _ => unreachable!(),
432        }
433    }
434    pub fn insert_number(&mut self, value: f64) -> &mut f64 {
435        *self = Self::Number(value);
436        match self {
437            Self::Number(value) => value,
438            _ => unreachable!(),
439        }
440    }
441    pub fn insert_bool(&mut self, value: bool) -> &mut bool {
442        *self = Self::Bool(value);
443        match self {
444            Self::Bool(value) => value,
445            _ => unreachable!(),
446        }
447    }
448    pub fn insert_files(&mut self, value: Vec<PathBuf>) -> &mut Vec<PathBuf> {
449        *self = Self::Files(value);
450        match self {
451            Self::Files(value) => value,
452            _ => unreachable!(),
453        }
454    }
455    pub fn insert_notes(&mut self, value: Vec<Note>) -> &mut Vec<Note> {
456        *self = Self::Notes(value);
457        match self {
458            Self::Notes(value) => value,
459            _ => unreachable!(),
460        }
461    }
462    pub fn insert_table(&mut self, value: TableData) -> &mut TableData {
463        *self = Self::Table(value);
464        match self {
465            Self::Table(value) => value,
466            _ => unreachable!(),
467        }
468    }
469    pub fn insert_datetime(&mut self, value: NaiveDateTime) -> &mut NaiveDateTime {
470        *self = Self::DateTime(value);
471        match self {
472            Self::DateTime(value) => value,
473            _ => unreachable!(),
474        }
475    }
476    pub fn insert_time(&mut self, value: NaiveTime) -> &mut NaiveTime {
477        *self = Self::Time(value);
478        match self {
479            Self::Time(value) => value,
480            _ => unreachable!(),
481        }
482    }
483    pub fn insert_geo_pos(&mut self, value: GeoPos) -> &mut GeoPos {
484        *self = Self::GeoPos(value);
485        match self {
486            Self::GeoPos(value) => value,
487            _ => unreachable!(),
488        }
489    }
490}
491
492impl From<String> for UserValue {
493    fn from(value: String) -> Self {
494        Self::Text(value)
495    }
496}
497
498impl From<f32> for UserValue {
499    fn from(value: f32) -> Self {
500        Self::Number(value.into())
501    }
502}
503
504impl From<f64> for UserValue {
505    fn from(value: f64) -> Self {
506        Self::Number(value)
507    }
508}
509
510impl From<bool> for UserValue {
511    fn from(value: bool) -> Self {
512        Self::Bool(value)
513    }
514}
515
516impl From<NaiveDateTime> for UserValue {
517    fn from(value: NaiveDateTime) -> Self {
518        Self::DateTime(value)
519    }
520}
521
522impl From<NaiveTime> for UserValue {
523    fn from(value: NaiveTime) -> Self {
524        Self::Time(value)
525    }
526}
527
528impl From<Duration> for UserValue {
529    fn from(value: Duration) -> Self {
530        Self::Duration(value)
531    }
532}
533
534impl From<GeoPos> for UserValue {
535    fn from(value: GeoPos) -> Self {
536        Self::GeoPos(value)
537    }
538}
539
540fn fold_str<S: Into<String> + AsRef<str>>(iter: impl IntoIterator<Item = S>) -> String {
541    iter.into_iter().fold(String::new(), |mut value, str| {
542        if value.is_empty() {
543            str.into()
544        } else {
545            value.push('\n');
546            value.push_str(str.as_ref());
547            value
548        }
549    })
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555
556    #[test]
557    fn serialize_none() {
558        assert_eq!(
559            serde_yaml::to_value(UserValue::None).unwrap(),
560            serde_yaml::Value::Null
561        );
562        assert_eq!(
563            UserValue::None,
564            serde_yaml::from_value(serde_yaml::Value::Null).unwrap(),
565        );
566    }
567
568    #[test]
569    fn serialize_date_time() {
570        let date_time =
571            NaiveDateTime::parse_from_str("2025-5-15T4:46:12", DATE_TIME_FORMAT).unwrap();
572
573        assert_eq!(
574            serde_yaml::to_value(UserValue::DateTime(date_time)).unwrap(),
575            serde_yaml::Value::String("2025-05-15T04:46:12".to_owned())
576        );
577        assert_eq!(
578            UserValue::DateTime(date_time),
579            serde_yaml::from_str("2025-05-15T04:46:12").unwrap()
580        );
581    }
582
583    #[test]
584    fn serialize_time() {
585        let date_time = NaiveTime::parse_from_str("04:46:12", TIME_FORMAT).unwrap();
586
587        assert_eq!(
588            serde_yaml::to_value(UserValue::Time(date_time)).unwrap(),
589            serde_yaml::Value::String("04:46:12".to_owned())
590        );
591        assert_eq!(
592            UserValue::Time(date_time),
593            serde_yaml::from_str("04:46:12").unwrap()
594        );
595    }
596
597    #[test]
598    #[should_panic = "data did not match any variant"]
599    fn deserialize_error() {
600        let _: UserValue = serde_yaml::from_value(serde_yaml::Value::Mapping(
601            serde_yaml::Mapping::from_iter([
602                (
603                    serde_yaml::Value::String("foo".into()),
604                    serde_yaml::Value::String("bar".into()),
605                ),
606                //
607            ]),
608        ))
609        .unwrap();
610    }
611}