Skip to main content

astarte_device_sdk/
event.rs

1// This file is part of Astarte.
2//
3// Copyright 2023 - 2025 SECO Mind Srl
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9//    http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16//
17// SPDX-License-Identifier: Apache-2.0
18
19//! Event returned form the loop.
20
21use astarte_interfaces::mapping::endpoint::EndpointError;
22use astarte_interfaces::mapping::path::MappingPathError;
23
24use crate::aggregate::AstarteObject;
25use crate::error::{AggregationError, InterfaceTypeError};
26use crate::types::TypeError;
27use crate::{AstarteData, Timestamp};
28
29/// Astarte device event data structure.
30///
31/// Data structure received from the [`Client`](crate::DeviceClient) when the
32/// [`Connection`](crate::DeviceConnection) polls a valid event.
33#[derive(Debug, Clone, PartialEq)]
34pub struct DeviceEvent {
35    /// Interface on which the event has been triggered
36    pub interface: String,
37    /// Path to the endpoint for which the event has been triggered
38    pub path: String,
39    /// Payload of the event
40    pub data: Value,
41}
42
43/// Conversion error from an [`DeviceEvent].
44#[derive(thiserror::Error, Debug)]
45#[non_exhaustive]
46pub enum FromEventError {
47    /// couldn't parse request from interface
48    #[error("couldn't parse request from interface {0}")]
49    Interface(String),
50    /// couldn't parse the event path
51    #[error("the interface {interface} has wrong path {base_path}")]
52    Path {
53        /// Interface that generated the error
54        interface: &'static str,
55        /// Base path of the interface
56        base_path: String,
57    },
58    /// Invalid interface aggregation for the event
59    #[error("invalid interface aggregation for the event")]
60    Aggregation(#[from] AggregationError),
61    /// Invalid interface type for the event
62    #[error("invalid interface type for the event")]
63    InterfaceType(#[from] InterfaceTypeError),
64    /// unset passed to endpoint without allow unset
65    #[error("unset passed to {interface}{endpoint} without allow unset")]
66    Unset {
67        /// Interface that generated the error
68        interface: &'static str,
69        /// endpoint
70        endpoint: String,
71    },
72    /// object missing field
73    #[error("object {interface} missing field {base_path}/{path}")]
74    MissingField {
75        /// Interface that generated the error
76        interface: &'static str,
77        /// Base path of the interface
78        base_path: &'static str,
79        /// Path of the endpoint in error
80        path: &'static str,
81    },
82    /// couldn't convert from [`AstarteData`]
83    #[error("couldn't convert from AstarteData")]
84    Conversion(#[from] TypeError),
85    /// couldn't parse the [`crate::astarte_interfaces::mapping::endpoint::Endpoint`]
86    #[error("couldn't parse the endpoint")]
87    Endpoint(#[from] EndpointError),
88    /// couldn't parse the [`crate::astarte_interfaces::MappingPath`]
89    #[error("couldn't parse the mapping path")]
90    MappingPath(#[from] MappingPathError),
91}
92
93/// Converts a struct form an [`DeviceEvent`].
94///
95/// # Example
96///
97/// ```rust
98/// use astarte_device_sdk::astarte_interfaces::MappingPath;
99/// use astarte_device_sdk::astarte_interfaces::mapping::endpoint::Endpoint;
100/// use astarte_device_sdk::astarte_interfaces::schema::Aggregation;
101/// use astarte_device_sdk::error::AggregationError;
102/// use astarte_device_sdk::event::{FromEvent, FromEventError};
103/// use astarte_device_sdk::{Value, DeviceEvent};
104///
105/// use std::convert::TryFrom;
106///
107/// struct Sensor {
108///     name: String,
109///     value: i32,
110/// }
111///
112/// impl FromEvent for Sensor {
113///     type Err = FromEventError;
114///
115///     fn from_event(event: DeviceEvent) -> Result<Self, Self::Err> {
116///         let base_path: Endpoint<&str> = Endpoint::try_from("/sensor")?;
117///
118///         if event.interface != "com.example.Sensor" {
119///             return Err(FromEventError::Interface(event.interface.clone()));
120///         }
121///
122///         let path = MappingPath::try_from(event.path.as_str())?;
123///
124///         if base_path.eq_mapping(&path) {
125///             return Err(FromEventError::Path {
126///                 interface: "com.example.Sensor",
127///                 base_path: event.path.clone(),
128///             });
129///         }
130///
131///         let Value::Object{mut data, timestamp: _} = event.data else {
132///             return Err(FromEventError::Aggregation(AggregationError::new(
133///                 "com.example.Sensor",
134///                 "sensor",
135///                 Aggregation::Object,
136///                 Aggregation::Individual,
137///             )));
138///         };
139///
140///         let name = data
141///             .remove("name")
142///             .ok_or(FromEventError::MissingField {
143///                 interface: "com.example.Sensor",
144///                 base_path: "sensor",
145///                 path: "name",
146///             })?
147///             .try_into()?;
148///
149///         let value = data
150///             .remove("value")
151///             .ok_or(FromEventError::MissingField {
152///                 interface: "com.example.Sensor",
153///                 base_path: "sensor",
154///                 path: "value",
155///             })?
156///             .try_into()?;
157///
158///         Ok(Self { name, value })
159///     }
160/// }
161/// ```
162pub trait FromEvent: Sized {
163    /// Reason why the conversion failed.
164    type Err;
165
166    /// Perform the conversion from the event.
167    fn from_event(event: DeviceEvent) -> Result<Self, Self::Err>;
168}
169
170/// Data for an [`Astarte data event`](DeviceEvent).
171#[derive(Debug, Clone, PartialEq)]
172pub enum Value {
173    /// Data from an Individual Datastream interface.
174    Individual {
175        ///  The data publish on the endpoint.
176        data: AstarteData,
177        /// The timestamp of the data.
178        ///
179        /// If the interface has `explicit_timestamp` set to true, then this is the value we
180        /// received from Astarte, otherwise it's the reception timestamp.
181        timestamp: Timestamp,
182    },
183    /// Data from an Object Datastream interface.
184    Object {
185        /// The object data received.
186        data: AstarteObject,
187        /// The timestamp of the data.
188        ///
189        /// If the interface has `explicit_timestamp` set to true, then this is the value we
190        /// received from Astarte, otherwise it's the reception timestamp.
191        timestamp: Timestamp,
192    },
193    /// Property data, can also be an unset ,
194    Property(Option<AstarteData>),
195}
196
197impl Value {
198    /// Returns `true` if the aggregation is [`Individual`].
199    ///
200    /// [`Individual`]: Value::Individual
201    #[must_use]
202    pub fn is_individual(&self) -> bool {
203        matches!(self, Self::Individual { .. })
204    }
205
206    /// Get a reference to the [`AstarteData`] if the aggregate is
207    /// [`Individual`](Value::Individual).
208    #[must_use]
209    pub fn as_individual(&self) -> Option<(&AstarteData, &Timestamp)> {
210        if let Self::Individual { data, timestamp } = self {
211            Some((data, timestamp))
212        } else {
213            None
214        }
215    }
216
217    /// Take out of the enum an [`AstarteData`] if the aggregate is
218    /// [`Individual`](Value::Individual).
219    pub fn try_into_individual(self) -> Result<(AstarteData, Timestamp), Self> {
220        if let Self::Individual { data, timestamp } = self {
221            Ok((data, timestamp))
222        } else {
223            Err(self)
224        }
225    }
226
227    /// Returns `true` if the aggregation is [`Object`].
228    ///
229    /// [`Object`]: Value::Object
230    #[must_use]
231    pub fn is_object(&self) -> bool {
232        matches!(self, Self::Object { .. })
233    }
234
235    /// Get a reference to the [`AstarteObject`] if the aggregate is [`Object`](Value::Object).
236    #[must_use]
237    pub fn as_object(&self) -> Option<(&AstarteObject, &Timestamp)> {
238        if let Self::Object { data, timestamp } = self {
239            Some((data, timestamp))
240        } else {
241            None
242        }
243    }
244
245    /// Take out of the enum an [`AstarteObject`] if the aggregate is [`Object`](Value::Object).
246    pub fn try_into_object(self) -> Result<(AstarteObject, Timestamp), Self> {
247        if let Self::Object { data, timestamp } = self {
248            Ok((data, timestamp))
249        } else {
250            Err(self)
251        }
252    }
253
254    /// Returns `true` if the aggregation is [`Property`].
255    ///
256    /// [`Property`]: Value::Property
257    #[must_use]
258    pub fn is_property(&self) -> bool {
259        matches!(self, Self::Property(_))
260    }
261
262    /// Get a reference to the [`AstarteData`] if the type is [`Property`](Value::Property).
263    #[must_use]
264    pub fn as_property(&self) -> Option<&Option<AstarteData>> {
265        if let Self::Property(v) = self {
266            Some(v)
267        } else {
268            None
269        }
270    }
271
272    /// Take out of the enum an [`AstarteData`] if the type is [`Property`](Value::Property).
273    pub fn try_into_property(self) -> Result<Option<AstarteData>, Self> {
274        if let Self::Property(v) = self {
275            Ok(v)
276        } else {
277            Err(self)
278        }
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use chrono::Utc;
285    use pretty_assertions::assert_eq;
286
287    use super::*;
288
289    #[test]
290    fn should_increase_coverage() {
291        let timestamp = Utc::now();
292        let individual = AstarteData::Integer(42);
293        let val = Value::Individual {
294            data: individual.clone(),
295            timestamp,
296        };
297        assert!(val.is_individual());
298        assert_eq!(val.as_individual(), Some((&individual, &timestamp)));
299        assert_eq!(val.as_object(), None);
300        assert_eq!(val.as_property(), None);
301        assert_eq!(
302            val.clone().try_into_individual(),
303            Ok((individual, timestamp))
304        );
305        assert_eq!(val.clone().try_into_object(), Err(val.clone()));
306        assert_eq!(val.clone().try_into_property(), Err(val));
307
308        let val = Value::Object {
309            data: AstarteObject::new(),
310            timestamp,
311        };
312        assert!(val.is_object());
313        assert_eq!(val.as_individual(), None);
314        assert_eq!(val.as_property(), None);
315        assert_eq!(val.as_object(), Some((&AstarteObject::new(), &timestamp)));
316        assert_eq!(
317            val.clone().try_into_object(),
318            Ok((AstarteObject::new(), timestamp))
319        );
320        assert_eq!(val.clone().try_into_individual(), Err(val.clone()));
321        assert_eq!(val.clone().try_into_property(), Err(val));
322
323        let prop = Some(AstarteData::Integer(42));
324        let val = Value::Property(prop.clone());
325        assert!(val.is_property());
326        assert_eq!(val.as_property(), Some(&prop));
327        assert_eq!(val.as_object(), None);
328        assert_eq!(val.as_individual(), None);
329        assert_eq!(val.clone().try_into_individual(), Err(val.clone()));
330        assert_eq!(val.clone().try_into_object(), Err(val.clone()));
331        assert_eq!(val.clone().try_into_property(), Ok(prop));
332    }
333
334    #[test]
335    #[cfg(feature = "derive")]
336    fn should_derive_form_event_obj() {
337        use crate::aggregate::AstarteObject;
338        use crate::{DeviceEvent, FromEvent, Value};
339
340        // Alias the crate to the resulting macro
341        use crate::{self as astarte_device_sdk};
342
343        #[derive(Debug, FromEvent, PartialEq, Eq)]
344        #[from_event(
345            interface = "com.example.Sensor",
346            path = "/sensor",
347            aggregation = "object"
348        )]
349        struct Sensor {
350            name: String,
351            value: i32,
352        }
353
354        let mut data = AstarteObject::new();
355        data.insert("name".to_string(), "Foo".to_string().into());
356        data.insert("value".to_string(), 42i32.into());
357
358        let event = DeviceEvent {
359            interface: "com.example.Sensor".to_string(),
360            path: "/sensor".to_string(),
361            data: Value::Object {
362                data,
363                timestamp: Utc::now(),
364            },
365        };
366
367        let sensor = Sensor::from_event(event).expect("couldn't parse the event");
368
369        let expected = Sensor {
370            name: "Foo".to_string(),
371            value: 42,
372        };
373
374        assert_eq!(sensor, expected);
375    }
376
377    #[test]
378    #[cfg(feature = "derive")]
379    fn should_derive_form_event_individual() {
380        use crate::{AstarteData, DeviceEvent, FromEvent, Value};
381
382        // Alias the crate to the resulting macro
383        use crate::{self as astarte_device_sdk};
384
385        #[derive(Debug, FromEvent, PartialEq)]
386        #[from_event(interface = "com.example.Sensor", aggregation = "individual")]
387        enum Sensor {
388            #[mapping(endpoint = "/%{param}/luminosity")]
389            Luminosity(i32),
390            #[mapping(endpoint = "/sensor/temperature")]
391            Temperature(f64),
392        }
393
394        let event = DeviceEvent {
395            interface: "com.example.Sensor".to_string(),
396            path: "/sensor/luminosity".to_string(),
397            data: Value::Individual {
398                data: 42i32.into(),
399                timestamp: Utc::now(),
400            },
401        };
402
403        let luminosity = Sensor::from_event(event).expect("couldn't parse the event");
404
405        let expected = Sensor::Luminosity(42);
406
407        assert_eq!(luminosity, expected);
408
409        let event = DeviceEvent {
410            interface: "com.example.Sensor".to_string(),
411            path: "/sensor/temperature".to_string(),
412            data: Value::Individual {
413                data: AstarteData::try_from(3.0).unwrap(),
414                timestamp: Utc::now(),
415            },
416        };
417
418        let temperature = Sensor::from_event(event).expect("couldn't parse the event");
419
420        let expected = Sensor::Temperature(3.);
421
422        assert_eq!(temperature, expected);
423    }
424
425    #[test]
426    #[cfg(feature = "derive")]
427    fn should_derive_form_event_property() {
428        use crate::{AstarteData, DeviceEvent, FromEvent, Value};
429
430        // Alias the crate to the resulting macro
431        use crate::{self as astarte_device_sdk};
432
433        #[derive(Debug, FromEvent, PartialEq)]
434        #[from_event(interface = "com.example.Sensor", interface_type = "properties")]
435        enum Sensor {
436            #[mapping(endpoint = "/%{param}/luminosity")]
437            Luminosity(i32),
438            #[mapping(endpoint = "/sensor/temperature")]
439            Temperature(f64),
440            #[mapping(endpoint = "/sensor/unsettable", allow_unset = true)]
441            Unsettable(Option<bool>),
442        }
443
444        let event = DeviceEvent {
445            interface: "com.example.Sensor".to_string(),
446            path: "/sensor/luminosity".to_string(),
447            data: Value::Property(Some(42i32.into())),
448        };
449
450        let luminosity = Sensor::from_event(event).expect("couldn't parse the event");
451
452        let expected = Sensor::Luminosity(42);
453
454        assert_eq!(luminosity, expected);
455
456        let event = DeviceEvent {
457            interface: "com.example.Sensor".to_string(),
458            path: "/sensor/temperature".to_string(),
459            data: Value::Property(Some(AstarteData::try_from(3.0).unwrap())),
460        };
461
462        let temperature = Sensor::from_event(event).expect("couldn't parse the event");
463
464        let expected = Sensor::Temperature(3.0);
465
466        assert_eq!(temperature, expected);
467
468        let event = DeviceEvent {
469            interface: "com.example.Sensor".to_string(),
470            path: "/sensor/unsettable".to_string(),
471            data: Value::Property(Some(AstarteData::Boolean(true))),
472        };
473
474        let temperature = Sensor::from_event(event).expect("couldn't parse the event");
475
476        let expected = Sensor::Unsettable(Some(true));
477
478        assert_eq!(temperature, expected);
479
480        let event = DeviceEvent {
481            interface: "com.example.Sensor".to_string(),
482            path: "/sensor/unsettable".to_string(),
483            data: Value::Property(None),
484        };
485
486        let temperature = Sensor::from_event(event).expect("couldn't parse the event");
487
488        let expected = Sensor::Unsettable(None);
489
490        assert_eq!(temperature, expected);
491    }
492}