astarte_interfaces/mapping/datastream/
object.rs1use std::cmp::Ordering;
24
25use crate::{
26 mapping::{
27 endpoint::{Endpoint, Level},
28 InterfaceMapping, MappingError,
29 },
30 schema::{Mapping, MappingType},
31 MappingPath,
32};
33
34pub const MIN_OBJECT_ENDPOINT_LEN: usize = 2;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct DatastreamObjectMapping {
42 pub(crate) endpoint: Endpoint<String>,
43 pub(crate) mapping_type: MappingType,
44 pub(crate) required: bool,
45 #[cfg(feature = "doc-fields")]
46 pub(crate) description: Option<String>,
47 #[cfg(feature = "doc-fields")]
48 pub(crate) doc: Option<String>,
49}
50
51impl DatastreamObjectMapping {
52 pub fn required(&self) -> bool {
57 self.required
58 }
59
60 pub fn cmp_object_field(&self, path: &str) -> Ordering {
64 let last = self.endpoint.last();
65
66 debug_assert!(
67 last.is_some(),
68 "an endpoint should always have at least an endpoint"
69 );
70
71 match last {
72 Some(Level::Simple(simple)) => simple.as_str().cmp(path),
73 Some(Level::Parameter(_)) => Ordering::Equal,
74 None => Ordering::Less,
75 }
76 }
77
78 pub fn eq_object_field(&self, path: &str) -> bool {
82 let last = self.endpoint.last();
83
84 debug_assert!(
85 last.is_some(),
86 "an endpoint should always have at least an endpoint"
87 );
88
89 last.is_some_and(|endpoint_level| *endpoint_level == path)
90 }
91
92 pub(crate) fn is_object_path<'a>(&self, path: &MappingPath<'a>) -> bool {
93 if self.endpoint.len().saturating_sub(1) != path.len() {
95 return false;
96 }
97
98 self.endpoint
100 .iter()
101 .zip(path.levels.iter())
102 .all(|(endpoint_level, path_level)| endpoint_level == path_level)
103 }
104
105 pub(crate) fn is_same_object(&self, other: &DatastreamObjectMapping) -> bool {
109 if self.endpoint.len() != other.endpoint.len() {
110 return false;
111 }
112
113 self.endpoint
115 .iter()
116 .zip(other.endpoint.iter())
117 .rev()
118 .skip(1)
119 .all(|(level, other_level)| level == other_level)
120 }
121}
122
123impl InterfaceMapping for DatastreamObjectMapping {
124 fn endpoint(&self) -> &Endpoint<String> {
125 &self.endpoint
126 }
127
128 fn mapping_type(&self) -> MappingType {
129 self.mapping_type
130 }
131
132 #[cfg(feature = "doc-fields")]
133 #[cfg_attr(docsrs, doc(cfg(feature = "doc-fields")))]
134 fn description(&self) -> Option<&str> {
135 self.description.as_deref()
136 }
137
138 #[cfg(feature = "doc-fields")]
139 #[cfg_attr(docsrs, doc(cfg(feature = "doc-fields")))]
140 fn doc(&self) -> Option<&str> {
141 self.doc.as_deref()
142 }
143}
144
145impl<T> TryFrom<Mapping<T>> for DatastreamObjectMapping
146where
147 T: AsRef<str> + Into<String>,
148{
149 type Error = MappingError;
150
151 fn try_from(value: Mapping<T>) -> Result<Self, Self::Error> {
152 let endpoint = Endpoint::try_from(value.endpoint.as_ref())?;
153
154 if endpoint.len() < MIN_OBJECT_ENDPOINT_LEN {
155 return Err(MappingError::TooShortForObject(endpoint.to_string()));
156 }
157
158 Ok(Self {
159 endpoint,
160 mapping_type: value.mapping_type,
161 required: value.required.unwrap_or(false),
162 #[cfg(feature = "doc-fields")]
163 description: value.description.map(T::into),
164 #[cfg(feature = "doc-fields")]
165 doc: value.doc.map(T::into),
166 })
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use pretty_assertions::assert_eq;
173
174 use super::*;
175
176 fn mock_object_mapping(endpoint: &str) -> DatastreamObjectMapping {
177 DatastreamObjectMapping {
178 endpoint: endpoint.parse().unwrap(),
179 mapping_type: MappingType::Boolean,
180 required: false,
181 #[cfg(feature = "doc-fields")]
182 description: None,
183 #[cfg(feature = "doc-fields")]
184 doc: None,
185 }
186 }
187
188 #[test]
189 fn getters_success() {
190 let description = Some("Object mapping description");
191 let doc = Some("Object mapping doc");
192 let mapping_type = MappingType::Boolean;
193 let mapping = Mapping {
194 endpoint: "/object/path",
195 mapping_type,
196 reliability: None,
197 explicit_timestamp: None,
198 retention: None,
199 expiry: None,
200 database_retention_policy: None,
201 database_retention_ttl: None,
202 allow_unset: None,
203 required: None,
204 description,
205 doc,
206 };
207
208 let obj_mapping = DatastreamObjectMapping::try_from(mapping).unwrap();
209
210 let exp = Endpoint::try_from("/object/path").unwrap();
211 assert_eq!(*obj_mapping.endpoint(), exp);
212 assert_eq!(obj_mapping.mapping_type(), mapping_type);
213 #[cfg(feature = "doc-fields")]
214 {
215 assert_eq!(description, obj_mapping.description());
216 assert_eq!(doc, obj_mapping.doc());
217 }
218 }
219
220 #[test]
221 fn mapping_error_to_short() {
222 let mapping = Mapping {
223 endpoint: "/tooShort",
224 mapping_type: MappingType::Boolean,
225 reliability: None,
226 explicit_timestamp: None,
227 retention: None,
228 expiry: None,
229 database_retention_policy: None,
230 database_retention_ttl: None,
231 allow_unset: None,
232 required: None,
233 description: None,
234 doc: None,
235 };
236
237 let err = DatastreamObjectMapping::try_from(mapping).unwrap_err();
238 assert!(matches!(err, MappingError::TooShortForObject(_)));
239 }
240
241 #[test]
242 fn check_object_path() {
243 let endpoint = mock_object_mapping("/%{sensor_id}/boolean_endpoint");
244
245 let path = MappingPath::try_from("/1/boolean_endpoint").unwrap();
246
247 assert!(!endpoint.is_object_path(&path));
248
249 let path = MappingPath::try_from("/1").unwrap();
250 assert!(endpoint.is_object_path(&path));
251 }
252
253 #[test]
254 fn object_field() {
255 let endpoint = mock_object_mapping("/%{sensor_id}/boolean_endpoint");
256
257 assert!(endpoint.eq_object_field("boolean_endpoint"));
258 assert!(!endpoint.eq_object_field("foo"));
259 }
260
261 #[test]
262 fn same_object() {
263 let endpoint = mock_object_mapping("/base/foo");
264 let same = mock_object_mapping("/base/bar");
265 let different = mock_object_mapping("/other/bar");
266
267 assert!(endpoint.is_same_object(&same));
268 assert!(!endpoint.is_same_object(&different));
269 }
270}