Skip to main content

astarte_interfaces/mapping/
properties.rs

1// This file is part of Astarte.
2//
3// Copyright 2025, 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//! Mapping for interfaces of type Property.
20
21use std::borrow::Cow;
22
23use crate::mapping::invalid_filed;
24use crate::schema::{Mapping, MappingType};
25
26use super::{endpoint::Endpoint, InterfaceMapping, MappingError};
27
28/// Mapping of a [`Properties`](crate::Properties) interface.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct PropertiesMapping {
31    pub(crate) endpoint: Endpoint<String>,
32    pub(crate) mapping_type: MappingType,
33    pub(crate) allow_unset: bool,
34    #[cfg(feature = "doc-fields")]
35    pub(crate) description: Option<String>,
36    #[cfg(feature = "doc-fields")]
37    pub(crate) doc: Option<String>,
38}
39
40impl PropertiesMapping {
41    /// Returns true if the property can be unset.
42    #[must_use]
43    pub fn allow_unset(&self) -> bool {
44        self.allow_unset
45    }
46}
47
48impl InterfaceMapping for PropertiesMapping {
49    fn endpoint(&self) -> &Endpoint<String> {
50        &self.endpoint
51    }
52
53    fn mapping_type(&self) -> MappingType {
54        self.mapping_type
55    }
56
57    #[cfg(feature = "doc-fields")]
58    #[cfg_attr(docsrs, doc(cfg(feature = "doc-fields")))]
59    fn description(&self) -> Option<&str> {
60        self.description.as_deref()
61    }
62
63    #[cfg(feature = "doc-fields")]
64    #[cfg_attr(docsrs, doc(cfg(feature = "doc-fields")))]
65    fn doc(&self) -> Option<&str> {
66        self.doc.as_deref()
67    }
68}
69
70impl<T> TryFrom<Mapping<T>> for PropertiesMapping
71where
72    T: AsRef<str> + Into<String>,
73{
74    type Error = MappingError;
75
76    fn try_from(value: Mapping<T>) -> Result<Self, Self::Error> {
77        let endpoint = Endpoint::try_from(value.endpoint.as_ref())?;
78
79        if value.reliability.is_some() {
80            invalid_filed!(properties, "reliability");
81        }
82
83        if value.explicit_timestamp.is_some() {
84            invalid_filed!(properties, "explicit_timestamp");
85        }
86
87        if value.retention.is_some() {
88            invalid_filed!(properties, "retention");
89        }
90
91        if value.expiry.is_some() {
92            invalid_filed!(properties, "expiry");
93        }
94
95        if value.database_retention_policy.is_some() {
96            invalid_filed!(properties, "database_retention_policy");
97        }
98
99        if value.database_retention_ttl.is_some() {
100            invalid_filed!(properties, "database_retention_ttl");
101        }
102
103        if value.required.is_some() {
104            invalid_filed!(properties, "required");
105        }
106
107        Ok(Self {
108            endpoint,
109            mapping_type: value.mapping_type,
110            allow_unset: value.allow_unset.unwrap_or_default(),
111            #[cfg(feature = "doc-fields")]
112            description: value.description.map(T::into),
113            #[cfg(feature = "doc-fields")]
114            doc: value.doc.map(T::into),
115        })
116    }
117}
118
119impl<'a> From<&'a PropertiesMapping> for Mapping<Cow<'a, str>> {
120    fn from(value: &'a PropertiesMapping) -> Self {
121        Mapping {
122            endpoint: value.endpoint().to_string().into(),
123            mapping_type: value.mapping_type,
124            reliability: None,
125            explicit_timestamp: None,
126            retention: None,
127            expiry: None,
128            database_retention_policy: None,
129            database_retention_ttl: None,
130            allow_unset: Some(value.allow_unset),
131            required: None,
132            #[cfg(feature = "doc-fields")]
133            description: value.description().map(Cow::Borrowed),
134            #[cfg(feature = "doc-fields")]
135            doc: value.doc().map(Cow::Borrowed),
136            #[cfg(not(feature = "doc-fields"))]
137            description: None,
138            #[cfg(not(feature = "doc-fields"))]
139            doc: None,
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146
147    use super::*;
148
149    #[test]
150    fn getters_success() {
151        let mapping_type = MappingType::Boolean;
152        let description = Some("Property mapping description");
153        let doc = Some("Property mapping doc");
154        let mapping = Mapping {
155            endpoint: "/property/path",
156            mapping_type,
157            reliability: None,
158            explicit_timestamp: None,
159            retention: None,
160            expiry: None,
161            database_retention_policy: None,
162            database_retention_ttl: None,
163            allow_unset: Some(true),
164            required: None,
165            description,
166            doc,
167        };
168
169        let prop_mapping = PropertiesMapping::try_from(mapping).unwrap();
170        let exp_endpoint = Endpoint::try_from("/property/path").unwrap();
171        assert_eq!(*prop_mapping.endpoint(), exp_endpoint);
172        assert_eq!(prop_mapping.mapping_type(), mapping_type);
173        assert!(prop_mapping.allow_unset());
174        #[cfg(feature = "doc-fields")]
175        {
176            assert_eq!(prop_mapping.description(), description);
177            assert_eq!(prop_mapping.doc(), doc);
178        }
179    }
180
181    #[test]
182    fn from_and_into() {
183        let description = Some(Cow::Borrowed("Property mapping description"));
184        let doc = Some(Cow::Borrowed("Property mapping doc"));
185        let mapping = Mapping {
186            endpoint: Cow::Borrowed("/property/path"),
187            mapping_type: MappingType::Boolean,
188            reliability: None,
189            explicit_timestamp: None,
190            retention: None,
191            expiry: None,
192            database_retention_policy: None,
193            database_retention_ttl: None,
194            allow_unset: Some(true),
195            required: None,
196            description,
197            doc,
198        };
199
200        let prop_mapping = PropertiesMapping::try_from(mapping.clone()).unwrap();
201
202        let exp = PropertiesMapping {
203            endpoint: Endpoint::try_from("/property/path").unwrap(),
204            mapping_type: MappingType::Boolean,
205            allow_unset: true,
206            #[cfg(feature = "doc-fields")]
207            description: mapping.description.as_ref().map(|v| v.to_string()),
208            #[cfg(feature = "doc-fields")]
209            doc: mapping.doc.as_ref().map(|v| v.to_string()),
210        };
211        assert_eq!(prop_mapping, exp);
212
213        let cov_mapping: Mapping<Cow<str>> = (&prop_mapping).into();
214
215        #[cfg(not(feature = "doc-fields"))]
216        let mut mapping = mapping;
217        #[cfg(not(feature = "doc-fields"))]
218        {
219            mapping.description.take();
220            mapping.doc.take();
221        }
222
223        assert_eq!(cov_mapping, mapping);
224    }
225
226    #[cfg(feature = "strict")]
227    #[test]
228    fn mapping_error_invalid_fields() {
229        use crate::schema::{DatabaseRetentionPolicy, InterfaceType, Reliability, Retention};
230
231        let mapping = Mapping {
232            endpoint: "/property/path",
233            mapping_type: MappingType::Boolean,
234            reliability: Some(Reliability::Guaranteed),
235            explicit_timestamp: None,
236            retention: None,
237            expiry: None,
238            database_retention_policy: None,
239            database_retention_ttl: None,
240            allow_unset: None,
241            required: None,
242            description: None,
243            doc: None,
244        };
245
246        let err = PropertiesMapping::try_from(mapping).unwrap_err();
247        assert!(matches!(
248            err,
249            MappingError::InvalidField {
250                field: "reliability",
251                interface_type: InterfaceType::Properties
252            }
253        ));
254
255        let mapping = Mapping {
256            endpoint: "/property/path",
257            mapping_type: MappingType::Boolean,
258            reliability: None,
259            explicit_timestamp: Some(true),
260            retention: None,
261            expiry: None,
262            database_retention_policy: None,
263            database_retention_ttl: None,
264            allow_unset: None,
265            required: None,
266            description: None,
267            doc: None,
268        };
269
270        let err = PropertiesMapping::try_from(mapping).unwrap_err();
271        assert!(matches!(
272            err,
273            MappingError::InvalidField {
274                field: "explicit_timestamp",
275                interface_type: InterfaceType::Properties
276            }
277        ));
278
279        let mapping = Mapping {
280            endpoint: "/property/path",
281            mapping_type: MappingType::Boolean,
282            reliability: None,
283            explicit_timestamp: None,
284            retention: Some(Retention::Stored),
285            expiry: None,
286            database_retention_policy: None,
287            database_retention_ttl: None,
288            allow_unset: None,
289            required: None,
290            description: None,
291            doc: None,
292        };
293
294        let err = PropertiesMapping::try_from(mapping).unwrap_err();
295        assert!(matches!(
296            err,
297            MappingError::InvalidField {
298                field: "retention",
299                interface_type: InterfaceType::Properties
300            }
301        ));
302
303        let mapping = Mapping {
304            endpoint: "/property/path",
305            mapping_type: MappingType::Boolean,
306            reliability: None,
307            explicit_timestamp: None,
308            retention: None,
309            expiry: Some(420),
310            database_retention_policy: None,
311            database_retention_ttl: None,
312            allow_unset: None,
313            required: None,
314            description: None,
315            doc: None,
316        };
317
318        let err = PropertiesMapping::try_from(mapping).unwrap_err();
319        assert!(matches!(
320            err,
321            MappingError::InvalidField {
322                field: "expiry",
323                interface_type: InterfaceType::Properties
324            }
325        ));
326
327        let mapping = Mapping {
328            endpoint: "/property/path",
329            mapping_type: MappingType::Boolean,
330            reliability: None,
331            explicit_timestamp: None,
332            retention: None,
333            expiry: None,
334            database_retention_policy: Some(DatabaseRetentionPolicy::NoTtl),
335            database_retention_ttl: None,
336            allow_unset: None,
337            required: None,
338            description: None,
339            doc: None,
340        };
341
342        let err = PropertiesMapping::try_from(mapping).unwrap_err();
343        assert!(matches!(
344            err,
345            MappingError::InvalidField {
346                field: "database_retention_policy",
347                interface_type: InterfaceType::Properties
348            }
349        ));
350
351        let mapping = Mapping {
352            endpoint: "/property/path",
353            mapping_type: MappingType::Boolean,
354            reliability: None,
355            explicit_timestamp: None,
356            retention: None,
357            expiry: None,
358            database_retention_policy: None,
359            database_retention_ttl: Some(420),
360            allow_unset: None,
361            required: None,
362            description: None,
363            doc: None,
364        };
365
366        let err = PropertiesMapping::try_from(mapping).unwrap_err();
367        assert!(matches!(
368            err,
369            MappingError::InvalidField {
370                field: "database_retention_ttl",
371                interface_type: InterfaceType::Properties
372            }
373        ));
374
375        let mapping = Mapping {
376            endpoint: "/property/path",
377            mapping_type: MappingType::Boolean,
378            reliability: None,
379            explicit_timestamp: None,
380            retention: None,
381            expiry: None,
382            database_retention_policy: None,
383            database_retention_ttl: None,
384            allow_unset: None,
385            required: Some(true),
386            description: None,
387            doc: None,
388        };
389
390        let err = PropertiesMapping::try_from(mapping).unwrap_err();
391        assert!(matches!(
392            err,
393            MappingError::InvalidField {
394                field: "required",
395                interface_type: InterfaceType::Properties
396            }
397        ));
398    }
399}