Skip to main content

iot_core/
value.rs

1//! Self-describing scalar / collection values used by the Thing model.
2//!
3//! [`Value`] mirrors the JSON value lattice (null / bool / number / string /
4//! array / object) but is intentionally *not* a `serde_json::Value` — it
5//! works in `no_std + alloc` and avoids dragging the full JSON parser in
6//! when only the shape is needed.
7
8#[cfg(feature = "alloc")]
9use alloc::{collections::BTreeMap, string::String, vec::Vec};
10
11use smol_str::SmolStr;
12
13/// A single Thing property value — closed sum, JSON-shaped.
14///
15/// `Value` deliberately does not provide arithmetic or comparison helpers;
16/// protocol crates lift it into typed shapes via `DataType` (see the
17/// [`crate::binding`] module).
18#[derive(Debug, Clone, PartialEq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20#[cfg_attr(feature = "serde", serde(untagged))]
21pub enum Value {
22    /// Explicit null. Encoded as JSON `null`.
23    Null,
24    /// `true` / `false`.
25    Bool(bool),
26    /// Integer in the i64 range. Most industrial sensors fit here.
27    Int(i64),
28    /// IEEE-754 double. Use when the source has fractional units.
29    Float(f64),
30    /// Short string — backed by `SmolStr` so values up to 23 bytes are inline.
31    Str(SmolStr),
32    /// Heterogeneous list. Requires the `alloc` feature (always enabled by
33    /// default; only MCU-with-no-allocator builds turn it off).
34    #[cfg(feature = "alloc")]
35    Array(Vec<Value>),
36    /// Object with string keys. Requires the `alloc` feature.
37    #[cfg(feature = "alloc")]
38    Object(BTreeMap<String, Value>),
39}
40
41impl Value {
42    /// Returns `true` if the value is [`Value::Null`].
43    pub const fn is_null(&self) -> bool {
44        matches!(self, Value::Null)
45    }
46
47    /// Borrow the inner string if `self` is [`Value::Str`].
48    pub fn as_str(&self) -> Option<&str> {
49        if let Value::Str(s) = self {
50            Some(s.as_str())
51        } else {
52            None
53        }
54    }
55
56    /// Returns the inner integer if `self` is [`Value::Int`].
57    pub const fn as_int(&self) -> Option<i64> {
58        if let Value::Int(v) = *self {
59            Some(v)
60        } else {
61            None
62        }
63    }
64
65    /// Returns the inner float if `self` is [`Value::Float`] *or*
66    /// [`Value::Int`] (lossy widening on Int).
67    pub fn as_float(&self) -> Option<f64> {
68        match *self {
69            Value::Float(v) => Some(v),
70            Value::Int(v) => Some(v as f64),
71            _ => None,
72        }
73    }
74}
75
76impl From<bool> for Value {
77    fn from(v: bool) -> Self {
78        Value::Bool(v)
79    }
80}
81impl From<i32> for Value {
82    fn from(v: i32) -> Self {
83        Value::Int(v as i64)
84    }
85}
86impl From<i64> for Value {
87    fn from(v: i64) -> Self {
88        Value::Int(v)
89    }
90}
91impl From<u16> for Value {
92    fn from(v: u16) -> Self {
93        Value::Int(v as i64)
94    }
95}
96impl From<u32> for Value {
97    fn from(v: u32) -> Self {
98        Value::Int(v as i64)
99    }
100}
101impl From<f32> for Value {
102    fn from(v: f32) -> Self {
103        Value::Float(v as f64)
104    }
105}
106impl From<f64> for Value {
107    fn from(v: f64) -> Self {
108        Value::Float(v)
109    }
110}
111impl From<&str> for Value {
112    fn from(v: &str) -> Self {
113        Value::Str(SmolStr::new(v))
114    }
115}
116impl From<SmolStr> for Value {
117    fn from(v: SmolStr) -> Self {
118        Value::Str(v)
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn coercions_round_trip() {
128        assert_eq!(Value::from(42i32).as_int(), Some(42));
129        assert_eq!(Value::from(2.5f64).as_float(), Some(2.5));
130        assert_eq!(Value::from("hi").as_str(), Some("hi"));
131        assert_eq!(Value::from(7i64).as_float(), Some(7.0));
132        assert!(Value::Null.is_null());
133    }
134}