use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub enum TwinValue {
Float(f64),
Integer(i64),
Boolean(bool),
Text(String),
}
impl TwinValue {
pub fn as_float(&self) -> Option<f64> {
if let Self::Float(v) = self {
Some(*v)
} else {
None
}
}
pub fn as_integer(&self) -> Option<i64> {
if let Self::Integer(v) = self {
Some(*v)
} else {
None
}
}
pub fn as_boolean(&self) -> Option<bool> {
if let Self::Boolean(v) = self {
Some(*v)
} else {
None
}
}
pub fn as_text(&self) -> Option<&str> {
if let Self::Text(v) = self {
Some(v.as_str())
} else {
None
}
}
}
impl std::fmt::Display for TwinValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Float(v) => write!(f, "{v}"),
Self::Integer(v) => write!(f, "{v}"),
Self::Boolean(v) => write!(f, "{v}"),
Self::Text(v) => write!(f, "{v}"),
}
}
}
#[derive(Debug, Default)]
pub struct Twin {
properties: HashMap<String, TwinValue>,
version: u64,
}
impl Twin {
pub fn new() -> Self {
Self::default()
}
pub fn set_property(&mut self, name: &str, value: TwinValue) {
self.properties.insert(name.to_string(), value);
self.version += 1;
}
pub fn get_property(&self, name: &str) -> Option<&TwinValue> {
self.properties.get(name)
}
pub fn remove_property(&mut self, name: &str) -> Option<TwinValue> {
let removed = self.properties.remove(name);
if removed.is_some() {
self.version += 1;
}
removed
}
pub fn version(&self) -> u64 {
self.version
}
pub fn len(&self) -> usize {
self.properties.len()
}
pub fn is_empty(&self) -> bool {
self.properties.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &TwinValue)> {
self.properties.iter().map(|(k, v)| (k.as_str(), v))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn twin_default_is_empty() {
let twin = Twin::new();
assert!(twin.is_empty());
assert_eq!(twin.len(), 0);
assert_eq!(twin.version(), 0);
}
#[test]
fn twin_set_and_get_float() {
let mut twin = Twin::new();
twin.set_property("temperature", TwinValue::Float(98.6));
assert_eq!(
twin.get_property("temperature"),
Some(&TwinValue::Float(98.6))
);
assert_eq!(twin.version(), 1);
}
#[test]
fn twin_set_and_get_integer() {
let mut twin = Twin::new();
twin.set_property("rpm", TwinValue::Integer(1500));
assert_eq!(twin.get_property("rpm"), Some(&TwinValue::Integer(1500)));
}
#[test]
fn twin_set_and_get_boolean() {
let mut twin = Twin::new();
twin.set_property("mil_on", TwinValue::Boolean(true));
assert_eq!(twin.get_property("mil_on"), Some(&TwinValue::Boolean(true)));
}
#[test]
fn twin_set_and_get_text() {
let mut twin = Twin::new();
twin.set_property("vin", TwinValue::Text("1HGCM82633A123456".to_string()));
assert_eq!(
twin.get_property("vin"),
Some(&TwinValue::Text("1HGCM82633A123456".to_string()))
);
}
#[test]
fn twin_overwrite_increments_version_each_time() {
let mut twin = Twin::new();
twin.set_property("x", TwinValue::Integer(1));
twin.set_property("x", TwinValue::Integer(2));
assert_eq!(twin.version(), 2);
assert_eq!(twin.get_property("x"), Some(&TwinValue::Integer(2)));
}
#[test]
fn twin_missing_property_is_none() {
let twin = Twin::new();
assert!(twin.get_property("nonexistent").is_none());
}
#[test]
fn twin_remove_decrements_len_and_increments_version() {
let mut twin = Twin::new();
twin.set_property("a", TwinValue::Integer(1));
twin.set_property("b", TwinValue::Integer(2));
assert_eq!(twin.len(), 2);
let removed = twin.remove_property("a");
assert_eq!(removed, Some(TwinValue::Integer(1)));
assert_eq!(twin.len(), 1);
assert_eq!(twin.version(), 3); }
#[test]
fn twin_remove_absent_property_does_not_increment_version() {
let mut twin = Twin::new();
twin.set_property("a", TwinValue::Integer(1));
let removed = twin.remove_property("nosuchprop");
assert!(removed.is_none());
assert_eq!(twin.version(), 1); }
#[test]
fn twin_iter_covers_all_properties() {
let mut twin = Twin::new();
twin.set_property("p1", TwinValue::Integer(1));
twin.set_property("p2", TwinValue::Boolean(false));
let pairs: Vec<_> = twin.iter().collect();
assert_eq!(pairs.len(), 2);
}
#[test]
fn twin_value_accessors() {
assert_eq!(TwinValue::Float(1.5).as_float(), Some(1.5));
assert_eq!(TwinValue::Integer(7).as_integer(), Some(7));
assert_eq!(TwinValue::Boolean(true).as_boolean(), Some(true));
assert_eq!(TwinValue::Text("hi".to_string()).as_text(), Some("hi"));
assert_eq!(TwinValue::Integer(7).as_float(), None);
assert_eq!(TwinValue::Float(1.0).as_integer(), None);
assert_eq!(TwinValue::Boolean(false).as_text(), None);
}
#[test]
fn twin_value_display() {
assert_eq!(TwinValue::Float(1.5).to_string(), "1.5");
assert_eq!(TwinValue::Integer(42).to_string(), "42");
assert_eq!(TwinValue::Boolean(true).to_string(), "true");
assert_eq!(TwinValue::Text("hello".to_string()).to_string(), "hello");
}
}