Skip to main content

astarte_interfaces/
schema.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//! Astarte Interface definition, this module contains the structs for the actual JSON
20//! representation definition of the [`Interface`](crate::Interface) and mapping.
21//!
22//! For more information see:
23//! [Interface Schema - Astarte](https://docs.astarte-platform.org/astarte/latest/040-interface_schema.html)
24
25use std::{fmt::Display, time::Duration};
26
27use cfg_if::cfg_if;
28use serde::{Deserialize, Serialize};
29
30use crate::interface::{
31    DatabaseRetention as InterfaceDatabaseRetention, Retention as InterfaceRetention,
32};
33
34/// Error when validating an [`InterfaceJson`].
35#[non_exhaustive]
36#[derive(thiserror::Error, Debug)]
37pub enum SchemaError {
38    /// The expiry cannot be negative
39    #[error("expiry cannot be negative {0}")]
40    NegativeExpiry(i64),
41    /// The database retention ttl cannot be negative
42    #[error("database retention ttl cannot be negative {0}")]
43    NegativeDatabaseRetentionTtl(i64),
44    /// The database retention ttl must be greater than 60s
45    #[error("database retention ttl must be greater than 60s, instead of {0}")]
46    DatabaseRetentionTtlTooLow(u64),
47    /// Missing database retention ttl with policy use_ttl
48    #[error("database retention ttl is missing, but policy is use_ttl")]
49    MissingDatabaseRetentionTtl,
50    /// Database retention ttl is set to a non zero value, but database_retention_policy is `no_ttl`
51    #[cfg(feature = "strict")]
52    #[cfg_attr(docsrs, doc(cfg(feature = "strict")))]
53    #[error("database_retention_ttl is set to {0}, but database_retention_policy is no_ttl")]
54    DatabaseRetentionTtlWithNoTtl(i64),
55    /// Expiry is set to a non zero value, but retention is discard
56    #[cfg(feature = "strict")]
57    #[cfg_attr(docsrs, doc(cfg(feature = "strict")))]
58    #[error("expiry is set to {0}, but retention is discard")]
59    ExpiryWithDiscard(i64),
60}
61
62fn is_none_or_empty<T>(value: &Option<T>) -> bool
63where
64    T: AsRef<str>,
65{
66    match value {
67        Some(value) => value.as_ref().is_empty(),
68        None => true,
69    }
70}
71
72fn is_none_or_default<T>(value: &Option<T>) -> bool
73where
74    T: Default + Eq,
75{
76    match value {
77        Some(value) => *value == T::default(),
78        None => true,
79    }
80}
81
82/// The structure is a direct mapping of the JSON schema, they are then transformed in our
83/// internal representation of [Interface](crate::interface::Interface) when de-serializing using
84/// [`TryFrom`].
85///
86/// The fields of the JSON can either be:
87///
88/// - **Required**: the field is the value it represents, it cannot be omitted.
89/// - **Optional with default**: the field is optional, but it is value it represents (not wrapped
90///   in [`Option`]). It will not be serialized if the value is the default one.
91/// - **Optional**: the field is optional, it is wrapped in [`Option`]. It will not be serialized if
92///   the value is [`None`].
93#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
94#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
95pub struct InterfaceJson<T>
96where
97    T: AsRef<str>,
98{
99    /// The name of the interface.
100    ///
101    /// This has to be an unique, alphanumeric reverse internet domain
102    /// name, shorter than 128 characters.
103    pub(crate) interface_name: T,
104    /// The Major version qualifier for this interface.
105    ///
106    /// Interfaces with the same id and different `version_major` number are deemed incompatible. It
107    /// is then acceptable to redefine any property of the interface when changing the major
108    /// version number.
109    ///
110    /// It must be a positive number.
111    pub(crate) version_major: i32,
112    /// The Minor version qualifier for this interface.
113    ///
114    /// Interfaces with the same id and major version number and different `version_minor` number are
115    /// deemed compatible between each other. When changing the minor number, it is then only
116    /// possible to insert further mappings. Any other modification might lead to
117    /// incompatibilities and undefined behavior.
118    ///
119    /// It must be a positive number.
120    pub(crate) version_minor: i32,
121    /// Identifies the type of this Interface.
122    ///
123    /// Currently two types are supported: datastream and properties. Datastream should be used when
124    /// dealing with streams of non-persistent data, where a single path receives updates and
125    /// there's no concept of state. Properties, instead, are meant to be an actual state and as
126    /// such they have only a change history, and are retained.
127    #[serde(rename = "type")]
128    pub(crate) interface_type: InterfaceType,
129    /// Identifies the direction of the interface.
130    ///
131    /// Interfaces are meant to be unidirectional, and this property defines who's sending or
132    /// receiving data. device means the device/gateway is sending data to Astarte, consumer
133    /// means the device/gateway is receiving data from Astarte. Bidirectional mode is not
134    /// supported, you should instantiate another interface for that.
135    pub(crate) ownership: Ownership,
136    /// Identifies the aggregation of the mappings of the interface.
137    ///
138    /// Individual means every mapping changes state or streams data independently, whereas an
139    /// object aggregation treats the interface as an object, making all the mappings changes
140    /// interdependent. Choosing the right aggregation might drastically improve performances.
141    #[serde(default, skip_serializing_if = "is_none_or_default")]
142    pub(crate) aggregation: Option<Aggregation>,
143    /// An optional description of the interface.
144    #[serde(default, skip_serializing_if = "is_none_or_empty")]
145    pub(crate) description: Option<T>,
146    /// A string containing documentation that will be injected in the generated client code.
147    #[serde(default, skip_serializing_if = "is_none_or_empty")]
148    pub(crate) doc: Option<T>,
149    /// Mappings define the endpoint of the interface, where actual data is stored/streamed.
150    ///
151    /// They are defined as relative URLs (e.g. /my/path) and can be parametrized (e.g.:
152    /// /%{myparam}/path). A valid interface must have no mappings clash, which means that every
153    /// mapping must resolve to a unique path or collection of paths (including
154    /// parametrization). Every mapping acquires type, quality and aggregation of the interface.
155    pub(crate) mappings: Vec<Mapping<T>>,
156}
157
158impl<T> InterfaceJson<T>
159where
160    T: AsRef<str>,
161{
162    /// The name of the interface.
163    ///
164    /// This has to be an unique, alphanumeric reverse internet domain
165    /// name, shorter than 128 characters.
166    pub fn interface_name(&self) -> &str {
167        self.interface_name.as_ref()
168    }
169
170    /// The Major version qualifier for this interface.
171    ///
172    /// Interfaces with the same id and different `version_major` number are deemed incompatible. It
173    /// is then acceptable to redefine any property of the interface when changing the major
174    /// version number.
175    ///
176    /// It must be a positive number.
177    pub fn version_major(&self) -> i32 {
178        self.version_major
179    }
180
181    /// The Minor version qualifier for this interface.
182    ///
183    /// Interfaces with the same id and major version number and different `version_minor` number are
184    /// deemed compatible between each other. When changing the minor number, it is then only
185    /// possible to insert further mappings. Any other modification might lead to
186    /// incompatibilities and undefined behavior.
187    ///
188    /// It must be a positive number.
189    pub fn version_minor(&self) -> i32 {
190        self.version_minor
191    }
192
193    /// Identifies the type of this Interface.
194    ///
195    /// Currently two types are supported: datastream and properties. Datastream should be used when
196    /// dealing with streams of non-persistent data, where a single path receives updates and
197    /// there's no concept of state. Properties, instead, are meant to be an actual state and as
198    /// such they have only a change history, and are retained.
199    pub fn interface_type(&self) -> InterfaceType {
200        self.interface_type
201    }
202
203    /// Identifies the direction of the interface.
204    ///
205    /// Interfaces are meant to be unidirectional, and this property defines who's sending or
206    /// receiving data. device means the device/gateway is sending data to Astarte, consumer
207    /// means the device/gateway is receiving data from Astarte. Bidirectional mode is not
208    /// supported, you should instantiate another interface for that.
209    pub fn ownership(&self) -> Ownership {
210        self.ownership
211    }
212
213    /// Identifies the aggregation of the mappings of the interface.
214    ///
215    /// Individual means every mapping changes state or streams data independently, whereas an
216    /// object aggregation treats the interface as an object, making all the mappings changes
217    /// interdependent. Choosing the right aggregation might drastically improve performances.
218    pub fn aggregation(&self) -> Option<Aggregation> {
219        self.aggregation
220    }
221
222    /// An optional description of the interface.
223    pub fn description(&self) -> Option<&T> {
224        self.description.as_ref()
225    }
226
227    /// A string containing documentation that will be injected in the generated client code.
228    pub fn doc(&self) -> Option<&T> {
229        self.doc.as_ref()
230    }
231
232    /// Mappings define the endpoint of the interface, where actual data is stored/streamed.
233    ///
234    /// They are defined as relative URLs (e.g. /my/path) and can be parametrized (e.g.:
235    /// /%{myparam}/path). A valid interface must have no mappings clash, which means that every
236    /// mapping must resolve to a unique path or collection of paths (including
237    /// parametrization). Every mapping acquires type, quality and aggregation of the interface.
238    pub fn mappings(&self) -> &[Mapping<T>] {
239        &self.mappings
240    }
241}
242
243/// Mapping of an Interface.
244///
245/// It includes all the fields available for a mapping, but it it is validated when built with the
246/// [`TryFrom`]. It uniforms the different types of mappings like
247/// [`DatastreamIndividualMapping`](crate::mapping::datastream::individual::DatastreamIndividualMapping),
248/// [`DatastreamObjectMapping`](crate::mapping::datastream::object::DatastreamObjectMapping)
249/// mappings and [`PropertiesMapping`](super::mapping::properties::PropertiesMapping) in a single
250/// struct.
251///
252/// Since it's a 1:1 representation of the JSON it is used for serialization and deserialization,
253/// and then is converted to the internal representation of the mapping with the [`TryFrom`] and
254/// [`From`] traits of the [`Interface`](crate::Interface)'s mappings.
255//
256/// You can find the specification here [Mapping Schema -
257/// Astarte](https://docs.astarte-platform.org/astarte/latest/040-interface_schema.html#mapping)
258#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
259#[cfg_attr(feature = "strict", serde(deny_unknown_fields))]
260pub struct Mapping<T>
261where
262    T: AsRef<str>,
263{
264    /// Path of the mapping.
265    ///
266    /// It can be parametrized (e.g. `/foo/%{path}/baz`).
267    pub(crate) endpoint: T,
268    /// Defines the type of the mapping.
269    ///
270    /// This represent the data that will be published on the mapping.
271    #[serde(rename = "type")]
272    pub(crate) mapping_type: MappingType,
273    /// Defines when to consider the data delivered.
274    ///
275    /// Useful only with datastream. Defines whether the sent data should be considered delivered
276    /// when the transport successfully sends the data (unreliable), when we know that the data has
277    /// been received at least once (guaranteed) or when we know that the data has been received
278    /// exactly once (unique). Unreliable by default. When using reliable data, consider you might
279    /// incur in additional resource usage on both the transport and the device's end.
280    #[serde(default, skip_serializing_if = "is_none_or_default")]
281    pub(crate) reliability: Option<Reliability>,
282    /// Allow to set a custom timestamp.
283    ///
284    /// Otherwise a timestamp is added when the message is received. If true explicit timestamp will
285    /// also be used for sorting. This feature is only supported on datastreams.
286    #[serde(default, skip_serializing_if = "is_none_or_default")]
287    pub(crate) explicit_timestamp: Option<bool>,
288    /// Retention of the data when not deliverable.
289    ///
290    /// Useful only with datastream. Defines whether the sent data should be discarded if the
291    /// transport is temporarily uncapable of delivering it (discard) or should be kept in a cache in
292    /// memory (volatile) or on disk (stored), and guaranteed to be delivered in the timeframe
293    /// defined by the expiry.
294    #[serde(default, skip_serializing_if = "is_none_or_default")]
295    pub(crate) retention: Option<Retention>,
296    /// Expiry for the retain data.
297    ///
298    /// Useful when retention is stored. Defines after how many seconds a specific data entry should
299    /// be kept before giving up and erasing it from the persistent cache. A value <= 0 means the
300    /// persistent cache never expires, and is the default.
301    #[serde(default, skip_serializing_if = "is_none_or_default")]
302    pub(crate) expiry: Option<i64>,
303    /// Retention policy for the database.
304    ///
305    /// Useful only with datastream. Defines whether data should expire from the database after a
306    /// given interval. Valid values are: `no_ttl` and `use_ttl`.
307    #[serde(default, skip_serializing_if = "is_none_or_default")]
308    pub(crate) database_retention_policy: Option<DatabaseRetentionPolicy>,
309    /// Seconds to keep the data in the database.
310    ///
311    /// Useful when `database_retention_policy` is "`use_ttl`". Defines how many seconds a specific data
312    /// entry should be kept before erasing it from the database.
313    #[serde(default, skip_serializing_if = "is_none_or_default")]
314    pub(crate) database_retention_ttl: Option<i64>,
315    /// Allows the property to be unset.
316    ///
317    /// Used only with properties.
318    #[serde(default, skip_serializing_if = "is_none_or_default")]
319    pub(crate) allow_unset: Option<bool>,
320    /// Marks the mapping as required.
321    ///
322    /// Used only with object datastream.
323    #[serde(default, skip_serializing_if = "is_none_or_default")]
324    pub(crate) required: Option<bool>,
325    /// An optional description of the mapping.
326    #[serde(default, skip_serializing_if = "is_none_or_empty")]
327    pub(crate) description: Option<T>,
328    /// A string containing documentation that will be injected in the generated client code.
329    #[serde(default, skip_serializing_if = "is_none_or_empty")]
330    pub(crate) doc: Option<T>,
331}
332
333impl<T> Mapping<T>
334where
335    T: AsRef<str>,
336{
337    /// Path of the mapping.
338    ///
339    /// It can be parametrized (e.g. `/foo/%{path}/baz`).
340    pub fn endpoint(&self) -> &str {
341        self.endpoint.as_ref()
342    }
343
344    /// Defines the type of the mapping.
345    ///
346    /// This represent the data that will be published on the mapping.
347    pub fn mapping_type(&self) -> MappingType {
348        self.mapping_type
349    }
350
351    /// Defines when to consider the data delivered.
352    ///
353    /// Useful only with datastream. Defines whether the sent data should be considered delivered
354    /// when the transport successfully sends the data (unreliable), when we know that the data has
355    /// been received at least once (guaranteed) or when we know that the data has been received
356    /// exactly once (unique). Unreliable by default. When using reliable data, consider you might
357    /// incur in additional resource usage on both the transport and the device's end.
358    pub fn reliability(&self) -> Option<Reliability> {
359        self.reliability
360    }
361
362    /// Allow to set a custom timestamp.
363    ///
364    /// Otherwise a timestamp is added when the message is received. If true explicit timestamp will
365    /// also be used for sorting. This feature is only supported on datastreams.
366    pub fn explicit_timestamp(&self) -> Option<bool> {
367        self.explicit_timestamp
368    }
369
370    /// Retention of the data when not deliverable.
371    ///
372    /// Useful only with datastream. Defines whether the sent data should be discarded if the
373    /// transport is temporarily uncapable of delivering it (discard) or should be kept in a cache in
374    /// memory (volatile) or on disk (stored), and guaranteed to be delivered in the timeframe
375    /// defined by the expiry.
376    pub fn retention(&self) -> Option<Retention> {
377        self.retention
378    }
379
380    /// Expiry for the retain data.
381    ///
382    /// Useful when retention is stored. Defines after how many seconds a specific data entry should
383    /// be kept before giving up and erasing it from the persistent cache. A value <= 0 means the
384    /// persistent cache never expires, and is the default.
385    pub fn expiry(&self) -> Option<i64> {
386        self.expiry
387    }
388
389    /// Retention policy for the database.
390    ///
391    /// Useful only with datastream. Defines whether data should expire from the database after a
392    /// given interval. Valid values are: `no_ttl` and `use_ttl`.
393    pub fn database_retention_policy(&self) -> Option<DatabaseRetentionPolicy> {
394        self.database_retention_policy
395    }
396
397    /// Seconds to keep the data in the database.
398    ///
399    /// Useful when `database_retention_policy` is "`use_ttl`". Defines how many seconds a specific data
400    /// entry should be kept before erasing it from the database.
401    pub fn database_retention_ttl(&self) -> Option<i64> {
402        self.database_retention_ttl
403    }
404
405    /// Allows the property to be unset.
406    ///
407    /// Used only with properties.
408    pub fn allow_unset(&self) -> Option<bool> {
409        self.allow_unset
410    }
411
412    /// Marks the mapping as required.
413    ///
414    /// Used only with object datastream.
415    pub fn required(&self) -> Option<bool> {
416        self.required
417    }
418
419    /// An optional description of the mapping.
420    pub fn description(&self) -> Option<&T> {
421        self.description.as_ref()
422    }
423
424    /// A string containing documentation that will be injected in the generated client code.
425    pub fn doc(&self) -> Option<&T> {
426        self.doc.as_ref()
427    }
428
429    /// Expiry of the data stream.
430    ///
431    /// If it's [`None`] the stream will never expire.
432    pub fn expiry_as_duration(&self) -> Result<Option<Duration>, SchemaError> {
433        self.expiry
434            .filter(|expiry| *expiry != 0)
435            .map(|expiry| {
436                u64::try_from(expiry)
437                    .map(Duration::from_secs)
438                    .map_err(|_| SchemaError::NegativeExpiry(expiry))
439            })
440            .transpose()
441    }
442
443    /// Retention of the data stream.
444    ///
445    /// See the [`Retention`] documentation for more information.
446    pub fn retention_with_expiry(&self) -> Result<InterfaceRetention, SchemaError> {
447        let retention = match self.retention.unwrap_or_default() {
448            Retention::Discard => {
449                if let Some(expiry) = self.expiry.filter(|exp| *exp > 0) {
450                    cfg_if! {
451                        if #[cfg(feature = "strict")] {
452                            return Err(SchemaError::ExpiryWithDiscard(expiry));
453                        } else {
454                            tracing::warn!(expiry, "discard retention policy with expiry set, ignoring expiry");
455                        }
456                    }
457                }
458
459                InterfaceRetention::Discard
460            }
461            Retention::Volatile => InterfaceRetention::Volatile {
462                expiry: self.expiry_as_duration()?,
463            },
464            Retention::Stored => InterfaceRetention::Stored {
465                expiry: self.expiry_as_duration()?,
466            },
467        };
468
469        Ok(retention)
470    }
471
472    /// Database retention ttl of the data stream.
473    ///
474    /// If it's [`None`] the stream will never expire.
475    pub fn database_retention_ttl_as_duration(&self) -> Result<Option<Duration>, SchemaError> {
476        let Some(ttl) = self.database_retention_ttl else {
477            return Ok(None);
478        };
479
480        let ttl = u64::try_from(ttl).map_err(|_| SchemaError::NegativeDatabaseRetentionTtl(ttl))?;
481
482        if ttl < 60 {
483            return Err(SchemaError::DatabaseRetentionTtlTooLow(ttl));
484        }
485
486        Ok(Some(Duration::from_secs(ttl)))
487    }
488
489    /// Returns the database retention of the data stream.
490    ///
491    /// See the [`DatabaseRetention`](InterfaceDatabaseRetention) for more information.
492    pub fn database_retention_with_ttl(&self) -> Result<InterfaceDatabaseRetention, SchemaError> {
493        match self.database_retention_policy.unwrap_or_default() {
494            DatabaseRetentionPolicy::NoTtl => {
495                if let Some(ttl) = self.database_retention_ttl.filter(|ttl| *ttl > 0) {
496                    cfg_if! {
497                        if #[cfg(feature = "strict")] {
498                            return Err(SchemaError::DatabaseRetentionTtlWithNoTtl(ttl))
499                        } else {
500                            tracing::warn!(ttl, "no_ttl retention policy with ttl set, ignoring ttl");
501                        }
502                    }
503                }
504
505                Ok(InterfaceDatabaseRetention::NoTtl)
506            }
507            DatabaseRetentionPolicy::UseTtl => {
508                let ttl = self
509                    .database_retention_ttl_as_duration()
510                    .and_then(|opt| opt.ok_or(SchemaError::MissingDatabaseRetentionTtl))?;
511
512                Ok(InterfaceDatabaseRetention::UseTtl { ttl })
513            }
514        }
515    }
516}
517
518/// Type of an interface.
519///
520/// See [Interface Schema](https://docs.astarte-platform.org/latest/040-interface_schema.html#reference-astarte-interface-schema)
521/// for more information.
522#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy)]
523#[serde(rename_all = "snake_case")]
524pub enum InterfaceType {
525    /// Stream of non persistent data.
526    Datastream,
527    /// Stateful value.
528    Properties,
529}
530
531impl Display for InterfaceType {
532    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
533        match self {
534            InterfaceType::Datastream => write!(f, "datastream"),
535            InterfaceType::Properties => write!(f, "properties"),
536        }
537    }
538}
539
540/// Ownership of an interface.
541///
542/// See [Interface Schema](https://docs.astarte-platform.org/latest/040-interface_schema.html#reference-astarte-interface-schema)
543/// for more information.
544#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug, Copy, Clone)]
545#[serde(rename_all = "snake_case")]
546pub enum Ownership {
547    /// Data is sent from the device to Astarte.
548    Device,
549    /// Data is received from Astarte.
550    Server,
551}
552
553impl Ownership {
554    /// Returns `true` if the ownership is [`Device`].
555    ///
556    /// [`Device`]: Ownership::Device
557    #[must_use]
558    pub fn is_device(&self) -> bool {
559        matches!(self, Self::Device)
560    }
561
562    /// Returns `true` if the ownership is [`Server`].
563    ///
564    /// [`Server`]: Ownership::Server
565    #[must_use]
566    pub fn is_server(&self) -> bool {
567        matches!(self, Self::Server)
568    }
569}
570
571impl Display for Ownership {
572    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
573        match self {
574            Ownership::Device => write!(f, "device"),
575            Ownership::Server => write!(f, "server"),
576        }
577    }
578}
579
580/// Aggregation of interface's mappings.
581///
582/// See [Interface Schema](https://docs.astarte-platform.org/latest/040-interface_schema.html#reference-astarte-interface-schema)
583/// for more information.
584#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Default)]
585#[serde(rename_all = "snake_case")]
586pub enum Aggregation {
587    /// Every mapping changes state or streams data independently.
588    #[default]
589    Individual,
590    /// Send all the data for every mapping as a single object.
591    Object,
592}
593
594impl Display for Aggregation {
595    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
596        match self {
597            Aggregation::Individual => write!(f, "individual"),
598            Aggregation::Object => write!(f, "object"),
599        }
600    }
601}
602
603/// Defines the type of the mapping.
604#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy)]
605#[serde(rename_all = "lowercase")]
606pub enum MappingType {
607    /// Double mapping.
608    Double,
609    /// Integer mapping.
610    Integer,
611    /// Boolean mapping.
612    Boolean,
613    /// Long integers mapping.
614    LongInteger,
615    /// String mapping.
616    String,
617    /// Binary mapping.
618    BinaryBlob,
619    /// Date time mapping.
620    DateTime,
621    /// Double array mapping.
622    DoubleArray,
623    /// Integer array mapping.
624    IntegerArray,
625    /// Boolean array mapping.
626    BooleanArray,
627    /// Long integer array mapping.
628    LongIntegerArray,
629    /// String array mapping.
630    StringArray,
631    /// Binary array mapping.
632    BinaryBlobArray,
633    /// Date time array mapping.
634    DateTimeArray,
635}
636
637impl Display for MappingType {
638    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
639        match self {
640            MappingType::Double => write!(f, "double"),
641            MappingType::Integer => write!(f, "integer"),
642            MappingType::Boolean => write!(f, "boolean"),
643            MappingType::LongInteger => write!(f, "longinteger"),
644            MappingType::String => write!(f, "string"),
645            MappingType::BinaryBlob => write!(f, "binaryblob"),
646            MappingType::DateTime => write!(f, "datetime"),
647            MappingType::DoubleArray => write!(f, "doublearray"),
648            MappingType::IntegerArray => write!(f, "integerarray"),
649            MappingType::BooleanArray => write!(f, "booleanarray"),
650            MappingType::LongIntegerArray => write!(f, "longintegerarray"),
651            MappingType::StringArray => write!(f, "stringarray"),
652            MappingType::BinaryBlobArray => write!(f, "binaryblobarray"),
653            MappingType::DateTimeArray => write!(f, "datetimearray"),
654        }
655    }
656}
657
658/// Reliability of a data stream.
659///
660/// Defines whether the sent data should be considered delivered.
661///
662/// Properties have always a unique reliability.
663///
664/// See [Reliability](https://docs.astarte-platform.org/astarte/latest/040-interface_schema.html#astarte-mapping-schema-reliability)
665/// for more information.
666#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Copy, Clone, Default, PartialOrd, Ord)]
667#[serde(rename_all = "snake_case")]
668pub enum Reliability {
669    /// If the transport sends the data
670    #[default]
671    Unreliable,
672    /// When we know the data has been received at least once.
673    Guaranteed,
674    /// When we know the data has been received exactly once.
675    Unique,
676}
677
678impl Reliability {
679    /// Returns `true` if the reliability is [`Unreliable`].
680    ///
681    /// [`Unreliable`]: Reliability::Unreliable
682    #[must_use]
683    pub fn is_unreliable(&self) -> bool {
684        matches!(self, Self::Unreliable)
685    }
686
687    /// Returns `true` if the reliability is [`Guaranteed`].
688    ///
689    /// [`Guaranteed`]: Reliability::Guaranteed
690    #[must_use]
691    pub fn is_guaranteed(&self) -> bool {
692        matches!(self, Self::Guaranteed)
693    }
694
695    /// Returns `true` if the reliability is [`Unique`].
696    ///
697    /// [`Unique`]: Reliability::Unique
698    #[must_use]
699    pub fn is_unique(&self) -> bool {
700        matches!(self, Self::Unique)
701    }
702}
703
704impl Display for Reliability {
705    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
706        match self {
707            Reliability::Unreliable => write!(f, "unreliable"),
708            Reliability::Guaranteed => write!(f, "guaranteed"),
709            Reliability::Unique => write!(f, "unique"),
710        }
711    }
712}
713
714/// Defines the retention of a data stream.
715///
716/// Describes what to do with the sent data if the transport is incapable of delivering it.
717///
718/// See [Retention](https://docs.astarte-platform.org/astarte/latest/040-interface_schema.html#astarte-mapping-schema-retention)
719/// for more information.
720#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Copy, Clone, Default)]
721#[serde(rename_all = "snake_case")]
722pub enum Retention {
723    /// Data is discarded.
724    #[default]
725    Discard,
726    /// Data is kept in a cache in memory.
727    Volatile,
728    /// Data is kept on disk.
729    Stored,
730}
731
732impl Display for Retention {
733    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
734        match self {
735            Retention::Discard => write!(f, "discard"),
736            Retention::Volatile => write!(f, "volatile"),
737            Retention::Stored => write!(f, "stored"),
738        }
739    }
740}
741
742/// Defines whether data should expire from the database after a given interval.
743///
744/// See
745/// [Database Retention Policy](https://docs.astarte-platform.org/astarte/latest/040-interface_schema.html#astarte-mapping-schema-database_retention_policy)
746/// for more information.
747#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Copy, Clone, Default)]
748#[serde(rename_all = "snake_case")]
749pub enum DatabaseRetentionPolicy {
750    /// The data will never expiry.
751    #[default]
752    NoTtl,
753    /// The data will expire after the ttl.
754    ///
755    /// The field [`database_retention_ttl`](Mapping::database_retention_ttl) will be used to
756    /// determine how many seconds the data is kept in the database.
757    UseTtl,
758}
759
760impl Display for DatabaseRetentionPolicy {
761    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
762        match self {
763            DatabaseRetentionPolicy::NoTtl => write!(f, "no_ttl"),
764            DatabaseRetentionPolicy::UseTtl => write!(f, "use_ttl"),
765        }
766    }
767}
768
769#[cfg(test)]
770mod tests {
771    use pretty_assertions::assert_eq;
772
773    use super::*;
774
775    #[cfg(feature = "strict")]
776    #[test]
777    fn should_be_strict() {
778        let json = r#"{
779            "interfaceS_name": "org.astarte-platform.genericproperties.Values",
780            "version_major": 1,
781            "version_minor": 0,
782            "type": "properties",
783            "ownArship": "server",
784            "description": "Interface description \"escaped\"",
785            "doc": "Interface doc \"escaped\"",
786            "mappings": [{
787                "endpoint": "/double_endpoint",
788                "type": "double",
789                "doc": "Mapping doc \"escaped\""
790            }]
791        }"#;
792
793        serde_json::from_str::<InterfaceJson<String>>(json)
794            .expect_err("should error for misspelled fields");
795    }
796
797    #[test]
798    fn should_get_expiry() {
799        let json = |expiry: i64| {
800            format!(
801                r#"{{
802            "interface_name": "org.astarte-platform.genericproperties.Values",
803            "version_major": 1,
804            "version_minor": 0,
805            "type": "properties",
806            "ownership": "server",
807            "mappings": [{{
808                "endpoint": "/double_endpoint",
809                "expiry": {expiry},
810                "type": "double"
811            }}]
812        }}"#
813            )
814        };
815
816        let i = serde_json::from_str::<InterfaceJson<String>>(&json(10)).unwrap();
817
818        let mapping = i.mappings.first().unwrap();
819
820        assert_eq!(mapping.expiry, Some(10));
821        assert_eq!(
822            mapping.expiry_as_duration().unwrap(),
823            Some(Duration::from_secs(10))
824        );
825
826        let i: InterfaceJson<String> = serde_json::from_str(&json(-42)).unwrap();
827
828        let mapping = i.mappings.first().unwrap();
829
830        assert_eq!(mapping.expiry, Some(-42));
831        assert!(matches!(
832            mapping.expiry_as_duration().unwrap_err(),
833            SchemaError::NegativeExpiry(-42)
834        ));
835
836        let i: InterfaceJson<String> = serde_json::from_str(&json(0)).unwrap();
837
838        let mapping = i.mappings.first().unwrap();
839
840        assert_eq!(mapping.expiry, Some(0));
841        assert_eq!(mapping.expiry_as_duration().unwrap(), None);
842
843        let i: InterfaceJson<String> = serde_json::from_str(&json(1)).unwrap();
844
845        let mapping = i.mappings.first().unwrap();
846
847        assert_eq!(mapping.expiry, Some(1));
848        assert_eq!(
849            mapping.expiry_as_duration().unwrap(),
850            Some(Duration::from_secs(1))
851        );
852    }
853
854    #[test]
855    fn should_get_retention() {
856        let json = |ttl: i64| {
857            serde_json::from_str::<InterfaceJson<String>>(&format!(
858                r#"{{
859            "interface_name": "org.astarte-platform.genericproperties.Values",
860            "version_major": 1,
861            "version_minor": 0,
862            "type": "properties",
863            "ownership": "server",
864            "mappings": [{{
865                "endpoint": "/double_endpoint",
866                "database_retention_policy": "use_ttl",
867                "database_retention_ttl": {ttl},
868                "type": "double"
869            }}]
870        }}"#
871            ))
872            .unwrap()
873        };
874
875        let i = json(60);
876
877        let mapping = i.mappings.first().unwrap();
878
879        assert_eq!(mapping.database_retention_ttl, Some(60));
880        assert_eq!(
881            mapping.database_retention_with_ttl().unwrap(),
882            InterfaceDatabaseRetention::UseTtl {
883                ttl: Duration::from_secs(60)
884            }
885        );
886
887        let i = json(0);
888
889        let mapping = i.mappings.first().unwrap();
890
891        assert_eq!(mapping.database_retention_ttl, Some(0));
892        assert!(matches!(
893            mapping.database_retention_with_ttl().unwrap_err(),
894            SchemaError::DatabaseRetentionTtlTooLow(0)
895        ));
896
897        let i = json(-32);
898
899        let mapping = i.mappings.first().unwrap();
900
901        assert_eq!(mapping.database_retention_ttl, Some(-32));
902        assert!(matches!(
903            mapping.database_retention_with_ttl().unwrap_err(),
904            SchemaError::NegativeDatabaseRetentionTtl(-32)
905        ));
906    }
907
908    #[test]
909    fn retention_and_expiry() {
910        let mut mapping = Mapping {
911            endpoint: "/some/path",
912            mapping_type: MappingType::Boolean,
913            reliability: None,
914            explicit_timestamp: None,
915            retention: Some(Retention::Discard),
916            expiry: Some(420),
917            database_retention_policy: None,
918            database_retention_ttl: None,
919            allow_unset: None,
920            required: None,
921            description: None,
922            doc: None,
923        };
924
925        cfg_if! {
926            if #[cfg(feature = "strict")] {
927                let err = mapping.retention_with_expiry().unwrap_err();
928                assert!(matches!(err, SchemaError::ExpiryWithDiscard(420)));
929            } else {
930                let retention = mapping.retention_with_expiry().unwrap();
931                assert_eq!(retention, InterfaceRetention::Discard);
932           }
933        }
934
935        mapping.retention = Some(Retention::Volatile);
936
937        let exp = InterfaceRetention::Volatile {
938            expiry: Some(Duration::from_secs(420)),
939        };
940        assert_eq!(mapping.retention_with_expiry().unwrap(), exp);
941
942        mapping.retention = Some(Retention::Stored);
943
944        let exp = InterfaceRetention::Stored {
945            expiry: Some(Duration::from_secs(420)),
946        };
947        assert_eq!(mapping.retention_with_expiry().unwrap(), exp);
948
949        mapping.expiry = None;
950
951        let exp = InterfaceRetention::Stored { expiry: None };
952        assert_eq!(mapping.retention_with_expiry().unwrap(), exp);
953    }
954
955    #[test]
956    fn database_retention_ttl() {
957        let mut mapping = Mapping {
958            endpoint: "/some/path",
959            mapping_type: MappingType::Boolean,
960            reliability: None,
961            explicit_timestamp: None,
962            retention: None,
963            expiry: None,
964            database_retention_policy: Some(DatabaseRetentionPolicy::NoTtl),
965            database_retention_ttl: Some(420),
966            allow_unset: None,
967            required: None,
968            description: None,
969            doc: None,
970        };
971
972        cfg_if! {
973            if #[cfg(feature = "strict")] {
974                let err = mapping.database_retention_with_ttl().unwrap_err();
975                assert!(matches!(err, SchemaError::DatabaseRetentionTtlWithNoTtl(420)));
976            } else {
977                let retention = mapping.database_retention_with_ttl().unwrap();
978                assert_eq!(retention, InterfaceDatabaseRetention::NoTtl);
979           }
980        }
981
982        mapping.database_retention_policy = Some(DatabaseRetentionPolicy::UseTtl);
983
984        let exp = InterfaceDatabaseRetention::UseTtl {
985            ttl: Duration::from_secs(420),
986        };
987        assert_eq!(mapping.database_retention_with_ttl().unwrap(), exp);
988
989        mapping.database_retention_ttl = None;
990
991        assert_eq!(mapping.database_retention_ttl_as_duration().unwrap(), None);
992    }
993
994    #[test]
995    fn interface_type_functions() {
996        assert_eq!(InterfaceType::Datastream.to_string(), "datastream");
997        assert_eq!(InterfaceType::Properties.to_string(), "properties");
998    }
999
1000    #[test]
1001    fn ownership_functions() {
1002        assert_eq!(Ownership::Device.to_string(), "device");
1003        assert_eq!(Ownership::Server.to_string(), "server");
1004        assert!(Ownership::Server.is_server());
1005        assert!(!Ownership::Device.is_server());
1006        assert!(Ownership::Device.is_device());
1007        assert!(!Ownership::Server.is_device());
1008    }
1009
1010    #[test]
1011    fn aggregation_functions() {
1012        assert_eq!(Aggregation::Individual.to_string(), "individual");
1013        assert_eq!(Aggregation::Object.to_string(), "object");
1014    }
1015
1016    #[test]
1017    fn mapping_type_functions() {
1018        assert_eq!(MappingType::Double.to_string(), "double");
1019        assert_eq!(MappingType::Integer.to_string(), "integer");
1020        assert_eq!(MappingType::Boolean.to_string(), "boolean");
1021        assert_eq!(MappingType::LongInteger.to_string(), "longinteger");
1022        assert_eq!(MappingType::String.to_string(), "string");
1023        assert_eq!(MappingType::BinaryBlob.to_string(), "binaryblob");
1024        assert_eq!(MappingType::DateTime.to_string(), "datetime");
1025        assert_eq!(MappingType::DoubleArray.to_string(), "doublearray");
1026        assert_eq!(MappingType::IntegerArray.to_string(), "integerarray");
1027        assert_eq!(MappingType::BooleanArray.to_string(), "booleanarray");
1028        assert_eq!(
1029            MappingType::LongIntegerArray.to_string(),
1030            "longintegerarray"
1031        );
1032        assert_eq!(MappingType::StringArray.to_string(), "stringarray");
1033        assert_eq!(MappingType::BinaryBlobArray.to_string(), "binaryblobarray");
1034        assert_eq!(MappingType::DateTimeArray.to_string(), "datetimearray");
1035    }
1036
1037    #[test]
1038    fn reliability_functions() {
1039        assert_eq!(Reliability::Unreliable.to_string(), "unreliable");
1040        assert_eq!(Reliability::Guaranteed.to_string(), "guaranteed");
1041        assert_eq!(Reliability::Unique.to_string(), "unique");
1042        assert!(Reliability::Unreliable.is_unreliable());
1043        assert!(Reliability::Guaranteed.is_guaranteed());
1044        assert!(Reliability::Unique.is_unique());
1045    }
1046
1047    #[test]
1048    fn retention_functions() {
1049        assert_eq!(Retention::Discard.to_string(), "discard");
1050        assert_eq!(Retention::Volatile.to_string(), "volatile");
1051        assert_eq!(Retention::Stored.to_string(), "stored");
1052    }
1053
1054    #[test]
1055    fn database_retention_policy_functions() {
1056        assert_eq!(DatabaseRetentionPolicy::NoTtl.to_string(), "no_ttl");
1057        assert_eq!(DatabaseRetentionPolicy::UseTtl.to_string(), "use_ttl");
1058    }
1059}