Skip to main content

astarte_interfaces/interface/
properties.rs

1// This file is part of Astarte.
2//
3// Copyright 2025 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//! Interface for persistent, stateful, synchronized state with no concept of history or timestamp.
20//!
21//! Properties are useful, for example, when dealing with settings, states or policies/rules.
22
23use std::{borrow::Cow, fmt::Display, str::FromStr};
24
25use serde::Serialize;
26
27use crate::{
28    error::Error,
29    mapping::{path::MappingPath, properties::PropertiesMapping, MappingError},
30    schema::{Aggregation, InterfaceJson, InterfaceType, Mapping, Ownership},
31};
32
33use super::{
34    name::InterfaceName, version::InterfaceVersion, AggregationIndividual, MappingVec, Schema,
35};
36
37/// Interface of type individual property.
38///
39/// For this interface all the mappings have their own configuration.
40#[derive(Debug, PartialEq, Eq, Clone)]
41pub struct Properties {
42    name: InterfaceName,
43    version: InterfaceVersion,
44    ownership: Ownership,
45    mappings: MappingVec<PropertiesMapping>,
46    #[cfg(feature = "doc-fields")]
47    #[cfg_attr(docsrs, doc(cfg(feature = "doc-fields")))]
48    description: Option<String>,
49    #[cfg(feature = "doc-fields")]
50    #[cfg_attr(docsrs, doc(cfg(feature = "doc-fields")))]
51    doc: Option<String>,
52}
53
54impl Schema for Properties {
55    type Mapping = PropertiesMapping;
56
57    fn name(&self) -> &str {
58        self.name.as_ref()
59    }
60
61    fn interface_name(&self) -> &InterfaceName {
62        &self.name
63    }
64
65    fn version_major(&self) -> i32 {
66        self.version.version_major()
67    }
68
69    fn version_minor(&self) -> i32 {
70        self.version.version_minor()
71    }
72
73    fn version(&self) -> InterfaceVersion {
74        self.version
75    }
76
77    fn interface_type(&self) -> InterfaceType {
78        InterfaceType::Properties
79    }
80
81    fn ownership(&self) -> Ownership {
82        self.ownership
83    }
84
85    fn aggregation(&self) -> Aggregation {
86        Aggregation::Individual
87    }
88
89    #[cfg(feature = "doc-fields")]
90    #[cfg_attr(docsrs, doc(cfg(feature = "doc-fields")))]
91    fn description(&self) -> Option<&str> {
92        self.description.as_deref()
93    }
94
95    #[cfg(feature = "doc-fields")]
96    #[cfg_attr(docsrs, doc(cfg(feature = "doc-fields")))]
97    fn doc(&self) -> Option<&str> {
98        self.doc.as_deref()
99    }
100
101    fn iter_mappings(&self) -> impl Iterator<Item = &Self::Mapping> {
102        self.mappings.iter()
103    }
104
105    fn mappings_len(&self) -> usize {
106        self.mappings.len()
107    }
108
109    fn iter_interface_mappings(&self) -> impl Iterator<Item = Mapping<Cow<'_, str>>> {
110        self.mappings.iter().map(Mapping::from)
111    }
112}
113
114impl AggregationIndividual for Properties {
115    fn mapping(&self, path: &MappingPath) -> Option<&Self::Mapping> {
116        self.mappings.get(path)
117    }
118}
119
120impl Display for Properties {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        write!(f, "{}:{}", self.name, self.version)
123    }
124}
125
126impl<T> TryFrom<InterfaceJson<T>> for Properties
127where
128    T: AsRef<str> + Into<String>,
129{
130    type Error = Error;
131
132    fn try_from(value: InterfaceJson<T>) -> Result<Self, Self::Error> {
133        if value.interface_type != InterfaceType::Properties
134            || value.aggregation.unwrap_or_default() != Aggregation::Individual
135        {
136            return Err(Error::InterfaceConversion {
137                exp_type: InterfaceType::Properties,
138                exp_aggregation: Aggregation::Individual,
139                got_type: value.interface_type,
140                got_aggregation: value.aggregation.unwrap_or_default(),
141            });
142        }
143
144        let name = InterfaceName::from_str_ref(value.interface_name)?;
145        let version = InterfaceVersion::try_new(value.version_major, value.version_minor)?;
146
147        let mappings = value
148            .mappings
149            .into_iter()
150            .map(PropertiesMapping::try_from)
151            .collect::<Result<Vec<_>, MappingError>>()
152            .and_then(MappingVec::try_from)?;
153
154        Ok(Self {
155            name: name.into_string(),
156            version,
157            ownership: value.ownership,
158            mappings,
159            #[cfg(feature = "doc-fields")]
160            description: value.description.as_ref().map(|v| v.as_ref().to_string()),
161            #[cfg(feature = "doc-fields")]
162            doc: value.doc.as_ref().map(|v| v.as_ref().to_string()),
163        })
164    }
165}
166
167impl Serialize for Properties {
168    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
169    where
170        S: serde::Serializer,
171    {
172        InterfaceJson::from(self).serialize(serializer)
173    }
174}
175
176impl FromStr for Properties {
177    type Err = Error;
178
179    fn from_str(s: &str) -> Result<Self, Self::Err> {
180        let interface: InterfaceJson<Cow<str>> = serde_json::from_str(s)?;
181
182        Self::try_from(interface)
183    }
184}