1#[cfg(feature = "alloc")]
14use alloc::{collections::BTreeMap, vec::Vec};
15
16use smol_str::SmolStr;
17
18use crate::value::Value;
19
20#[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 pub fn new(s: impl Into<SmolStr>) -> Self {
34 Self(s.into())
35 }
36
37 pub fn as_str(&self) -> &str {
39 self.0.as_str()
40 }
41
42 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 pub fn is_valid(&self) -> bool {
53 self.split().is_some()
54 }
55}
56
57#[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 pub fn new(s: impl Into<SmolStr>) -> Self {
66 Self(s.into())
67 }
68 pub fn as_str(&self) -> &str {
70 self.0.as_str()
71 }
72}
73
74#[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 pub fn new(s: impl Into<SmolStr>) -> Self {
83 Self(s.into())
84 }
85 pub fn as_str(&self) -> &str {
87 self.0.as_str()
88 }
89}
90
91#[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 pub fn new(s: impl Into<SmolStr>) -> Self {
103 Self(s.into())
104 }
105 pub fn as_str(&self) -> &str {
107 self.0.as_str()
108 }
109}
110
111#[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 pub fn new(s: impl Into<SmolStr>) -> Self {
122 Self(s.into())
123 }
124 pub fn as_str(&self) -> &str {
126 self.0.as_str()
127 }
128}
129
130#[cfg(feature = "alloc")]
135#[derive(Debug, Clone, PartialEq, Default)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
137pub struct Feature {
138 #[cfg_attr(
140 feature = "serde",
141 serde(default, skip_serializing_if = "Vec::is_empty")
142 )]
143 pub definition: Vec<DefinitionIdentifier>,
144 #[cfg_attr(
146 feature = "serde",
147 serde(default, skip_serializing_if = "BTreeMap::is_empty")
148 )]
149 pub properties: BTreeMap<PropertyId, Value>,
150 #[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 pub fn new() -> Self {
166 Self::default()
167 }
168
169 pub fn with_property(mut self, id: PropertyId, value: Value) -> Self {
171 self.properties.insert(id, value);
172 self
173 }
174
175 pub fn property(&self, id: &PropertyId) -> Option<&Value> {
177 self.properties.get(id)
178 }
179}
180
181#[cfg(feature = "alloc")]
183#[derive(Debug, Clone, PartialEq)]
184#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
185pub struct Thing {
186 #[cfg_attr(feature = "serde", serde(rename = "thingId"))]
188 pub thing_id: ThingId,
189 #[cfg_attr(
191 feature = "serde",
192 serde(default, skip_serializing_if = "Option::is_none", rename = "policyId")
193 )]
194 pub policy_id: Option<PolicyId>,
195 #[cfg_attr(
197 feature = "serde",
198 serde(default, skip_serializing_if = "BTreeMap::is_empty")
199 )]
200 pub attributes: BTreeMap<SmolStr, Value>,
201 #[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 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 pub fn with_feature(mut self, id: FeatureId, feature: Feature) -> Self {
223 self.features.insert(id, feature);
224 self
225 }
226
227 pub fn feature(&self, id: &FeatureId) -> Option<&Feature> {
229 self.features.get(id)
230 }
231
232 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}