Skip to main content

astarte_interfaces/interface/
mod.rs

1// This file is part of Astarte.
2//
3// Copyright 2021-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//! Provides the functionalities to parse and validate an Astarte interface.
20
21use std::borrow::Cow;
22use std::fmt::Display;
23use std::str::FromStr;
24use std::time::Duration;
25
26use name::InterfaceName;
27use serde::{Deserialize, Serialize};
28use tracing::{info, warn};
29
30use self::datastream::individual::DatastreamIndividual;
31use self::datastream::object::DatastreamObject;
32use self::properties::Properties;
33use self::validation::VersionChange;
34use self::version::InterfaceVersion;
35use crate::error::Error;
36use crate::mapping::{collection::MappingVec, path::MappingPath};
37use crate::schema::{InterfaceJson, Mapping};
38
39// Re export the schema types
40pub use crate::schema::{Aggregation, DatabaseRetentionPolicy, InterfaceType, Ownership};
41
42pub mod datastream;
43pub mod name;
44pub mod properties;
45pub mod validation;
46pub mod version;
47
48/// Maximum number of mappings an interface can have
49///
50/// See the [Astarte interface scheme](https://docs.astarte-platform.org/latest/040-interface_schema.html#astarte-interface-schema-mappings)
51pub const MAX_INTERFACE_MAPPINGS: usize = 1024;
52
53/// Astarte interface implementation.
54///
55/// Should be used only through its conversion methods, not instantiated directly.
56#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
57#[serde(try_from = "InterfaceJson<std::borrow::Cow<str>>")]
58pub struct Interface {
59    inner: InterfaceTypeAggregation,
60}
61
62impl Interface {
63    /// Returns the inner enum of the [`InterfaceTypeAggregation`]
64    pub fn inner(&self) -> &InterfaceTypeAggregation {
65        &self.inner
66    }
67
68    /// Returns the interface name.
69    #[must_use]
70    pub fn interface_name(&self) -> &str {
71        match &self.inner {
72            InterfaceTypeAggregation::DatastreamIndividual(datastream_individual) => {
73                datastream_individual.name()
74            }
75            InterfaceTypeAggregation::DatastreamObject(datastream_object) => {
76                datastream_object.name()
77            }
78            InterfaceTypeAggregation::Properties(property) => property.name(),
79        }
80    }
81
82    /// Returns the interface version.
83    #[must_use]
84    pub fn version(&self) -> InterfaceVersion {
85        match &self.inner {
86            InterfaceTypeAggregation::DatastreamIndividual(datastream_individual) => {
87                datastream_individual.version()
88            }
89            InterfaceTypeAggregation::DatastreamObject(datastream_object) => {
90                datastream_object.version()
91            }
92            InterfaceTypeAggregation::Properties(property) => property.version(),
93        }
94    }
95
96    /// Returns the interface major version.
97    #[must_use]
98    pub fn version_major(&self) -> i32 {
99        match &self.inner {
100            InterfaceTypeAggregation::DatastreamIndividual(datastream_individual) => {
101                datastream_individual.version_major()
102            }
103            InterfaceTypeAggregation::DatastreamObject(datastream_object) => {
104                datastream_object.version_major()
105            }
106            InterfaceTypeAggregation::Properties(property) => property.version_major(),
107        }
108    }
109
110    /// Returns the interface minor version.
111    #[must_use]
112    pub fn version_minor(&self) -> i32 {
113        match &self.inner {
114            InterfaceTypeAggregation::DatastreamIndividual(datastream_individual) => {
115                datastream_individual.version_minor()
116            }
117            InterfaceTypeAggregation::DatastreamObject(datastream_object) => {
118                datastream_object.version_minor()
119            }
120            InterfaceTypeAggregation::Properties(property) => property.version_minor(),
121        }
122    }
123
124    /// Returns the interface type.
125    #[must_use]
126    pub fn interface_type(&self) -> InterfaceType {
127        match &self.inner {
128            InterfaceTypeAggregation::DatastreamIndividual(_)
129            | InterfaceTypeAggregation::DatastreamObject(_) => InterfaceType::Datastream,
130            InterfaceTypeAggregation::Properties(_) => InterfaceType::Properties,
131        }
132    }
133
134    /// Returns the interface ownership.
135    #[must_use]
136    pub fn ownership(&self) -> Ownership {
137        match &self.inner {
138            InterfaceTypeAggregation::DatastreamIndividual(datastream_individual) => {
139                datastream_individual.ownership()
140            }
141            InterfaceTypeAggregation::DatastreamObject(datastream_object) => {
142                datastream_object.ownership()
143            }
144            InterfaceTypeAggregation::Properties(property) => property.ownership(),
145        }
146    }
147
148    /// Returns the interface aggregation.
149    #[must_use]
150    pub fn aggregation(&self) -> Aggregation {
151        match &self.inner {
152            InterfaceTypeAggregation::Properties(_)
153            | InterfaceTypeAggregation::DatastreamIndividual(_) => Aggregation::Individual,
154            InterfaceTypeAggregation::DatastreamObject(_) => Aggregation::Object,
155        }
156    }
157
158    #[cfg(feature = "doc-fields")]
159    #[cfg_attr(docsrs, doc(cfg(feature = "doc-fields")))]
160    /// Returns the interface description
161    #[must_use]
162    pub fn description(&self) -> Option<&str> {
163        match &self.inner {
164            InterfaceTypeAggregation::Properties(interface) => interface.description(),
165            InterfaceTypeAggregation::DatastreamIndividual(interface) => interface.description(),
166            InterfaceTypeAggregation::DatastreamObject(interface) => interface.description(),
167        }
168    }
169
170    #[cfg(feature = "doc-fields")]
171    #[cfg_attr(docsrs, doc(cfg(feature = "doc-fields")))]
172    /// Returns the interface documentation.
173    #[must_use]
174    pub fn doc(&self) -> Option<&str> {
175        match &self.inner {
176            InterfaceTypeAggregation::Properties(interface) => interface.doc(),
177            InterfaceTypeAggregation::DatastreamIndividual(interface) => interface.doc(),
178            InterfaceTypeAggregation::DatastreamObject(interface) => interface.doc(),
179        }
180    }
181
182    /// Validate an interface given the previous version `prev`.
183    ///
184    /// It will check whether:
185    ///
186    /// - Both the versions are valid
187    /// - The name of the interface is the same
188    /// - The new version is a valid successor of the previous version.
189    pub fn validate_with(&self, prev: &Self) -> Result<&Self, Error> {
190        // If the interfaces are the same, they are valid
191        if self == prev {
192            return Ok(self);
193        }
194
195        // Check if the wrong interface was passed
196        let name = self.interface_name();
197        let prev_name = prev.interface_name();
198        if name != prev_name {
199            return Err(Error::NameMismatch {
200                name: name.to_string(),
201                prev_name: prev_name.to_string(),
202            });
203        }
204
205        // Validate the new interface version
206        VersionChange::try_new(self, prev)
207            .map_err(Error::VersionChange)
208            .map(|change| {
209                info!("Interface {} version changed: {}", name, change);
210
211                self
212            })
213    }
214
215    /// Return a reference to a [`DatastreamIndividual`].
216    #[must_use]
217    pub fn as_datastream_individual(&self) -> Option<&DatastreamIndividual> {
218        if let InterfaceTypeAggregation::DatastreamIndividual(v) = &self.inner {
219            Some(v)
220        } else {
221            None
222        }
223    }
224
225    /// Return a reference to a [`DatastreamObject`].
226    #[must_use]
227    pub fn as_datastream_object(&self) -> Option<&DatastreamObject> {
228        if let InterfaceTypeAggregation::DatastreamObject(v) = &self.inner {
229            Some(v)
230        } else {
231            None
232        }
233    }
234
235    /// Return a reference to a [`Properties`].
236    #[must_use]
237    pub fn as_properties(&self) -> Option<&Properties> {
238        if let InterfaceTypeAggregation::Properties(v) = &self.inner {
239            Some(v)
240        } else {
241            None
242        }
243    }
244
245    /// Returns `true` if the interface type is [`DatastreamIndividual`].
246    ///
247    /// [`DatastreamIndividual`]: InterfaceTypeAggregation::DatastreamIndividual
248    #[must_use]
249    pub fn is_datastream_individual(&self) -> bool {
250        matches!(
251            self.inner,
252            InterfaceTypeAggregation::DatastreamIndividual(..)
253        )
254    }
255
256    /// Returns `true` if the interface type is [`DatastreamObject`].
257    ///
258    /// [`DatastreamObject`]: InterfaceTypeAggregation::DatastreamObject
259    #[must_use]
260    pub fn is_datastream_object(&self) -> bool {
261        matches!(self.inner, InterfaceTypeAggregation::DatastreamObject(..))
262    }
263
264    /// Returns `true` if the interface type is [`Properties`].
265    ///
266    /// [`Properties`]: InterfaceTypeAggregation::Properties
267    #[must_use]
268    pub fn is_properties(&self) -> bool {
269        matches!(self.inner, InterfaceTypeAggregation::Properties(..))
270    }
271}
272
273impl<T> TryFrom<InterfaceJson<T>> for Interface
274where
275    T: AsRef<str> + Into<String>,
276{
277    type Error = Error;
278
279    fn try_from(value: InterfaceJson<T>) -> Result<Self, Self::Error> {
280        let inner = match (value.interface_type, value.aggregation.unwrap_or_default()) {
281            (InterfaceType::Datastream, Aggregation::Individual) => {
282                let interface = DatastreamIndividual::try_from(value)?;
283
284                InterfaceTypeAggregation::DatastreamIndividual(interface)
285            }
286            (InterfaceType::Datastream, Aggregation::Object) => {
287                let interface = DatastreamObject::try_from(value)?;
288
289                InterfaceTypeAggregation::DatastreamObject(interface)
290            }
291            (InterfaceType::Properties, Aggregation::Individual) => {
292                let interface = Properties::try_from(value)?;
293
294                InterfaceTypeAggregation::Properties(interface)
295            }
296            (InterfaceType::Properties, Aggregation::Object) => return Err(Error::PropertyObject),
297        };
298
299        Ok(Interface { inner })
300    }
301}
302
303impl<'a> From<&'a Interface> for InterfaceJson<Cow<'a, str>> {
304    fn from(value: &'a Interface) -> Self {
305        match &value.inner {
306            InterfaceTypeAggregation::DatastreamIndividual(datastream_individual) => {
307                Self::from(datastream_individual)
308            }
309            InterfaceTypeAggregation::DatastreamObject(datastream_object) => {
310                Self::from(datastream_object)
311            }
312            InterfaceTypeAggregation::Properties(properties) => Self::from(properties),
313        }
314    }
315}
316
317impl Serialize for Interface {
318    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
319        match &self.inner {
320            InterfaceTypeAggregation::DatastreamIndividual(datastream_individual) => {
321                datastream_individual.serialize(serializer)
322            }
323            InterfaceTypeAggregation::DatastreamObject(datastream_object) => {
324                datastream_object.serialize(serializer)
325            }
326            InterfaceTypeAggregation::Properties(properties) => properties.serialize(serializer),
327        }
328    }
329}
330
331impl FromStr for Interface {
332    type Err = Error;
333
334    fn from_str(s: &str) -> Result<Self, Self::Err> {
335        let interface: InterfaceJson<Cow<str>> = serde_json::from_str(s)?;
336
337        Interface::try_from(interface)
338    }
339}
340
341impl Display for Interface {
342    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343        match &self.inner {
344            InterfaceTypeAggregation::DatastreamIndividual(datastream_individual) => {
345                write!(f, "{datastream_individual}")
346            }
347            InterfaceTypeAggregation::DatastreamObject(datastream_object) => {
348                write!(f, "{datastream_object}")
349            }
350            InterfaceTypeAggregation::Properties(property) => {
351                write!(f, "{property}")
352            }
353        }
354    }
355}
356
357/// Enum of all the types and aggregation of interfaces
358///
359/// This is not a direct representation of only the mapping to permit extensibility of specific
360/// properties present only in some aggregations.
361#[derive(Debug, PartialEq, Eq, Clone)]
362pub enum InterfaceTypeAggregation {
363    /// Interface with type datastream and aggregations individual.
364    DatastreamIndividual(DatastreamIndividual),
365    /// Interface with type datastream and aggregations object.
366    DatastreamObject(DatastreamObject),
367    /// A property interface.
368    Properties(Properties),
369}
370
371/// Defines the retention of a data stream.
372///
373/// Describes what to do with the sent data if the transport is incapable of delivering it.
374///
375/// See [Retention](https://docs.astarte-platform.org/astarte/latest/040-interface_schema.html#astarte-mapping-schema-retention)
376/// for more information.
377#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
378pub enum Retention {
379    /// Data is discarded.
380    #[default]
381    Discard,
382    /// Data is kept in a cache in memory.
383    Volatile {
384        /// Duration for the data to expire.
385        ///
386        /// If it's [`None`] it will never expire.
387        expiry: Option<Duration>,
388    },
389    /// Data is kept on disk.
390    Stored {
391        /// Duration for the data to expire.
392        ///
393        /// If it's [`None`] it will never expire.
394        expiry: Option<Duration>,
395    },
396}
397
398impl Retention {
399    /// Returns `true` if the retention is [`Stored`].
400    ///
401    /// [`Stored`]: Retention::Stored
402    #[must_use]
403    pub const fn is_stored(&self) -> bool {
404        matches!(self, Self::Stored { .. })
405    }
406
407    /// Returns the expiry for the retention.
408    ///
409    /// For the [`Discard`](Retention::Discard) will always return [`None`], while for
410    /// [`Volatile`](Retention::Volatile) or [`Stored`](Retention::Stored) returns the inner expiry
411    /// only if set.
412    #[must_use]
413    pub const fn as_expiry(&self) -> Option<&Duration> {
414        match self {
415            Retention::Discard => None,
416            // Duration is copy
417            Retention::Volatile { expiry } | Retention::Stored { expiry } => expiry.as_ref(),
418        }
419    }
420
421    /// Returns the expiry for the retention in seconds.
422    ///
423    /// For the [`Discard`](Retention::Discard) will always return [`None`], while for
424    /// [`Volatile`](Retention::Volatile) or [`Stored`](Retention::Stored) returns the inner expiry
425    /// only if set.
426    #[must_use]
427    pub fn as_expiry_seconds(&self) -> Option<i64> {
428        self.as_expiry().map(|duration| {
429            // The expiry duration was created from a i64, but we cap at i64::MAX to be sure
430            i64::try_from(duration.as_secs())
431                .inspect_err(|err| warn!(%err, "expiry conversion error"))
432                .unwrap_or(i64::MAX)
433        })
434    }
435
436    /// Returns `true` if the retention is [`Volatile`].
437    ///
438    /// [`Volatile`]: Retention::Volatile
439    #[must_use]
440    pub const fn is_volatile(&self) -> bool {
441        matches!(self, Self::Volatile { .. })
442    }
443
444    /// Returns `true` if the retention is [`Discard`].
445    ///
446    /// [`Discard`]: Retention::Discard
447    #[must_use]
448    pub const fn is_discard(&self) -> bool {
449        matches!(self, Self::Discard)
450    }
451}
452
453impl From<Retention> for crate::schema::Retention {
454    fn from(value: Retention) -> Self {
455        match value {
456            Retention::Discard => crate::schema::Retention::Discard,
457            Retention::Volatile { .. } => crate::schema::Retention::Volatile,
458            Retention::Stored { .. } => crate::schema::Retention::Stored,
459        }
460    }
461}
462
463/// Defines if data should be expired from the database after a given interval.
464///
465/// See [Database Retention Policy](https://docs.astarte-platform.org/astarte/latest/040-interface_schema.html#astarte-mapping-schema-database_retention_policy)
466/// for more information.
467#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
468pub enum DatabaseRetention {
469    /// Data will never expire.
470    #[default]
471    NoTtl,
472    /// Data will live for the ttl.
473    UseTtl {
474        /// Time to live int the database.
475        ttl: Duration,
476    },
477}
478
479impl DatabaseRetention {
480    /// Returns `true` if the database retention is [`NoTtl`].
481    ///
482    /// [`NoTtl`]: DatabaseRetention::NoTtl
483    #[must_use]
484    pub fn is_no_ttl(&self) -> bool {
485        matches!(self, Self::NoTtl)
486    }
487
488    /// Returns `true` if the database retention is [`UseTtl`].
489    ///
490    /// [`UseTtl`]: DatabaseRetention::UseTtl
491    #[must_use]
492    pub fn is_use_ttl(&self) -> bool {
493        matches!(self, Self::UseTtl { .. })
494    }
495
496    /// Returns the duration of the ttl if the policy is [`UseTtl`]
497    ///
498    /// [`UseTtl`]: DatabaseRetention::UseTtl
499    #[must_use]
500    pub fn as_ttl(&self) -> Option<&Duration> {
501        match self {
502            DatabaseRetention::NoTtl => None,
503            DatabaseRetention::UseTtl { ttl } => Some(ttl),
504        }
505    }
506
507    /// Returns the duration of the ttl if the policy is [`UseTtl`]
508    ///
509    /// [`UseTtl`]: DatabaseRetention::UseTtl
510    #[must_use]
511    pub fn as_ttl_secs(&self) -> Option<i64> {
512        self.as_ttl().map(|ttl| {
513            // The expiry duration was created from a i64, but we cap at i64::MAX to be sure
514            i64::try_from(ttl.as_secs())
515                .inspect_err(|err| warn!(%err, "ttl conversion error"))
516                .unwrap_or(i64::MAX)
517        })
518    }
519}
520
521impl From<DatabaseRetention> for DatabaseRetentionPolicy {
522    fn from(value: DatabaseRetention) -> Self {
523        match value {
524            DatabaseRetention::NoTtl => DatabaseRetentionPolicy::NoTtl,
525            DatabaseRetention::UseTtl { .. } => DatabaseRetentionPolicy::UseTtl,
526        }
527    }
528}
529
530/// Access to the information of an interface.
531pub trait Schema {
532    /// Mapping specific for the interface type and aggregation.
533    type Mapping: Sized;
534
535    /// Returns the interface name.
536    fn name(&self) -> &str;
537    /// Returns the interface name.
538    fn interface_name(&self) -> &InterfaceName;
539    /// Returns the interface major version.
540    fn version_major(&self) -> i32;
541    /// Returns the interface minor version.
542    fn version_minor(&self) -> i32;
543    /// Returns the interface version.
544    fn version(&self) -> InterfaceVersion;
545    /// Returns the interface type.
546    fn interface_type(&self) -> InterfaceType;
547    /// Returns the interface ownership.
548    fn ownership(&self) -> Ownership;
549    /// Returns the interface aggregation.
550    fn aggregation(&self) -> Aggregation;
551
552    #[cfg(feature = "doc-fields")]
553    #[cfg_attr(docsrs, doc(cfg(feature = "doc-fields")))]
554    /// Returns the interface description
555    fn description(&self) -> Option<&str>;
556
557    #[cfg(feature = "doc-fields")]
558    #[cfg_attr(docsrs, doc(cfg(feature = "doc-fields")))]
559    /// Returns the interface documentation.
560    fn doc(&self) -> Option<&str>;
561
562    /// Returns an iterator over the interface's mappings.
563    fn iter_mappings(&self) -> impl Iterator<Item = &Self::Mapping>;
564
565    /// Returns the number of Mappings in the interface.
566    fn mappings_len(&self) -> usize;
567
568    /// Returns an iterator over the interface's mappings.
569    fn iter_interface_mappings(&self) -> impl Iterator<Item = Mapping<Cow<'_, str>>>;
570}
571
572/// Access information of an interface with [`Aggregation`] individual.
573pub trait AggregationIndividual: Schema {
574    /// Returns the mapping with the given path.
575    fn mapping(&self, path: &MappingPath) -> Option<&Self::Mapping>;
576}
577
578impl<'a, T> From<&'a T> for InterfaceJson<Cow<'a, str>>
579where
580    T: Schema,
581{
582    fn from(value: &'a T) -> Self {
583        InterfaceJson {
584            interface_name: value.interface_name().as_str().into(),
585            version_major: value.version_major(),
586            version_minor: value.version_minor(),
587            interface_type: value.interface_type(),
588            ownership: value.ownership(),
589            aggregation: Some(value.aggregation()),
590            #[cfg(feature = "doc-fields")]
591            description: value.description().map(Cow::from),
592            #[cfg(feature = "doc-fields")]
593            doc: value.doc().map(Cow::from),
594            #[cfg(not(feature = "doc-fields"))]
595            description: None,
596            #[cfg(not(feature = "doc-fields"))]
597            doc: None,
598            mappings: value.iter_interface_mappings().collect(),
599        }
600    }
601}
602
603#[cfg(test)]
604pub(crate) mod tests {
605    use std::str::FromStr;
606
607    use pretty_assertions::assert_eq;
608
609    use super::*;
610    use crate::{
611        mapping::{InterfaceMapping, MappingError},
612        schema::{InterfaceType, MappingType, Ownership, Reliability},
613        DatastreamIndividual, DatastreamIndividualMapping, Endpoint, Interface, MappingPath,
614        Schema,
615    };
616
617    const E2E_DEVICE_PROPERTY: &str =
618        include_str!("../../interfaces/org.astarte-platform.rust.e2etest.DeviceProperty.json");
619    const E2E_DEVICE_PROPERTY_NAME: &str = "org.astarte-platform.rust.e2etest.DeviceProperty";
620    pub(crate) const E2E_DEVICE_AGGREGATE: &str =
621        include_str!("../../interfaces/org.astarte-platform.rust.e2etest.DeviceAggregate.json");
622    const E2E_DEVICE_AGGREGATE_NAME: &str = "org.astarte-platform.rust.e2etest.DeviceAggregate";
623    const E2E_DEVICE_DATASTREAM: &str =
624        include_str!("../../interfaces/org.astarte-platform.rust.e2etest.DeviceDatastream.json");
625    const E2E_DEVICE_DATASTREAM_NAME: &str = "org.astarte-platform.rust.e2etest.DeviceDatastream";
626
627    // The mappings are sorted alphabetically by endpoint, so we can confront them
628    #[cfg(feature = "doc-fields")]
629    const INTERFACE_JSON: &str = r#"{
630            "interface_name": "org.astarte-platform.genericsensors.Values",
631            "version_major": 1,
632            "version_minor": 0,
633            "type": "datastream",
634            "ownership": "device",
635            "description": "Interface description",
636            "doc": "Interface doc",
637            "mappings": [
638                {
639                    "endpoint": "/%{sensor_id}/otherValue",
640                    "type": "longinteger",
641                    "explicit_timestamp": true,
642                    "description": "Mapping description",
643                    "doc": "Mapping doc"
644                },
645                {
646                    "endpoint": "/%{sensor_id}/value",
647                    "type": "double",
648                    "explicit_timestamp": true,
649                    "description": "Mapping description",
650                    "doc": "Mapping doc"
651                }
652            ]
653        }"#;
654
655    #[cfg(not(feature = "doc-fields"))]
656    const INTERFACE_JSON: &str = r#"{
657            "interface_name": "org.astarte-platform.genericsensors.Values",
658            "version_major": 1,
659            "version_minor": 0,
660            "type": "datastream",
661            "ownership": "device",
662            "mappings": [
663                {
664                    "endpoint": "/%{sensor_id}/otherValue",
665                    "type": "longinteger",
666                    "explicit_timestamp": true
667                },
668                {
669                    "endpoint": "/%{sensor_id}/value",
670                    "type": "double",
671                    "explicit_timestamp": true
672                }
673            ]
674        }"#;
675
676    // The mappings are sorted alphabetically by endpoint, so we can confront them
677    const PROPERTIES_JSON: &str = r#"{
678            "interface_name": "org.astarte-platform.genericproperties.Values",
679            "version_major": 1,
680            "version_minor": 0,
681            "type": "properties",
682            "ownership": "server",
683            "description": "Interface description",
684            "doc": "Interface doc",
685            "mappings": [
686                {
687                    "endpoint": "/%{sensor_id}/aaaa",
688                    "type": "longinteger",
689                    "allow_unset": true
690                },
691                {
692                    "endpoint": "/%{sensor_id}/bbbb",
693                    "type": "double",
694                    "allow_unset": false
695                }
696            ]
697        }"#;
698
699    #[test]
700    fn datastream_interface_deserialization() {
701        let value_mapping = DatastreamIndividualMapping {
702            endpoint: Endpoint::try_from("/%{sensor_id}/value").unwrap(),
703            mapping_type: MappingType::Double,
704            reliability: Reliability::default(),
705            retention: Retention::default(),
706            #[cfg(feature = "server-fields")]
707            database_retention: DatabaseRetention::default(),
708            explicit_timestamp: true,
709            #[cfg(feature = "doc-fields")]
710            description: Some("Mapping description".to_string()),
711            #[cfg(feature = "doc-fields")]
712            doc: Some("Mapping doc".to_string()),
713        };
714
715        let other_value_mapping = DatastreamIndividualMapping {
716            endpoint: Endpoint::try_from("/%{sensor_id}/otherValue").unwrap(),
717            mapping_type: MappingType::LongInteger,
718            reliability: Reliability::default(),
719            retention: Retention::default(),
720            #[cfg(feature = "server-fields")]
721            database_retention: DatabaseRetention::default(),
722            explicit_timestamp: true,
723            #[cfg(feature = "doc-fields")]
724            description: Some("Mapping description".to_string()),
725            #[cfg(feature = "doc-fields")]
726            doc: Some("Mapping doc".to_string()),
727        };
728
729        let interface_name =
730            InterfaceName::try_from("org.astarte-platform.genericsensors.Values").unwrap();
731        let version = InterfaceVersion::try_from((1, 0)).unwrap();
732        let ownership = Ownership::Device;
733        #[cfg(feature = "doc-fields")]
734        let description = Some("Interface description".to_owned());
735        #[cfg(feature = "doc-fields")]
736        let doc = Some("Interface doc".to_owned());
737
738        let mappings = MappingVec::try_from([other_value_mapping, value_mapping].to_vec()).unwrap();
739
740        let datastream_individual = DatastreamIndividual {
741            name: interface_name.into_string(),
742            version,
743            ownership,
744            #[cfg(feature = "doc-fields")]
745            description,
746            #[cfg(feature = "doc-fields")]
747            doc,
748            mappings,
749        };
750
751        let interface = Interface {
752            inner: InterfaceTypeAggregation::DatastreamIndividual(datastream_individual),
753        };
754
755        let deser_interface = Interface::from_str(INTERFACE_JSON).unwrap();
756
757        assert_eq!(interface, deser_interface);
758    }
759
760    #[test]
761    fn must_have_one_mapping() {
762        let json = r#"{
763            "interface_name": "org.astarte-platform.genericproperties.Values",
764            "version_major": 1,
765            "version_minor": 0,
766            "type": "properties",
767            "ownership": "server",
768            "description": "Interface description",
769            "doc": "Interface doc",
770            "mappings": []
771        }"#;
772
773        let interface = Interface::from_str(json);
774
775        let err = interface.unwrap_err();
776        assert!(matches!(err, Error::Mapping(MappingError::Empty)));
777    }
778
779    #[test]
780    fn test_properties() {
781        let interface = Interface::from_str(PROPERTIES_JSON).unwrap();
782
783        let exp = Properties::from_str(PROPERTIES_JSON).unwrap();
784        assert_eq!(
785            *interface.inner(),
786            InterfaceTypeAggregation::Properties(exp)
787        );
788        assert_eq!(interface.interface_type(), InterfaceType::Properties);
789
790        let exp = InterfaceVersion::try_new(1, 0).unwrap();
791        assert_eq!(interface.version(), exp);
792        assert_eq!(interface.version_major(), 1);
793        assert_eq!(interface.version_minor(), 0);
794
795        let InterfaceTypeAggregation::Properties(interface) = interface.inner else {
796            panic!()
797        };
798
799        let paths: Vec<_> = interface.iter_mappings().collect();
800
801        assert_eq!(paths.len(), 2);
802        assert_eq!(paths[0].endpoint().to_string(), "/%{sensor_id}/aaaa");
803        assert_eq!(paths[1].endpoint().to_string(), "/%{sensor_id}/bbbb");
804
805        let path = MappingPath::try_from("/1/aaaa").unwrap();
806
807        let f = interface.mapping(&path).unwrap();
808
809        assert_eq!(f.mapping_type(), MappingType::LongInteger);
810        assert!(f.allow_unset());
811    }
812
813    #[test]
814    fn test_iter_mappings() {
815        let value_mapping = DatastreamIndividualMapping {
816            endpoint: Endpoint::try_from("/%{sensor_id}/value").unwrap(),
817            mapping_type: MappingType::Double,
818            #[cfg(feature = "doc-fields")]
819            description: Some("Mapping description".to_string()),
820            #[cfg(feature = "doc-fields")]
821            doc: Some("Mapping doc".to_string()),
822            reliability: Reliability::default(),
823            retention: Retention::default(),
824            #[cfg(feature = "server-fields")]
825            database_retention: DatabaseRetention::default(),
826            explicit_timestamp: true,
827        };
828
829        let other_value_mapping = DatastreamIndividualMapping {
830            endpoint: Endpoint::try_from("/%{sensor_id}/otherValue").unwrap(),
831            mapping_type: MappingType::LongInteger,
832            #[cfg(feature = "doc-fields")]
833            description: Some("Mapping description".to_string()),
834            #[cfg(feature = "doc-fields")]
835            doc: Some("Mapping doc".to_string()),
836            reliability: Reliability::default(),
837            retention: Retention::default(),
838            #[cfg(feature = "server-fields")]
839            database_retention: DatabaseRetention::default(),
840            explicit_timestamp: true,
841        };
842
843        let interface = Interface::from_str(INTERFACE_JSON).unwrap();
844        let interface = interface.as_datastream_individual().unwrap();
845
846        let mut mappings = interface.iter_mappings();
847
848        assert_eq!(mappings.next(), Some(&other_value_mapping));
849        assert_eq!(mappings.next(), Some(&value_mapping));
850        assert_eq!(mappings.next(), None);
851    }
852
853    #[test]
854    fn methods_test() {
855        let interface = Interface::from_str(INTERFACE_JSON).unwrap();
856
857        assert_eq!(
858            interface.interface_name(),
859            "org.astarte-platform.genericsensors.Values"
860        );
861        assert_eq!(interface.version_major(), 1);
862        assert_eq!(interface.version_minor(), 0);
863        assert_eq!(interface.ownership(), Ownership::Device);
864        #[cfg(feature = "doc-fields")]
865        assert_eq!(interface.description(), Some("Interface description"));
866        assert_eq!(interface.aggregation(), Aggregation::Individual);
867        assert_eq!(interface.interface_type(), InterfaceType::Datastream);
868        #[cfg(feature = "doc-fields")]
869        assert_eq!(interface.doc(), Some("Interface doc"));
870    }
871
872    #[test]
873    fn serialize_and_deserialize() {
874        let interface = Interface::from_str(INTERFACE_JSON).unwrap();
875        let serialized = serde_json::to_string(&interface).unwrap();
876        let deserialized: Interface = serde_json::from_str(&serialized).unwrap();
877
878        assert_eq!(interface, deserialized);
879
880        let value = serde_json::Value::from_str(&serialized).unwrap();
881        let expected = serde_json::Value::from_str(INTERFACE_JSON).unwrap();
882        assert_eq!(value, expected);
883    }
884
885    #[test]
886    fn check_as_prop() {
887        let interface = Interface::from_str(PROPERTIES_JSON).unwrap();
888
889        interface.as_properties().expect("interface is a property");
890
891        let interface = Interface::from_str(INTERFACE_JSON).unwrap();
892
893        assert_eq!(interface.as_properties(), None);
894    }
895
896    #[cfg(feature = "doc-fields")]
897    #[test]
898    fn test_with_escaped_descriptions() {
899        let json = r#"{
900            "interface_name": "org.astarte-platform.genericproperties.Values",
901            "version_major": 1,
902            "version_minor": 0,
903            "type": "properties",
904            "ownership": "server",
905            "description": "Interface description \"escaped\"",
906            "doc": "Interface doc \"escaped\"",
907            "mappings": [{
908                "endpoint": "/double_endpoint",
909                "type": "double",
910                "doc": "Mapping doc \"escaped\""
911            }]
912        }"#;
913
914        let interface = Interface::from_str(json).unwrap();
915        let interface = interface.as_properties().unwrap();
916
917        assert_eq!(
918            interface.description().unwrap(),
919            r#"Interface description "escaped""#
920        );
921        assert_eq!(interface.doc().unwrap(), r#"Interface doc "escaped""#);
922        let mapping_doc = interface
923            .mapping(&MappingPath::try_from("/double_endpoint").unwrap())
924            .unwrap()
925            .doc()
926            .unwrap();
927        assert_eq!(mapping_doc, r#"Mapping doc "escaped""#);
928    }
929
930    #[test]
931    fn should_convert_into_inner() {
932        let interface = Interface::from_str(E2E_DEVICE_PROPERTY).unwrap();
933
934        assert!(interface.as_properties().is_some());
935        assert!(interface.as_datastream_object().is_none());
936        assert!(interface.as_datastream_individual().is_none());
937        assert!(interface.is_properties());
938        assert!(!interface.is_datastream_object());
939        assert!(!interface.is_datastream_object());
940
941        let interface = interface.as_properties().unwrap();
942        assert_eq!(interface.mappings_len(), 14);
943
944        let interface = Interface::from_str(E2E_DEVICE_AGGREGATE).unwrap();
945
946        assert!(interface.as_properties().is_none());
947        assert!(interface.as_datastream_object().is_some());
948        assert!(interface.as_datastream_individual().is_none());
949        assert!(!interface.is_properties());
950        assert!(interface.is_datastream_object());
951        assert!(!interface.is_datastream_individual());
952
953        let interface = interface.as_datastream_object().unwrap();
954        assert_eq!(interface.mappings_len(), 14);
955
956        let interface = Interface::from_str(E2E_DEVICE_DATASTREAM).unwrap();
957
958        assert!(interface.as_properties().is_none());
959        assert!(interface.as_datastream_object().is_none());
960        assert!(interface.as_datastream_individual().is_some());
961        assert!(!interface.is_properties());
962        assert!(!interface.is_datastream_object());
963        assert!(interface.is_datastream_individual());
964
965        let interface = interface.as_datastream_individual().unwrap();
966        assert_eq!(interface.mappings_len(), 14);
967    }
968
969    #[test]
970    fn test_interface_getters() {
971        let version = InterfaceVersion::try_new(0, 1).unwrap();
972
973        let interface = Interface::from_str(E2E_DEVICE_DATASTREAM).unwrap();
974
975        assert_eq!(interface.interface_name(), E2E_DEVICE_DATASTREAM_NAME);
976        assert_eq!(interface.version(), version);
977        assert_eq!(interface.version_major(), 0);
978        assert_eq!(interface.version_minor(), 1);
979        assert_eq!(interface.interface_type(), InterfaceType::Datastream);
980        assert_eq!(interface.ownership(), Ownership::Device);
981        assert_eq!(interface.aggregation(), Aggregation::Individual);
982        #[cfg(feature = "doc-fields")]
983        assert_eq!(interface.description(), Some("Test datastream interface."));
984        #[cfg(feature = "doc-fields")]
985        assert_eq!(
986            interface.doc(),
987            Some("Test interface used to test datastream.")
988        );
989
990        let interface = Interface::from_str(E2E_DEVICE_AGGREGATE).unwrap();
991
992        assert_eq!(interface.interface_name(), E2E_DEVICE_AGGREGATE_NAME);
993        assert_eq!(interface.version(), version);
994        assert_eq!(interface.version_major(), 0);
995        assert_eq!(interface.version_minor(), 1);
996        assert_eq!(interface.interface_type(), InterfaceType::Datastream);
997        assert_eq!(interface.ownership(), Ownership::Device);
998        assert_eq!(interface.aggregation(), Aggregation::Object);
999        #[cfg(feature = "doc-fields")]
1000        assert_eq!(interface.description(), Some("Test aggregate interface."));
1001        #[cfg(feature = "doc-fields")]
1002        assert_eq!(
1003            interface.doc(),
1004            Some("Test interface used to test aggregates.")
1005        );
1006
1007        let interface = Interface::from_str(E2E_DEVICE_PROPERTY).unwrap();
1008
1009        assert_eq!(interface.interface_name(), E2E_DEVICE_PROPERTY_NAME);
1010        assert_eq!(interface.version(), version);
1011        assert_eq!(interface.version_major(), 0);
1012        assert_eq!(interface.version_minor(), 1);
1013        assert_eq!(interface.interface_type(), InterfaceType::Properties);
1014        assert_eq!(interface.ownership(), Ownership::Device);
1015        assert_eq!(interface.aggregation(), Aggregation::Individual);
1016        #[cfg(feature = "doc-fields")]
1017        assert_eq!(interface.description(), Some("Test properties interface."));
1018        #[cfg(feature = "doc-fields")]
1019        assert_eq!(
1020            interface.doc(),
1021            Some("Test interface used to test properties.")
1022        );
1023    }
1024}