Skip to main content

iot_core/
thing.rs

1//! Identifiers and aggregates for the Eclipse-Ditto-style Thing model.
2//!
3//! A [`Thing`] is the canonical digital twin: a top-level entity (`ThingId`)
4//! holds free-form `attributes`, plus zero or more named `Feature`s. Each
5//! `Feature` is a typed bag of `properties` and `desired_properties` (the
6//! desired-state pattern from device twins).
7//!
8//! Every protocol in the SDK ultimately reads or writes a `Property`
9//! identified by a [`crate::path::PropertyPath`] = `(ThingId, FeatureId,
10//! PropertyId)`. The mapping from that triple to a Modbus register / MQTT
11//! topic / CoAP URI lives in [`crate::binding::ThingMapping`].
12
13#[cfg(feature = "alloc")]
14use alloc::{collections::BTreeMap, vec::Vec};
15
16use smol_str::SmolStr;
17
18use crate::value::Value;
19
20/// Globally unique identifier of a Thing — `namespace:name`.
21///
22/// The format mirrors Eclipse Ditto: a non-empty namespace, a colon, and a
23/// non-empty name. We do not enforce the regex at construction time (it
24/// would force allocations on the failure path); use [`ThingId::is_valid`]
25/// when accepting external input.
26#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28#[cfg_attr(feature = "serde", serde(transparent))]
29pub struct ThingId(pub SmolStr);
30
31impl ThingId {
32    /// Construct from any string-like input.
33    pub fn new(s: impl Into<SmolStr>) -> Self {
34        Self(s.into())
35    }
36
37    /// Borrow the underlying `&str`.
38    pub fn as_str(&self) -> &str {
39        self.0.as_str()
40    }
41
42    /// Return `(namespace, name)` if the id has the canonical shape.
43    pub fn split(&self) -> Option<(&str, &str)> {
44        let (ns, name) = self.0.as_str().split_once(':')?;
45        if ns.is_empty() || name.is_empty() {
46            return None;
47        }
48        Some((ns, name))
49    }
50
51    /// Lightweight syntactic check — *not* a full Ditto regex match.
52    pub fn is_valid(&self) -> bool {
53        self.split().is_some()
54    }
55}
56
57/// Identifier of a Feature inside a Thing.
58#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60#[cfg_attr(feature = "serde", serde(transparent))]
61pub struct FeatureId(pub SmolStr);
62
63impl FeatureId {
64    /// Construct.
65    pub fn new(s: impl Into<SmolStr>) -> Self {
66        Self(s.into())
67    }
68    /// Borrow.
69    pub fn as_str(&self) -> &str {
70        self.0.as_str()
71    }
72}
73
74/// Identifier of a Property inside a Feature.
75#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77#[cfg_attr(feature = "serde", serde(transparent))]
78pub struct PropertyId(pub SmolStr);
79
80impl PropertyId {
81    /// Construct.
82    pub fn new(s: impl Into<SmolStr>) -> Self {
83        Self(s.into())
84    }
85    /// Borrow.
86    pub fn as_str(&self) -> &str {
87        self.0.as_str()
88    }
89}
90
91/// Optional access-policy identifier — referenced by `Thing.policy_id`.
92///
93/// The SDK does not interpret policies; it only round-trips the id so it
94/// can be re-emitted to the cloud side.
95#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
96#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
97#[cfg_attr(feature = "serde", serde(transparent))]
98pub struct PolicyId(pub SmolStr);
99
100impl PolicyId {
101    /// Construct.
102    pub fn new(s: impl Into<SmolStr>) -> Self {
103        Self(s.into())
104    }
105    /// Borrow.
106    pub fn as_str(&self) -> &str {
107        self.0.as_str()
108    }
109}
110
111/// Reference to a Vorto / W3C-WoT Thing-Description definition.
112///
113/// Stored verbatim; the SDK never resolves it.
114#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
115#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
116#[cfg_attr(feature = "serde", serde(transparent))]
117pub struct DefinitionIdentifier(pub SmolStr);
118
119impl DefinitionIdentifier {
120    /// Construct.
121    pub fn new(s: impl Into<SmolStr>) -> Self {
122        Self(s.into())
123    }
124    /// Borrow.
125    pub fn as_str(&self) -> &str {
126        self.0.as_str()
127    }
128}
129
130/// A Feature — a named, typed bag of properties on a Thing.
131///
132/// `properties` is the *reported* state (what the device says it is);
133/// `desired_properties` is the cloud-driven target state — twin pattern.
134#[cfg(feature = "alloc")]
135#[derive(Debug, Clone, PartialEq, Default)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
137pub struct Feature {
138    /// Definitions implemented by this feature, in priority order.
139    #[cfg_attr(
140        feature = "serde",
141        serde(default, skip_serializing_if = "Vec::is_empty")
142    )]
143    pub definition: Vec<DefinitionIdentifier>,
144    /// Reported state.
145    #[cfg_attr(
146        feature = "serde",
147        serde(default, skip_serializing_if = "BTreeMap::is_empty")
148    )]
149    pub properties: BTreeMap<PropertyId, Value>,
150    /// Desired state (target).
151    #[cfg_attr(
152        feature = "serde",
153        serde(
154            default,
155            skip_serializing_if = "BTreeMap::is_empty",
156            rename = "desiredProperties"
157        )
158    )]
159    pub desired_properties: BTreeMap<PropertyId, Value>,
160}
161
162#[cfg(feature = "alloc")]
163impl Feature {
164    /// Empty feature — no definition, no properties.
165    pub fn new() -> Self {
166        Self::default()
167    }
168
169    /// Set or overwrite a reported property.
170    pub fn with_property(mut self, id: PropertyId, value: Value) -> Self {
171        self.properties.insert(id, value);
172        self
173    }
174
175    /// Look up a reported property.
176    pub fn property(&self, id: &PropertyId) -> Option<&Value> {
177        self.properties.get(id)
178    }
179}
180
181/// A Thing — the top-level digital twin.
182#[cfg(feature = "alloc")]
183#[derive(Debug, Clone, PartialEq)]
184#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
185pub struct Thing {
186    /// Globally unique id, `namespace:name`.
187    #[cfg_attr(feature = "serde", serde(rename = "thingId"))]
188    pub thing_id: ThingId,
189    /// Optional access policy reference.
190    #[cfg_attr(
191        feature = "serde",
192        serde(default, skip_serializing_if = "Option::is_none", rename = "policyId")
193    )]
194    pub policy_id: Option<PolicyId>,
195    /// Free-form attributes (manufacturer, location, …).
196    #[cfg_attr(
197        feature = "serde",
198        serde(default, skip_serializing_if = "BTreeMap::is_empty")
199    )]
200    pub attributes: BTreeMap<SmolStr, Value>,
201    /// Features by id.
202    #[cfg_attr(
203        feature = "serde",
204        serde(default, skip_serializing_if = "BTreeMap::is_empty")
205    )]
206    pub features: BTreeMap<FeatureId, Feature>,
207}
208
209#[cfg(feature = "alloc")]
210impl Thing {
211    /// Construct an empty Thing — only the id is required.
212    pub fn new(thing_id: ThingId) -> Self {
213        Self {
214            thing_id,
215            policy_id: None,
216            attributes: BTreeMap::new(),
217            features: BTreeMap::new(),
218        }
219    }
220
221    /// Insert or replace a feature.
222    pub fn with_feature(mut self, id: FeatureId, feature: Feature) -> Self {
223        self.features.insert(id, feature);
224        self
225    }
226
227    /// Borrow a feature by id.
228    pub fn feature(&self, id: &FeatureId) -> Option<&Feature> {
229        self.features.get(id)
230    }
231
232    /// Mutably borrow a feature by id, creating it if absent.
233    pub fn feature_mut_or_default(&mut self, id: FeatureId) -> &mut Feature {
234        self.features.entry(id).or_default()
235    }
236}
237
238#[cfg(test)]
239#[cfg(feature = "alloc")]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn thing_id_split() {
245        let id = ThingId::new("acme:pump-7");
246        assert!(id.is_valid());
247        assert_eq!(id.split(), Some(("acme", "pump-7")));
248
249        let bad = ThingId::new("no-colon");
250        assert!(!bad.is_valid());
251    }
252
253    #[test]
254    fn build_thing() {
255        let t = Thing::new(ThingId::new("acme:pump-7")).with_feature(
256            FeatureId::new("temperature"),
257            Feature::new().with_property(PropertyId::new("value"), 23.5.into()),
258        );
259
260        assert_eq!(
261            t.feature(&FeatureId::new("temperature"))
262                .and_then(|f| f.property(&PropertyId::new("value")))
263                .and_then(Value::as_float),
264            Some(23.5)
265        );
266    }
267}