iot-core 0.0.1

Core types for the iot-protocols SDK: Thing model, IotClient trait, errors, paths, protocol bindings.
Documentation
//! Self-describing scalar / collection values used by the Thing model.
//!
//! [`Value`] mirrors the JSON value lattice (null / bool / number / string /
//! array / object) but is intentionally *not* a `serde_json::Value` — it
//! works in `no_std + alloc` and avoids dragging the full JSON parser in
//! when only the shape is needed.

#[cfg(feature = "alloc")]
use alloc::{collections::BTreeMap, string::String, vec::Vec};

use smol_str::SmolStr;

/// A single Thing property value — closed sum, JSON-shaped.
///
/// `Value` deliberately does not provide arithmetic or comparison helpers;
/// protocol crates lift it into typed shapes via `DataType` (see the
/// [`crate::binding`] module).
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
pub enum Value {
    /// Explicit null. Encoded as JSON `null`.
    Null,
    /// `true` / `false`.
    Bool(bool),
    /// Integer in the i64 range. Most industrial sensors fit here.
    Int(i64),
    /// IEEE-754 double. Use when the source has fractional units.
    Float(f64),
    /// Short string — backed by `SmolStr` so values up to 23 bytes are inline.
    Str(SmolStr),
    /// Heterogeneous list. Requires the `alloc` feature (always enabled by
    /// default; only MCU-with-no-allocator builds turn it off).
    #[cfg(feature = "alloc")]
    Array(Vec<Value>),
    /// Object with string keys. Requires the `alloc` feature.
    #[cfg(feature = "alloc")]
    Object(BTreeMap<String, Value>),
}

impl Value {
    /// Returns `true` if the value is [`Value::Null`].
    pub const fn is_null(&self) -> bool {
        matches!(self, Value::Null)
    }

    /// Borrow the inner string if `self` is [`Value::Str`].
    pub fn as_str(&self) -> Option<&str> {
        if let Value::Str(s) = self {
            Some(s.as_str())
        } else {
            None
        }
    }

    /// Returns the inner integer if `self` is [`Value::Int`].
    pub const fn as_int(&self) -> Option<i64> {
        if let Value::Int(v) = *self {
            Some(v)
        } else {
            None
        }
    }

    /// Returns the inner float if `self` is [`Value::Float`] *or*
    /// [`Value::Int`] (lossy widening on Int).
    pub fn as_float(&self) -> Option<f64> {
        match *self {
            Value::Float(v) => Some(v),
            Value::Int(v) => Some(v as f64),
            _ => None,
        }
    }
}

impl From<bool> for Value {
    fn from(v: bool) -> Self {
        Value::Bool(v)
    }
}
impl From<i32> for Value {
    fn from(v: i32) -> Self {
        Value::Int(v as i64)
    }
}
impl From<i64> for Value {
    fn from(v: i64) -> Self {
        Value::Int(v)
    }
}
impl From<u16> for Value {
    fn from(v: u16) -> Self {
        Value::Int(v as i64)
    }
}
impl From<u32> for Value {
    fn from(v: u32) -> Self {
        Value::Int(v as i64)
    }
}
impl From<f32> for Value {
    fn from(v: f32) -> Self {
        Value::Float(v as f64)
    }
}
impl From<f64> for Value {
    fn from(v: f64) -> Self {
        Value::Float(v)
    }
}
impl From<&str> for Value {
    fn from(v: &str) -> Self {
        Value::Str(SmolStr::new(v))
    }
}
impl From<SmolStr> for Value {
    fn from(v: SmolStr) -> Self {
        Value::Str(v)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn coercions_round_trip() {
        assert_eq!(Value::from(42i32).as_int(), Some(42));
        assert_eq!(Value::from(2.5f64).as_float(), Some(2.5));
        assert_eq!(Value::from("hi").as_str(), Some("hi"));
        assert_eq!(Value::from(7i64).as_float(), Some(7.0));
        assert!(Value::Null.is_null());
    }
}