Skip to main content

drasi_core/models/
element_value.rs

1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![allow(clippy::unwrap_used)]
16use super::ConversionError;
17
18use std::{
19    collections::BTreeMap,
20    ops::{Index, IndexMut},
21};
22
23use crate::evaluation::variable_value::{
24    float::Float, integer::Integer, zoned_datetime::ZonedDateTime as VarZonedDateTime,
25    VariableValue,
26};
27
28use std::sync::Arc;
29
30use chrono::{DateTime, FixedOffset, NaiveDateTime};
31use ordered_float::OrderedFloat;
32use serde::{Deserialize, Serialize};
33
34#[derive(Debug, Clone, Hash, Eq, PartialEq, Default, Serialize, Deserialize)]
35pub enum ElementValue {
36    #[default]
37    Null,
38    Bool(bool),
39    Float(OrderedFloat<f64>),
40    Integer(i64),
41    String(Arc<str>),
42    List(Vec<ElementValue>),
43    Object(ElementPropertyMap),
44    LocalDateTime(NaiveDateTime),
45    // ElementValue stores zoned datetimes as fixed offsets only; optional
46    // IANA timezone names from VariableValue are not represented here.
47    ZonedDateTime(DateTime<FixedOffset>),
48}
49
50impl From<&ElementPropertyMap> for VariableValue {
51    fn from(val: &ElementPropertyMap) -> Self {
52        let mut map = BTreeMap::new();
53        for (key, value) in val.values.iter() {
54            map.insert(key.to_string(), value.into());
55        }
56        VariableValue::Object(map)
57    }
58}
59
60impl From<&ElementValue> for VariableValue {
61    fn from(val: &ElementValue) -> Self {
62        match val {
63            ElementValue::Null => VariableValue::Null,
64            ElementValue::Bool(b) => VariableValue::Bool(*b),
65            ElementValue::Float(f) => VariableValue::Float(Float::from(f.0)),
66            ElementValue::Integer(i) => VariableValue::Integer(Integer::from(*i)),
67            ElementValue::String(s) => VariableValue::String(s.to_string()),
68            ElementValue::List(l) => VariableValue::List(l.iter().map(|x| x.into()).collect()),
69            ElementValue::Object(o) => o.into(),
70            ElementValue::LocalDateTime(dt) => VariableValue::LocalDateTime(*dt),
71            ElementValue::ZonedDateTime(dt) => {
72                // ElementValue cannot carry IANA timezone names, so we emit None.
73                VariableValue::ZonedDateTime(VarZonedDateTime::new(*dt, None))
74            }
75        }
76    }
77}
78
79impl TryInto<ElementValue> for &VariableValue {
80    type Error = ConversionError;
81
82    fn try_into(self) -> Result<ElementValue, ConversionError> {
83        match self {
84            VariableValue::Null => Ok(ElementValue::Null),
85            VariableValue::Bool(b) => Ok(ElementValue::Bool(*b)),
86            VariableValue::Float(f) => Ok(ElementValue::Float(OrderedFloat(
87                f.as_f64().unwrap_or_default(),
88            ))),
89            VariableValue::Integer(i) => Ok(ElementValue::Integer(i.as_i64().unwrap_or_default())),
90            VariableValue::String(s) => Ok(ElementValue::String(Arc::from(s.as_str()))),
91            VariableValue::List(l) => Ok(ElementValue::List(
92                l.iter().map(|x| x.try_into().unwrap_or_default()).collect(),
93            )),
94            VariableValue::Object(o) => Ok(ElementValue::Object(o.into())),
95            VariableValue::LocalDateTime(dt) => Ok(ElementValue::LocalDateTime(*dt)),
96            // Preserve the fixed-offset instant; timezone_name metadata is dropped.
97            VariableValue::ZonedDateTime(zdt) => Ok(ElementValue::ZonedDateTime(*zdt.datetime())),
98            _ => Err(ConversionError {}),
99        }
100    }
101}
102
103impl TryInto<ElementValue> for VariableValue {
104    type Error = ConversionError;
105
106    fn try_into(self) -> Result<ElementValue, ConversionError> {
107        match self {
108            VariableValue::Null => Ok(ElementValue::Null),
109            VariableValue::Bool(b) => Ok(ElementValue::Bool(b)),
110            VariableValue::Float(f) => Ok(ElementValue::Float(OrderedFloat(
111                f.as_f64().unwrap_or_default(),
112            ))),
113            VariableValue::Integer(i) => Ok(ElementValue::Integer(i.as_i64().unwrap_or_default())),
114            VariableValue::String(s) => Ok(ElementValue::String(Arc::from(s.as_str()))),
115            VariableValue::List(l) => Ok(ElementValue::List(
116                l.iter().map(|x| x.try_into().unwrap_or_default()).collect(),
117            )),
118            VariableValue::Object(o) => Ok(ElementValue::Object(o.into())),
119            VariableValue::LocalDateTime(dt) => Ok(ElementValue::LocalDateTime(dt)),
120            // Preserve the fixed-offset instant; timezone_name metadata is dropped.
121            VariableValue::ZonedDateTime(zdt) => Ok(ElementValue::ZonedDateTime(*zdt.datetime())),
122            _ => Err(ConversionError {}),
123        }
124    }
125}
126
127/// Tag used inside JSON envelopes to preserve `ElementValue` datetime type
128/// information across serialization round-trips.
129/// This key is intentionally specific to minimize collisions with user data.
130/// This envelope is for internal round-trips, not an external API shape.
131const DRASI_TYPE_TAG: &str = "__drasi_v1_type__";
132const DRASI_ENVELOPE_MARKER_TAG: &str = "__drasi_v1_envelope__";
133const DRASI_ENVELOPE_MARKER_VALUE: &str = "drasi.element_value.datetime";
134
135impl From<&ElementValue> for serde_json::Value {
136    fn from(val: &ElementValue) -> Self {
137        match val {
138            ElementValue::Null => serde_json::Value::Null,
139            ElementValue::Bool(b) => serde_json::Value::Bool(*b),
140            ElementValue::Float(f) => {
141                serde_json::Value::Number(serde_json::Number::from_f64(f.into_inner()).unwrap())
142            }
143            ElementValue::Integer(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
144            ElementValue::String(s) => serde_json::Value::String(s.to_string()),
145            ElementValue::List(l) => serde_json::Value::Array(l.iter().map(|x| x.into()).collect()),
146            ElementValue::Object(o) => serde_json::Value::Object(o.into()),
147            ElementValue::LocalDateTime(dt) => {
148                serde_json::json!({
149                    DRASI_ENVELOPE_MARKER_TAG: DRASI_ENVELOPE_MARKER_VALUE,
150                    DRASI_TYPE_TAG: "drasi.LocalDateTime",
151                    "value": dt.to_string()
152                })
153            }
154            ElementValue::ZonedDateTime(dt) => {
155                serde_json::json!({
156                    DRASI_ENVELOPE_MARKER_TAG: DRASI_ENVELOPE_MARKER_VALUE,
157                    DRASI_TYPE_TAG: "drasi.ZonedDateTime",
158                    "value": dt.to_rfc3339()
159                })
160            }
161        }
162    }
163}
164
165impl From<&serde_json::Value> for ElementValue {
166    fn from(value: &serde_json::Value) -> Self {
167        match value {
168            serde_json::Value::Null => ElementValue::Null,
169            serde_json::Value::Bool(b) => ElementValue::Bool(*b),
170            serde_json::Value::Number(n) => {
171                if let Some(i) = n.as_i64() {
172                    ElementValue::Integer(i)
173                } else if let Some(f) = n.as_f64() {
174                    ElementValue::Float(OrderedFloat(f))
175                } else {
176                    ElementValue::Null
177                }
178            }
179            serde_json::Value::String(s) => ElementValue::String(Arc::from(s.as_str())),
180            serde_json::Value::Array(a) => ElementValue::List(a.iter().map(|x| x.into()).collect()),
181            serde_json::Value::Object(o) => {
182                // Check for tagged datetime envelope
183                let has_internal_marker = o.get(DRASI_ENVELOPE_MARKER_TAG).and_then(|v| v.as_str())
184                    == Some(DRASI_ENVELOPE_MARKER_VALUE);
185                if has_internal_marker {
186                    if let Some(type_tag) = o.get(DRASI_TYPE_TAG).and_then(|v| v.as_str()) {
187                        if let Some(val_str) = o.get("value").and_then(|v| v.as_str()) {
188                            match type_tag {
189                                "drasi.LocalDateTime" => {
190                                    if let Ok(dt) = NaiveDateTime::parse_from_str(
191                                        val_str,
192                                        "%Y-%m-%d %H:%M:%S%.f",
193                                    ) {
194                                        return ElementValue::LocalDateTime(dt);
195                                    }
196                                }
197                                "drasi.ZonedDateTime" => {
198                                    if let Ok(dt) = DateTime::parse_from_rfc3339(val_str) {
199                                        return ElementValue::ZonedDateTime(dt);
200                                    }
201                                }
202                                _ => {}
203                            }
204                        }
205                    }
206                }
207                ElementValue::Object(o.into())
208            }
209        }
210    }
211}
212
213impl TryInto<ElementPropertyMap> for &VariableValue {
214    type Error = ConversionError;
215
216    fn try_into(self) -> Result<ElementPropertyMap, ConversionError> {
217        match self {
218            VariableValue::Object(o) => {
219                let mut values = BTreeMap::new();
220                for (key, value) in o.iter() {
221                    values.insert(Arc::from(key.as_str()), value.try_into()?);
222                }
223                Ok(ElementPropertyMap { values })
224            }
225            _ => Err(ConversionError {}),
226        }
227    }
228}
229
230#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
231pub struct ElementPropertyMap {
232    values: BTreeMap<Arc<str>, ElementValue>,
233}
234
235impl Default for ElementPropertyMap {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241impl ElementPropertyMap {
242    pub fn new() -> Self {
243        ElementPropertyMap {
244            values: BTreeMap::new(),
245        }
246    }
247
248    pub fn get(&self, key: &str) -> Option<&ElementValue> {
249        self.values.get(key)
250    }
251
252    pub fn insert(&mut self, key: &str, value: ElementValue) {
253        self.values.insert(Arc::from(key), value);
254    }
255
256    pub fn merge(&mut self, other: &ElementPropertyMap) {
257        for (key, value) in other.values.iter() {
258            self.values
259                .entry(key.clone())
260                .or_insert_with(|| value.clone());
261        }
262    }
263
264    pub fn map_iter<T>(
265        &self,
266        f: impl Fn(&Arc<str>, &ElementValue) -> T + 'static,
267    ) -> impl Iterator<Item = T> + '_ {
268        self.values.iter().map(move |(k, v)| f(k, v))
269    }
270}
271
272impl Index<&str> for ElementPropertyMap {
273    type Output = ElementValue;
274
275    fn index(&self, key: &str) -> &Self::Output {
276        static NULL: ElementValue = ElementValue::Null;
277        match self.values.get(key) {
278            Some(value) => value,
279            None => &NULL,
280        }
281    }
282}
283
284impl IndexMut<&str> for ElementPropertyMap {
285    fn index_mut(&mut self, key: &str) -> &mut Self::Output {
286        self.values
287            .entry(Arc::from(key))
288            .or_insert_with(|| ElementValue::Null)
289    }
290}
291
292impl From<&BTreeMap<String, VariableValue>> for ElementPropertyMap {
293    fn from(map: &BTreeMap<String, VariableValue>) -> Self {
294        let mut values = BTreeMap::new();
295        for (key, value) in map.iter() {
296            values.insert(Arc::from(key.as_str()), value.try_into().unwrap());
297        }
298        ElementPropertyMap { values }
299    }
300}
301
302impl From<BTreeMap<String, VariableValue>> for ElementPropertyMap {
303    fn from(map: BTreeMap<String, VariableValue>) -> Self {
304        let mut values = BTreeMap::new();
305        for (key, value) in map {
306            values.insert(Arc::from(key.as_str()), value.try_into().unwrap());
307        }
308        ElementPropertyMap { values }
309    }
310}
311
312impl From<BTreeMap<String, ElementValue>> for ElementPropertyMap {
313    fn from(map: BTreeMap<String, ElementValue>) -> Self {
314        let mut values = BTreeMap::new();
315        for (key, value) in map {
316            values.insert(Arc::from(key.as_str()), value);
317        }
318        ElementPropertyMap { values }
319    }
320}
321
322impl From<&ElementPropertyMap> for serde_json::Map<String, serde_json::Value> {
323    fn from(val: &ElementPropertyMap) -> Self {
324        val.values
325            .iter()
326            .map(|(k, v)| (k.to_string(), v.into()))
327            .collect()
328    }
329}
330
331impl From<&serde_json::Map<String, serde_json::Value>> for ElementPropertyMap {
332    fn from(map: &serde_json::Map<String, serde_json::Value>) -> Self {
333        let mut values = BTreeMap::new();
334        for (key, value) in map.iter() {
335            values.insert(Arc::from(key.as_str()), value.into());
336        }
337        ElementPropertyMap { values }
338    }
339}
340
341impl From<serde_json::Value> for ElementPropertyMap {
342    fn from(value: serde_json::Value) -> Self {
343        match value {
344            serde_json::Value::Object(o) => (&o).into(),
345            _ => ElementPropertyMap::new(),
346        }
347    }
348}
349
350impl From<&serde_json::Value> for ElementPropertyMap {
351    fn from(value: &serde_json::Value) -> Self {
352        match value {
353            serde_json::Value::Object(o) => o.into(),
354            _ => ElementPropertyMap::new(),
355        }
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use crate::evaluation::variable_value::{
363        float::Float, integer::Integer, zoned_datetime::ZonedDateTime as VarZonedDateTime,
364        VariableValue,
365    };
366    use chrono::{NaiveDate, TimeZone, Utc};
367
368    fn sample_naive_datetime() -> NaiveDateTime {
369        NaiveDate::from_ymd_opt(2024, 6, 15)
370            .unwrap()
371            .and_hms_opt(10, 30, 45)
372            .unwrap()
373    }
374
375    fn sample_fixed_datetime() -> DateTime<FixedOffset> {
376        let offset = FixedOffset::east_opt(3600).unwrap(); // +01:00
377        offset.with_ymd_and_hms(2024, 6, 15, 10, 30, 45).unwrap()
378    }
379
380    // ── ElementValue → VariableValue ────────────────────────────────────
381
382    #[test]
383    fn local_datetime_to_variable_value() {
384        let dt = sample_naive_datetime();
385        let ev = ElementValue::LocalDateTime(dt);
386        let vv: VariableValue = (&ev).into();
387        assert_eq!(vv, VariableValue::LocalDateTime(dt));
388    }
389
390    #[test]
391    fn zoned_datetime_to_variable_value() {
392        let dt = sample_fixed_datetime();
393        let ev = ElementValue::ZonedDateTime(dt);
394        let vv: VariableValue = (&ev).into();
395        let expected = VariableValue::ZonedDateTime(VarZonedDateTime::new(dt, None));
396        assert_eq!(vv, expected);
397    }
398
399    // ── VariableValue → ElementValue (by ref) ──────────────────────────
400
401    #[test]
402    fn variable_value_ref_local_datetime_to_element_value() {
403        let dt = sample_naive_datetime();
404        let vv = VariableValue::LocalDateTime(dt);
405        let ev: ElementValue = (&vv).try_into().unwrap();
406        assert_eq!(ev, ElementValue::LocalDateTime(dt));
407    }
408
409    #[test]
410    fn variable_value_ref_zoned_datetime_to_element_value() {
411        let dt = sample_fixed_datetime();
412        let vv = VariableValue::ZonedDateTime(VarZonedDateTime::new(dt, None));
413        let ev: ElementValue = (&vv).try_into().unwrap();
414        assert_eq!(ev, ElementValue::ZonedDateTime(dt));
415    }
416
417    // ── VariableValue → ElementValue (by value) ────────────────────────
418
419    #[test]
420    fn variable_value_owned_local_datetime_to_element_value() {
421        let dt = sample_naive_datetime();
422        let vv = VariableValue::LocalDateTime(dt);
423        let ev: ElementValue = vv.try_into().unwrap();
424        assert_eq!(ev, ElementValue::LocalDateTime(dt));
425    }
426
427    #[test]
428    fn variable_value_owned_zoned_datetime_to_element_value() {
429        let dt = sample_fixed_datetime();
430        let vv = VariableValue::ZonedDateTime(VarZonedDateTime::new(dt, None));
431        let ev: ElementValue = vv.try_into().unwrap();
432        assert_eq!(ev, ElementValue::ZonedDateTime(dt));
433    }
434
435    // ── ElementValue → serde_json::Value ───────────────────────────────
436
437    #[test]
438    fn local_datetime_to_json() {
439        let dt = sample_naive_datetime();
440        let ev = ElementValue::LocalDateTime(dt);
441        let json: serde_json::Value = (&ev).into();
442        assert_eq!(
443            json,
444            serde_json::json!({
445                "__drasi_v1_envelope__": "drasi.element_value.datetime",
446                "__drasi_v1_type__": "drasi.LocalDateTime",
447                "value": dt.to_string()
448            })
449        );
450    }
451
452    #[test]
453    fn zoned_datetime_to_json() {
454        let dt = sample_fixed_datetime();
455        let ev = ElementValue::ZonedDateTime(dt);
456        let json: serde_json::Value = (&ev).into();
457        assert_eq!(
458            json,
459            serde_json::json!({
460                "__drasi_v1_envelope__": "drasi.element_value.datetime",
461                "__drasi_v1_type__": "drasi.ZonedDateTime",
462                "value": dt.to_rfc3339()
463            })
464        );
465    }
466
467    // ── Round-trip: ElementValue → VariableValue → ElementValue ────────
468
469    #[test]
470    fn roundtrip_local_datetime() {
471        let dt = sample_naive_datetime();
472        let original = ElementValue::LocalDateTime(dt);
473        let vv: VariableValue = (&original).into();
474        let recovered: ElementValue = vv.try_into().unwrap();
475        assert_eq!(original, recovered);
476    }
477
478    #[test]
479    fn roundtrip_zoned_datetime() {
480        let dt = sample_fixed_datetime();
481        let original = ElementValue::ZonedDateTime(dt);
482        let vv: VariableValue = (&original).into();
483        let recovered: ElementValue = vv.try_into().unwrap();
484        assert_eq!(original, recovered);
485    }
486
487    // ── JSON round-trip (note: JSON strings do NOT auto-parse to datetime) ──
488
489    #[test]
490    fn json_string_does_not_become_datetime() {
491        // A JSON string that looks like a timestamp should remain an
492        // ElementValue::String, not magically become LocalDateTime.
493        let json = serde_json::Value::String("2024-06-15 10:30:45".to_string());
494        let ev: ElementValue = (&json).into();
495        assert!(matches!(ev, ElementValue::String(_)));
496    }
497
498    // ── JSON round-trip via tagged envelope ─────────────────────────────
499
500    #[test]
501    fn json_roundtrip_local_datetime() {
502        let dt = sample_naive_datetime();
503        let original = ElementValue::LocalDateTime(dt);
504        let json: serde_json::Value = (&original).into();
505        let recovered: ElementValue = (&json).into();
506        assert_eq!(original, recovered);
507    }
508
509    #[test]
510    fn json_roundtrip_zoned_datetime() {
511        let dt = sample_fixed_datetime();
512        let original = ElementValue::ZonedDateTime(dt);
513        let json: serde_json::Value = (&original).into();
514        let recovered: ElementValue = (&json).into();
515        assert_eq!(original, recovered);
516    }
517
518    #[test]
519    fn json_roundtrip_zoned_datetime_utc() {
520        let dt = Utc
521            .with_ymd_and_hms(2024, 1, 1, 0, 0, 0)
522            .unwrap()
523            .fixed_offset();
524        let original = ElementValue::ZonedDateTime(dt);
525        let json: serde_json::Value = (&original).into();
526        let recovered: ElementValue = (&json).into();
527        assert_eq!(original, recovered);
528    }
529
530    #[test]
531    fn tagged_object_with_unknown_type_stays_object() {
532        // An object with the internal tag set to an unknown type should
533        // be deserialized as a regular Object, not a datetime.
534        let json = serde_json::json!({
535            "__drasi_v1_envelope__": "drasi.element_value.datetime",
536            "__drasi_v1_type__": "UnknownType",
537            "value": "some-value"
538        });
539        let ev: ElementValue = (&json).into();
540        assert!(matches!(ev, ElementValue::Object(_)));
541    }
542
543    #[test]
544    fn tagged_object_with_invalid_datetime_stays_object() {
545        // A tagged envelope with an unparseable value should fall back
546        // to Object rather than panic.
547        let json = serde_json::json!({
548            "__drasi_v1_envelope__": "drasi.element_value.datetime",
549            "__drasi_v1_type__": "drasi.ZonedDateTime",
550            "value": "not-a-datetime"
551        });
552        let ev: ElementValue = (&json).into();
553        assert!(matches!(ev, ElementValue::Object(_)));
554    }
555
556    #[test]
557    fn object_with_common_type_key_does_not_get_interpreted_as_datetime() {
558        // User objects using a common `_type` key must not collide with internal tags.
559        let json = serde_json::json!({
560            "_type": "drasi.LocalDateTime",
561            "value": "2024-06-15 10:30:45",
562            "timezone_name": "UTC"
563        });
564        let ev: ElementValue = (&json).into();
565        assert!(matches!(ev, ElementValue::Object(_)));
566    }
567
568    #[test]
569    fn object_with_internal_type_but_no_marker_stays_object() {
570        // Objects that happen to contain the internal type key are not interpreted
571        // as datetime envelopes unless the dedicated marker is also present.
572        let json = serde_json::json!({
573            "__drasi_v1_type__": "drasi.ZonedDateTime",
574            "value": "1970-01-01T00:00:00+00:00"
575        });
576        let ev: ElementValue = (&json).into();
577        assert!(matches!(ev, ElementValue::Object(_)));
578    }
579
580    // ── Other variants still work ──────────────────────────────────────
581
582    #[test]
583    fn null_roundtrip() {
584        let ev = ElementValue::Null;
585        let vv: VariableValue = (&ev).into();
586        assert_eq!(vv, VariableValue::Null);
587        let recovered: ElementValue = vv.try_into().unwrap();
588        assert_eq!(recovered, ElementValue::Null);
589    }
590
591    #[test]
592    fn bool_roundtrip() {
593        let ev = ElementValue::Bool(true);
594        let vv: VariableValue = (&ev).into();
595        assert_eq!(vv, VariableValue::Bool(true));
596    }
597
598    #[test]
599    fn integer_roundtrip() {
600        let ev = ElementValue::Integer(42);
601        let vv: VariableValue = (&ev).into();
602        assert_eq!(vv, VariableValue::Integer(Integer::from(42i64)));
603    }
604
605    #[test]
606    fn zoned_datetime_utc() {
607        let dt = Utc
608            .with_ymd_and_hms(2024, 1, 1, 0, 0, 0)
609            .unwrap()
610            .fixed_offset();
611        let ev = ElementValue::ZonedDateTime(dt);
612        let vv: VariableValue = (&ev).into();
613        let recovered: ElementValue = vv.try_into().unwrap();
614        assert_eq!(recovered, ev);
615    }
616}