shvrpc 15.0.0

Rust implementation of the SHV-RPC protocol
Documentation
use bitflags::bitflags;
use shvproto::{DateTime as ShvDateTime, RpcValue};

#[derive(Clone,Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct DataChange {
    pub value: RpcValue,
    pub date_time: Option<ShvDateTime>,
    pub short_time: Option<i32>,
    pub value_flags: ValueFlags,
}

#[derive(Debug, Clone, Copy)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ValueFlags(u64);

bitflags! {
    impl ValueFlags: u64 {
        const SPONTANEOUS = 1 << 1;
        const PROVISIONAL = 1 << 2;

        // The source may set any bits
        const _ = !0;
    }
}

impl Default for ValueFlags {
    fn default() -> Self {
        Self::empty()
    }
}

#[derive(Copy,Clone,Debug)]
enum DataChangeMetaTag {
    DateTime = crate::rpctype::Tag::USER as isize,
    ShortTime,
    ValueFlags,
    SpecialListValue,
}

pub fn meta_value<I: shvproto::metamap::GetIndex>(rv: &RpcValue, key: I) -> Option<&RpcValue> {
    rv.meta.as_ref()?.get(key)
}

pub fn is_data_change(rv: &RpcValue) -> bool {
    meta_value(rv, crate::rpctype::Tag::MetaTypeNameSpaceId as usize).unwrap_or_default().as_int() == crate::rpctype::global_ns::NAMESPACE_ID &&
        meta_value(rv, crate::rpctype::Tag::MetaTypeId as usize).unwrap_or_default().as_int() == crate::rpctype::global_ns::MetaTypeID::ValueChange as i64
}

impl shvproto::metamap::GetIndex for DataChangeMetaTag {
    fn make_key(&self) -> shvproto::metamap::GetKey<'_> {
        shvproto::metamap::GetKey::Int(*self as i32)
    }
}

impl From<RpcValue> for DataChange {
    fn from(value: RpcValue) -> Self {
        if !is_data_change(&value) {
            return DataChange {
                value,
                date_time: None,
                short_time: None,
                value_flags: ValueFlags::default(),
            }
        }

        let date_time = meta_value(&value, DataChangeMetaTag::DateTime)
            .and_then(RpcValue::to_datetime);
        let short_time = meta_value(&value, DataChangeMetaTag::ShortTime)
            .map(RpcValue::as_i32);
        let value_flags = meta_value(&value, DataChangeMetaTag::ValueFlags)
            .map(|v| ValueFlags::from_bits_retain(v.as_u64())).unwrap_or_default();

        let unpacked_value = if let shvproto::Value::List(lst) = &value.value &&
            let [single_element] = lst.as_slice() &&
            single_element.meta.as_ref().is_some_and(|meta| !meta.is_empty()) &&
            value.meta.as_ref().is_none_or(|meta| meta.get(DataChangeMetaTag::SpecialListValue).is_none())
        {
            single_element.clone()
        } else {
            RpcValue {
                meta: None,
                value: value.value,
            }
        };
        DataChange {
            value: unpacked_value,
            date_time,
            short_time,
            value_flags,
        }
    }
}

impl From<DataChange> for RpcValue {
    fn from(data_change: DataChange) -> Self {
        let mut res = if data_change.value.meta.as_ref().is_none_or(|meta| meta.is_empty()) {
            let val = data_change.value.value.clone();
            if let shvproto::Value::List(lst) = &val &&
                let [single_element] = lst.as_slice() &&
                single_element.meta.as_ref().is_some_and(|meta| !meta.is_empty()) {
                    let mut mm = shvproto::MetaMap::new();
                    mm.insert(DataChangeMetaTag::SpecialListValue as usize, true.into());
                    RpcValue::new(val, Some(mm))
            } else {
                RpcValue::new(val, None)
            }
        } else {
            shvproto::make_list!(data_change.value).into()
        };
        let mm = res.meta
            .get_or_insert_default()
            .insert(crate::rpctype::Tag::MetaTypeId as usize, RpcValue::from(crate::rpctype::global_ns::MetaTypeID::ValueChange as i64));
        if let Some(date_time) = data_change.date_time {
            mm.insert(DataChangeMetaTag::DateTime, date_time.into());
        }
        if let Some(short_time) = data_change.short_time {
            mm.insert(DataChangeMetaTag::ShortTime, short_time.into());
        }
        if !data_change.value_flags.is_empty() {
            mm.insert(DataChangeMetaTag::ValueFlags, data_change.value_flags.bits().into());
        }
        res
    }
}

#[cfg(test)]
mod tests {
    use shvproto::{DateTime as ShvDateTime, RpcValue};

    use crate::datachange::ValueFlags;

    use super::DataChange;

    #[test]
    fn rpcvalue_to_datachange() {
        let notification_param = RpcValue::from_cpon(r#"<1:5,8:d"2025-07-14T15:01:00.201Z",10:2>false"#).unwrap();
        let data_change = DataChange::from(notification_param);
        assert_eq!(data_change.value, false.into());
        assert_eq!(data_change.date_time, Some(ShvDateTime::from_iso_str("2025-07-14T15:01:00.201Z").unwrap()));
        assert_eq!(data_change.short_time, None);

        let notification_param = RpcValue::from_cpon("true").unwrap();
        let data_change = DataChange::from(notification_param);
        assert_eq!(data_change.value, true.into());
        assert_eq!(data_change.date_time, None);
        assert_eq!(data_change.short_time, None);

        let notification_param = RpcValue::from_cpon("<1:5,10:4>[<1:123>true]").unwrap();
        let data_change = DataChange::from(notification_param);
        assert_eq!(data_change.value.value, true.into());
        assert!(data_change.value.meta.is_some_and(|m| m.get(1).is_some_and(|v| v == &123.into())));
        assert_eq!(data_change.date_time, None);
        assert_eq!(data_change.short_time, None);
        assert_eq!(data_change.value_flags, ValueFlags::PROVISIONAL);

        let notification_param = RpcValue::from_cpon("<1:5,10:4,11:true>[<1:123>true]").unwrap();
        let data_change = DataChange::from(notification_param);
        assert_eq!(data_change.value, shvproto::make_list!(RpcValue::new(true.into(), {let mut mm = shvproto::metamap::MetaMap::new(); mm.insert(1, 123.into()); Some(mm)})).into());
        assert!(data_change.value.meta.is_none());
        assert_eq!(data_change.date_time, None);
        assert_eq!(data_change.short_time, None);
        assert_eq!(data_change.value_flags, ValueFlags::PROVISIONAL);
    }

    #[test]
    fn rpcvalue_datachange_conversions() {
        let data_change = DataChange {
            value: 123.into(),
            date_time: None,
            short_time: None,
            value_flags: ValueFlags::empty(),
        };
        let rv: RpcValue = data_change.clone().into();
        assert_eq!(data_change, rv.into());

        let data_change = DataChange {
            value: "foo".into(),
            date_time: Some(ShvDateTime::from_iso_str("2025-07-14T15:01:00.201Z").unwrap()),
            short_time: Some(10),
            value_flags: ValueFlags::PROVISIONAL,
        };
        let rv: RpcValue = data_change.clone().into();
        assert_eq!(data_change, rv.into());
    }
}