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}