#![allow(clippy::unwrap_used)]
use super::ConversionError;
use std::{
collections::BTreeMap,
ops::{Index, IndexMut},
};
use crate::evaluation::variable_value::{
float::Float, integer::Integer, zoned_datetime::ZonedDateTime as VarZonedDateTime,
VariableValue,
};
use std::sync::Arc;
use chrono::{DateTime, FixedOffset, NaiveDateTime};
use ordered_float::OrderedFloat;
#[derive(Debug, Clone, Hash, Eq, PartialEq, Default)]
pub enum ElementValue {
#[default]
Null,
Bool(bool),
Float(OrderedFloat<f64>),
Integer(i64),
String(Arc<str>),
List(Vec<ElementValue>),
Object(ElementPropertyMap),
LocalDateTime(NaiveDateTime),
ZonedDateTime(DateTime<FixedOffset>),
}
impl From<&ElementPropertyMap> for VariableValue {
fn from(val: &ElementPropertyMap) -> Self {
let mut map = BTreeMap::new();
for (key, value) in val.values.iter() {
map.insert(key.to_string(), value.into());
}
VariableValue::Object(map)
}
}
impl From<&ElementValue> for VariableValue {
fn from(val: &ElementValue) -> Self {
match val {
ElementValue::Null => VariableValue::Null,
ElementValue::Bool(b) => VariableValue::Bool(*b),
ElementValue::Float(f) => VariableValue::Float(Float::from(f.0)),
ElementValue::Integer(i) => VariableValue::Integer(Integer::from(*i)),
ElementValue::String(s) => VariableValue::String(s.to_string()),
ElementValue::List(l) => VariableValue::List(l.iter().map(|x| x.into()).collect()),
ElementValue::Object(o) => o.into(),
ElementValue::LocalDateTime(dt) => VariableValue::LocalDateTime(*dt),
ElementValue::ZonedDateTime(dt) => {
VariableValue::ZonedDateTime(VarZonedDateTime::new(*dt, None))
}
}
}
}
impl TryInto<ElementValue> for &VariableValue {
type Error = ConversionError;
fn try_into(self) -> Result<ElementValue, ConversionError> {
match self {
VariableValue::Null => Ok(ElementValue::Null),
VariableValue::Bool(b) => Ok(ElementValue::Bool(*b)),
VariableValue::Float(f) => Ok(ElementValue::Float(OrderedFloat(
f.as_f64().unwrap_or_default(),
))),
VariableValue::Integer(i) => Ok(ElementValue::Integer(i.as_i64().unwrap_or_default())),
VariableValue::String(s) => Ok(ElementValue::String(Arc::from(s.as_str()))),
VariableValue::List(l) => Ok(ElementValue::List(
l.iter().map(|x| x.try_into().unwrap_or_default()).collect(),
)),
VariableValue::Object(o) => Ok(ElementValue::Object(o.into())),
VariableValue::LocalDateTime(dt) => Ok(ElementValue::LocalDateTime(*dt)),
VariableValue::ZonedDateTime(zdt) => Ok(ElementValue::ZonedDateTime(*zdt.datetime())),
_ => Err(ConversionError {}),
}
}
}
impl TryInto<ElementValue> for VariableValue {
type Error = ConversionError;
fn try_into(self) -> Result<ElementValue, ConversionError> {
match self {
VariableValue::Null => Ok(ElementValue::Null),
VariableValue::Bool(b) => Ok(ElementValue::Bool(b)),
VariableValue::Float(f) => Ok(ElementValue::Float(OrderedFloat(
f.as_f64().unwrap_or_default(),
))),
VariableValue::Integer(i) => Ok(ElementValue::Integer(i.as_i64().unwrap_or_default())),
VariableValue::String(s) => Ok(ElementValue::String(Arc::from(s.as_str()))),
VariableValue::List(l) => Ok(ElementValue::List(
l.iter().map(|x| x.try_into().unwrap_or_default()).collect(),
)),
VariableValue::Object(o) => Ok(ElementValue::Object(o.into())),
VariableValue::LocalDateTime(dt) => Ok(ElementValue::LocalDateTime(dt)),
VariableValue::ZonedDateTime(zdt) => Ok(ElementValue::ZonedDateTime(*zdt.datetime())),
_ => Err(ConversionError {}),
}
}
}
const DRASI_TYPE_TAG: &str = "__drasi_v1_type__";
const DRASI_ENVELOPE_MARKER_TAG: &str = "__drasi_v1_envelope__";
const DRASI_ENVELOPE_MARKER_VALUE: &str = "drasi.element_value.datetime";
impl From<&ElementValue> for serde_json::Value {
fn from(val: &ElementValue) -> Self {
match val {
ElementValue::Null => serde_json::Value::Null,
ElementValue::Bool(b) => serde_json::Value::Bool(*b),
ElementValue::Float(f) => {
serde_json::Value::Number(serde_json::Number::from_f64(f.into_inner()).unwrap())
}
ElementValue::Integer(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
ElementValue::String(s) => serde_json::Value::String(s.to_string()),
ElementValue::List(l) => serde_json::Value::Array(l.iter().map(|x| x.into()).collect()),
ElementValue::Object(o) => serde_json::Value::Object(o.into()),
ElementValue::LocalDateTime(dt) => {
serde_json::json!({
DRASI_ENVELOPE_MARKER_TAG: DRASI_ENVELOPE_MARKER_VALUE,
DRASI_TYPE_TAG: "drasi.LocalDateTime",
"value": dt.to_string()
})
}
ElementValue::ZonedDateTime(dt) => {
serde_json::json!({
DRASI_ENVELOPE_MARKER_TAG: DRASI_ENVELOPE_MARKER_VALUE,
DRASI_TYPE_TAG: "drasi.ZonedDateTime",
"value": dt.to_rfc3339()
})
}
}
}
}
impl From<&serde_json::Value> for ElementValue {
fn from(value: &serde_json::Value) -> Self {
match value {
serde_json::Value::Null => ElementValue::Null,
serde_json::Value::Bool(b) => ElementValue::Bool(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
ElementValue::Integer(i)
} else if let Some(f) = n.as_f64() {
ElementValue::Float(OrderedFloat(f))
} else {
ElementValue::Null
}
}
serde_json::Value::String(s) => ElementValue::String(Arc::from(s.as_str())),
serde_json::Value::Array(a) => ElementValue::List(a.iter().map(|x| x.into()).collect()),
serde_json::Value::Object(o) => {
let has_internal_marker = o.get(DRASI_ENVELOPE_MARKER_TAG).and_then(|v| v.as_str())
== Some(DRASI_ENVELOPE_MARKER_VALUE);
if has_internal_marker {
if let Some(type_tag) = o.get(DRASI_TYPE_TAG).and_then(|v| v.as_str()) {
if let Some(val_str) = o.get("value").and_then(|v| v.as_str()) {
match type_tag {
"drasi.LocalDateTime" => {
if let Ok(dt) = NaiveDateTime::parse_from_str(
val_str,
"%Y-%m-%d %H:%M:%S%.f",
) {
return ElementValue::LocalDateTime(dt);
}
}
"drasi.ZonedDateTime" => {
if let Ok(dt) = DateTime::parse_from_rfc3339(val_str) {
return ElementValue::ZonedDateTime(dt);
}
}
_ => {}
}
}
}
}
ElementValue::Object(o.into())
}
}
}
}
impl TryInto<ElementPropertyMap> for &VariableValue {
type Error = ConversionError;
fn try_into(self) -> Result<ElementPropertyMap, ConversionError> {
match self {
VariableValue::Object(o) => {
let mut values = BTreeMap::new();
for (key, value) in o.iter() {
values.insert(Arc::from(key.as_str()), value.try_into()?);
}
Ok(ElementPropertyMap { values })
}
_ => Err(ConversionError {}),
}
}
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct ElementPropertyMap {
values: BTreeMap<Arc<str>, ElementValue>,
}
impl Default for ElementPropertyMap {
fn default() -> Self {
Self::new()
}
}
impl ElementPropertyMap {
pub fn new() -> Self {
ElementPropertyMap {
values: BTreeMap::new(),
}
}
pub fn get(&self, key: &str) -> Option<&ElementValue> {
self.values.get(key)
}
pub fn insert(&mut self, key: &str, value: ElementValue) {
self.values.insert(Arc::from(key), value);
}
pub fn merge(&mut self, other: &ElementPropertyMap) {
for (key, value) in other.values.iter() {
self.values
.entry(key.clone())
.or_insert_with(|| value.clone());
}
}
pub fn map_iter<T>(
&self,
f: impl Fn(&Arc<str>, &ElementValue) -> T + 'static,
) -> impl Iterator<Item = T> + '_ {
self.values.iter().map(move |(k, v)| f(k, v))
}
}
impl Index<&str> for ElementPropertyMap {
type Output = ElementValue;
fn index(&self, key: &str) -> &Self::Output {
static NULL: ElementValue = ElementValue::Null;
match self.values.get(key) {
Some(value) => value,
None => &NULL,
}
}
}
impl IndexMut<&str> for ElementPropertyMap {
fn index_mut(&mut self, key: &str) -> &mut Self::Output {
self.values
.entry(Arc::from(key))
.or_insert_with(|| ElementValue::Null)
}
}
impl From<&BTreeMap<String, VariableValue>> for ElementPropertyMap {
fn from(map: &BTreeMap<String, VariableValue>) -> Self {
let mut values = BTreeMap::new();
for (key, value) in map.iter() {
values.insert(Arc::from(key.as_str()), value.try_into().unwrap());
}
ElementPropertyMap { values }
}
}
impl From<BTreeMap<String, VariableValue>> for ElementPropertyMap {
fn from(map: BTreeMap<String, VariableValue>) -> Self {
let mut values = BTreeMap::new();
for (key, value) in map {
values.insert(Arc::from(key.as_str()), value.try_into().unwrap());
}
ElementPropertyMap { values }
}
}
impl From<BTreeMap<String, ElementValue>> for ElementPropertyMap {
fn from(map: BTreeMap<String, ElementValue>) -> Self {
let mut values = BTreeMap::new();
for (key, value) in map {
values.insert(Arc::from(key.as_str()), value);
}
ElementPropertyMap { values }
}
}
impl From<&ElementPropertyMap> for serde_json::Map<String, serde_json::Value> {
fn from(val: &ElementPropertyMap) -> Self {
val.values
.iter()
.map(|(k, v)| (k.to_string(), v.into()))
.collect()
}
}
impl From<&serde_json::Map<String, serde_json::Value>> for ElementPropertyMap {
fn from(map: &serde_json::Map<String, serde_json::Value>) -> Self {
let mut values = BTreeMap::new();
for (key, value) in map.iter() {
values.insert(Arc::from(key.as_str()), value.into());
}
ElementPropertyMap { values }
}
}
impl From<serde_json::Value> for ElementPropertyMap {
fn from(value: serde_json::Value) -> Self {
match value {
serde_json::Value::Object(o) => (&o).into(),
_ => ElementPropertyMap::new(),
}
}
}
impl From<&serde_json::Value> for ElementPropertyMap {
fn from(value: &serde_json::Value) -> Self {
match value {
serde_json::Value::Object(o) => o.into(),
_ => ElementPropertyMap::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::evaluation::variable_value::{
float::Float, integer::Integer, zoned_datetime::ZonedDateTime as VarZonedDateTime,
VariableValue,
};
use chrono::{NaiveDate, TimeZone, Utc};
fn sample_naive_datetime() -> NaiveDateTime {
NaiveDate::from_ymd_opt(2024, 6, 15)
.unwrap()
.and_hms_opt(10, 30, 45)
.unwrap()
}
fn sample_fixed_datetime() -> DateTime<FixedOffset> {
let offset = FixedOffset::east_opt(3600).unwrap(); offset.with_ymd_and_hms(2024, 6, 15, 10, 30, 45).unwrap()
}
#[test]
fn local_datetime_to_variable_value() {
let dt = sample_naive_datetime();
let ev = ElementValue::LocalDateTime(dt);
let vv: VariableValue = (&ev).into();
assert_eq!(vv, VariableValue::LocalDateTime(dt));
}
#[test]
fn zoned_datetime_to_variable_value() {
let dt = sample_fixed_datetime();
let ev = ElementValue::ZonedDateTime(dt);
let vv: VariableValue = (&ev).into();
let expected = VariableValue::ZonedDateTime(VarZonedDateTime::new(dt, None));
assert_eq!(vv, expected);
}
#[test]
fn variable_value_ref_local_datetime_to_element_value() {
let dt = sample_naive_datetime();
let vv = VariableValue::LocalDateTime(dt);
let ev: ElementValue = (&vv).try_into().unwrap();
assert_eq!(ev, ElementValue::LocalDateTime(dt));
}
#[test]
fn variable_value_ref_zoned_datetime_to_element_value() {
let dt = sample_fixed_datetime();
let vv = VariableValue::ZonedDateTime(VarZonedDateTime::new(dt, None));
let ev: ElementValue = (&vv).try_into().unwrap();
assert_eq!(ev, ElementValue::ZonedDateTime(dt));
}
#[test]
fn variable_value_owned_local_datetime_to_element_value() {
let dt = sample_naive_datetime();
let vv = VariableValue::LocalDateTime(dt);
let ev: ElementValue = vv.try_into().unwrap();
assert_eq!(ev, ElementValue::LocalDateTime(dt));
}
#[test]
fn variable_value_owned_zoned_datetime_to_element_value() {
let dt = sample_fixed_datetime();
let vv = VariableValue::ZonedDateTime(VarZonedDateTime::new(dt, None));
let ev: ElementValue = vv.try_into().unwrap();
assert_eq!(ev, ElementValue::ZonedDateTime(dt));
}
#[test]
fn local_datetime_to_json() {
let dt = sample_naive_datetime();
let ev = ElementValue::LocalDateTime(dt);
let json: serde_json::Value = (&ev).into();
assert_eq!(
json,
serde_json::json!({
"__drasi_v1_envelope__": "drasi.element_value.datetime",
"__drasi_v1_type__": "drasi.LocalDateTime",
"value": dt.to_string()
})
);
}
#[test]
fn zoned_datetime_to_json() {
let dt = sample_fixed_datetime();
let ev = ElementValue::ZonedDateTime(dt);
let json: serde_json::Value = (&ev).into();
assert_eq!(
json,
serde_json::json!({
"__drasi_v1_envelope__": "drasi.element_value.datetime",
"__drasi_v1_type__": "drasi.ZonedDateTime",
"value": dt.to_rfc3339()
})
);
}
#[test]
fn roundtrip_local_datetime() {
let dt = sample_naive_datetime();
let original = ElementValue::LocalDateTime(dt);
let vv: VariableValue = (&original).into();
let recovered: ElementValue = vv.try_into().unwrap();
assert_eq!(original, recovered);
}
#[test]
fn roundtrip_zoned_datetime() {
let dt = sample_fixed_datetime();
let original = ElementValue::ZonedDateTime(dt);
let vv: VariableValue = (&original).into();
let recovered: ElementValue = vv.try_into().unwrap();
assert_eq!(original, recovered);
}
#[test]
fn json_string_does_not_become_datetime() {
let json = serde_json::Value::String("2024-06-15 10:30:45".to_string());
let ev: ElementValue = (&json).into();
assert!(matches!(ev, ElementValue::String(_)));
}
#[test]
fn json_roundtrip_local_datetime() {
let dt = sample_naive_datetime();
let original = ElementValue::LocalDateTime(dt);
let json: serde_json::Value = (&original).into();
let recovered: ElementValue = (&json).into();
assert_eq!(original, recovered);
}
#[test]
fn json_roundtrip_zoned_datetime() {
let dt = sample_fixed_datetime();
let original = ElementValue::ZonedDateTime(dt);
let json: serde_json::Value = (&original).into();
let recovered: ElementValue = (&json).into();
assert_eq!(original, recovered);
}
#[test]
fn json_roundtrip_zoned_datetime_utc() {
let dt = Utc
.with_ymd_and_hms(2024, 1, 1, 0, 0, 0)
.unwrap()
.fixed_offset();
let original = ElementValue::ZonedDateTime(dt);
let json: serde_json::Value = (&original).into();
let recovered: ElementValue = (&json).into();
assert_eq!(original, recovered);
}
#[test]
fn tagged_object_with_unknown_type_stays_object() {
let json = serde_json::json!({
"__drasi_v1_envelope__": "drasi.element_value.datetime",
"__drasi_v1_type__": "UnknownType",
"value": "some-value"
});
let ev: ElementValue = (&json).into();
assert!(matches!(ev, ElementValue::Object(_)));
}
#[test]
fn tagged_object_with_invalid_datetime_stays_object() {
let json = serde_json::json!({
"__drasi_v1_envelope__": "drasi.element_value.datetime",
"__drasi_v1_type__": "drasi.ZonedDateTime",
"value": "not-a-datetime"
});
let ev: ElementValue = (&json).into();
assert!(matches!(ev, ElementValue::Object(_)));
}
#[test]
fn object_with_common_type_key_does_not_get_interpreted_as_datetime() {
let json = serde_json::json!({
"_type": "drasi.LocalDateTime",
"value": "2024-06-15 10:30:45",
"timezone_name": "UTC"
});
let ev: ElementValue = (&json).into();
assert!(matches!(ev, ElementValue::Object(_)));
}
#[test]
fn object_with_internal_type_but_no_marker_stays_object() {
let json = serde_json::json!({
"__drasi_v1_type__": "drasi.ZonedDateTime",
"value": "1970-01-01T00:00:00+00:00"
});
let ev: ElementValue = (&json).into();
assert!(matches!(ev, ElementValue::Object(_)));
}
#[test]
fn null_roundtrip() {
let ev = ElementValue::Null;
let vv: VariableValue = (&ev).into();
assert_eq!(vv, VariableValue::Null);
let recovered: ElementValue = vv.try_into().unwrap();
assert_eq!(recovered, ElementValue::Null);
}
#[test]
fn bool_roundtrip() {
let ev = ElementValue::Bool(true);
let vv: VariableValue = (&ev).into();
assert_eq!(vv, VariableValue::Bool(true));
}
#[test]
fn integer_roundtrip() {
let ev = ElementValue::Integer(42);
let vv: VariableValue = (&ev).into();
assert_eq!(vv, VariableValue::Integer(Integer::from(42i64)));
}
#[test]
fn zoned_datetime_utc() {
let dt = Utc
.with_ymd_and_hms(2024, 1, 1, 0, 0, 0)
.unwrap()
.fixed_offset();
let ev = ElementValue::ZonedDateTime(dt);
let vv: VariableValue = (&ev).into();
let recovered: ElementValue = vv.try_into().unwrap();
assert_eq!(recovered, ev);
}
}