Skip to main content

astarte_device_sdk/
error.rs

1// This file is part of Astarte.
2//
3// Copyright 2023-2026 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//! Error types for the Astarte SDK.
20
21use std::convert::Infallible;
22use std::fmt::{Display, Formatter};
23
24use astarte_interfaces::error::Error as InterfaceError;
25use astarte_interfaces::mapping::path::MappingPathError;
26use astarte_interfaces::schema::{Aggregation, InterfaceType, Ownership};
27
28use crate::builder::BuilderError;
29use crate::introspection::AddInterfaceError;
30use crate::properties::PropertiesError;
31use crate::retention::RetentionError;
32use crate::session::SessionError;
33use crate::store::error::StoreError;
34use crate::transport::mqtt::error::MqttError;
35use crate::types::TypeError;
36use crate::validate::UserValidationError;
37
38/// Dynamic error type
39pub(crate) type DynError = Box<dyn std::error::Error + Send + Sync + 'static>;
40
41/// Astarte error.
42///
43/// Possible errors returned by functions of the Astarte device SDK.
44#[non_exhaustive]
45#[derive(thiserror::Error, Debug)]
46pub enum Error {
47    /// The connection poll reached the max number of retries.
48    #[error("connection reached max retries")]
49    ConnectionTimeout,
50    /// Error while parsing interface.
51    #[error("couldn't parse interface")]
52    Interface(#[from] InterfaceError),
53    /// Error while operating on the device introspection.
54    #[error("couldn't complete introspection operation")]
55    AddInterface(#[from] AddInterfaceError),
56    /// Couldn't find an interface with the given name.
57    #[error("couldn't find interface '{name}'")]
58    InterfaceNotFound {
59        /// Name of the missing interface.
60        name: String,
61    },
62    /// Couldn't find missing mapping in the interface.
63    #[error("couldn't find mapping {mapping} in interface {interface}")]
64    MappingNotFound {
65        /// Name of the interface.
66        interface: String,
67        /// Path of the missing mapping.
68        mapping: String,
69    },
70    /// Couldn't parse the mapping path.
71    #[error("invalid mapping path")]
72    InvalidEndpoint(#[from] MappingPathError),
73    /// Errors when converting between Astarte types.
74    #[error("couldn't convert to Astarte Type")]
75    Types(#[from] TypeError),
76    /// Error while parsing the /control/consumer/properties payload.
77    #[error("couldn't handle properties")]
78    Properties(#[from] PropertiesError),
79    /// Error returned by a store operation.
80    #[error("couldn't complete store operation")]
81    Store(#[from] StoreError),
82    /// Send or receive validation failed
83    #[error("validation of the send payload failed")]
84    Validation(#[from] UserValidationError),
85    /// Invalid aggregation between the interface and the data.
86    #[error(transparent)]
87    Aggregation(#[from] AggregationError),
88    /// Invalid interface type between the interface and the data.
89    #[error(transparent)]
90    InterfaceType(#[from] InterfaceTypeError),
91    /// Couldn't build the device connection and client
92    #[error("couldn't build the device connection and client")]
93    Builder(#[from] BuilderError),
94    /// Error returned by the MQTT connection.
95    #[error(transparent)]
96    Mqtt(#[from] MqttError),
97    /// Error when the Device is disconnected from Astarte or client.
98    ///
99    /// This is an unrecoverable error for the SDK.
100    #[error("disconnected from Astarte")]
101    Disconnected,
102    /// Retention operation failed.
103    #[error("retention operation failed")]
104    Retention(#[from] RetentionError),
105    /// Persistent session operation failed
106    #[error("persistent session operation failed")]
107    Session(#[from] SessionError),
108    /// Error returned by the gRPC transport
109    #[cfg(feature = "message-hub")]
110    #[cfg_attr(astarte_device_sdk_docsrs, doc(cfg(feature = "message-hub")))]
111    #[error(transparent)]
112    Grpc(#[from] crate::transport::grpc::GrpcError),
113    /// Infallible error
114    #[doc(hidden)]
115    #[error(transparent)]
116    Infallible(#[from] Infallible),
117}
118
119/// Aggregation error.
120///
121/// This provides additional context in case of an aggregation error
122#[derive(Debug, thiserror::Error)]
123#[error("invalid aggregation for {interface}{path}, expected {exp} but got {got}")]
124#[non_exhaustive]
125pub struct AggregationError {
126    /// Interface name
127    interface: String,
128    /// Path
129    path: String,
130    /// Expected aggregation of the interface.
131    exp: Aggregation,
132    /// The actual aggregation.
133    got: Aggregation,
134}
135
136impl AggregationError {
137    // Public to be used in the derive macro.
138    #[doc(hidden)]
139    pub fn new(
140        interface: impl Into<String>,
141        path: impl Into<String>,
142        exp: Aggregation,
143        got: Aggregation,
144    ) -> Self {
145        Self {
146            interface: interface.into(),
147            path: path.into(),
148            exp,
149            got,
150        }
151    }
152}
153
154/// Invalid interface type when sending or receiving.
155#[derive(Debug, thiserror::Error)]
156#[error("invalid interface type for {name}{}, expected {exp} but got {got}", path.as_deref().unwrap_or_default())]
157pub struct InterfaceTypeError {
158    /// Name of the interface.
159    name: String,
160    /// Optional path
161    path: Option<String>,
162    /// Expected interface type.
163    exp: InterfaceType,
164    /// Actual interface type.
165    got: InterfaceType,
166}
167
168impl InterfaceTypeError {
169    pub(crate) fn new(name: impl Into<String>, exp: InterfaceType, got: InterfaceType) -> Self {
170        Self {
171            name: name.into(),
172            path: None,
173            exp,
174            got,
175        }
176    }
177
178    // Public to be used in the derive macro.
179    #[doc(hidden)]
180    pub fn with_path(
181        name: impl Into<String>,
182        path: impl Into<String>,
183        exp: InterfaceType,
184        got: InterfaceType,
185    ) -> Self {
186        Self {
187            name: name.into(),
188            path: Some(path.into()),
189            exp,
190            got,
191        }
192    }
193}
194
195/// Sending data on an interface not owned by the device
196#[derive(Debug, thiserror::Error)]
197#[error("invalid ownership for interface {name}, expected {exp} but got {got}")]
198pub struct OwnershipError {
199    /// Name of the interface.
200    name: String,
201    /// Expected interface ownership.
202    exp: Ownership,
203    /// Actual interface ownership.
204    got: Ownership,
205}
206
207impl OwnershipError {
208    pub(crate) fn new(name: impl Into<String>, exp: Ownership, got: Ownership) -> Self {
209        Self {
210            name: name.into(),
211            exp,
212            got,
213        }
214    }
215}
216
217/// An error reporter that prints an error and its sources.
218///
219/// This is a stub until the std library implementation get stabilized[^1]
220///
221/// [^1]: https://doc.rust-lang.org/std/error/struct.Report.html
222pub(crate) struct Report<E = Box<dyn std::error::Error>> {
223    /// The error being reported.
224    error: E,
225}
226
227impl<E> Report<E>
228where
229    Report<E>: From<E>,
230{
231    /// Create a new Report from an input error.
232    pub(crate) fn new(error: E) -> Self {
233        Self::from(error)
234    }
235}
236
237impl<E> From<E> for Report<E>
238where
239    E: std::error::Error,
240{
241    fn from(value: E) -> Self {
242        Self { error: value }
243    }
244}
245
246impl<E> Display for Report<E>
247where
248    E: std::error::Error,
249{
250    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
251        write!(f, "{}", self.error)?;
252
253        let mut cause: &dyn std::error::Error = &self.error;
254
255        while let Some(source) = cause.source() {
256            cause = source;
257
258            write!(f, ": {cause}")?;
259        }
260
261        Ok(())
262    }
263}
264
265// This type intentionally outputs the same format for `Display` and `Debug`for
266// situations where you unwrap a `Report` or return it from main.
267impl<E> std::fmt::Debug for Report<E>
268where
269    Report<E>: Display,
270{
271    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
272        Display::fmt(self, f)
273    }
274}