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