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