Skip to main content

astarte_interfaces/interface/
validation.rs

1// This file is part of Astarte.
2//
3// Copyright 2023 - 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//! Validate an interface and the version change between two interfaces.
20
21use std::{cmp::Ordering, fmt::Display};
22
23use super::Interface;
24
25/// Error for changing the version of an interface.
26#[non_exhaustive]
27#[derive(thiserror::Error, Debug, Clone, Copy)]
28pub enum VersionChangeError {
29    /// The major version cannot be decreased.
30    #[error("the major version decreased: {0}")]
31    MajorDecreased(VersionChange),
32    /// The minor version cannot be decreased.
33    #[error("the minor version decreased: {0}")]
34    MinorDecreased(VersionChange),
35    /// The interface is different but the version did not change.
36    #[error("the version did not change: {0}")]
37    SameVersion(VersionChange),
38}
39
40/// A change in version of an interface.
41///
42/// This structure is used to validate that the new version of an interface is a valid successor of
43/// the previous version. The version cannot decrease, and they cannot be the same (not really a
44/// version change).
45///
46/// This validates only the version change, not the interface itself. For that, see [`Interface::validate_with`].
47#[derive(Debug, Clone, Copy)]
48pub struct VersionChange {
49    next_major: i32,
50    next_minor: i32,
51    prev_major: i32,
52    prev_minor: i32,
53}
54
55impl VersionChange {
56    /// Create a new version change from a new and previous interfaces.
57    pub fn try_new(next: &Interface, prev: &Interface) -> Result<Self, VersionChangeError> {
58        let change = Self {
59            next_major: next.version_major(),
60            next_minor: next.version_minor(),
61            prev_major: prev.version_major(),
62            prev_minor: prev.version_minor(),
63        };
64
65        change.validate()
66    }
67
68    /// Returns the previous version
69    #[must_use]
70    pub fn previous(&self) -> (i32, i32) {
71        (self.prev_major, self.prev_minor)
72    }
73
74    /// Returns the next version
75    #[must_use]
76    pub fn next(&self) -> (i32, i32) {
77        (self.next_major, self.next_minor)
78    }
79
80    /// Private method for a version change validation.
81    ///
82    /// Validate if the version change is valid.
83    pub fn validate(self) -> Result<Self, VersionChangeError> {
84        let major = self.next_major.cmp(&self.prev_major);
85        let minor = self.next_minor.cmp(&self.prev_minor);
86
87        match (major, minor) {
88            (Ordering::Less, _) => Err(VersionChangeError::MajorDecreased(self)),
89            (Ordering::Equal, Ordering::Less) => Err(VersionChangeError::MinorDecreased(self)),
90            (Ordering::Equal, Ordering::Equal) => Err(VersionChangeError::SameVersion(self)),
91            _ => Ok(self),
92        }
93    }
94}
95
96impl Display for VersionChange {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        write!(
99            f,
100            "{}.{} -> {}.{}",
101            self.prev_major, self.prev_minor, self.next_major, self.next_minor
102        )
103    }
104}
105
106#[cfg(test)]
107mod test {
108    use std::str::FromStr;
109
110    use crate::Interface;
111
112    #[test]
113    fn version_change() {
114        let prev = make_interface(1, 2);
115        let next = make_interface(2, 2);
116
117        let change = super::VersionChange::try_new(&next, &prev);
118
119        assert!(change.is_ok());
120
121        let change = change.unwrap();
122
123        assert_eq!(change.previous(), (1, 2));
124        assert_eq!(change.next(), (2, 2));
125
126        assert_eq!(change.to_string(), "1.2 -> 2.2");
127    }
128
129    #[test]
130    fn validation_test() {
131        // Both major and minor are 0
132        let interface_json = r#"
133        {
134            "interface_name": "org.astarte-platform.genericsensors.Values",
135            "version_major": 0,
136            "version_minor": 0,
137            "type": "datastream",
138            "ownership": "device",
139            "description": "Interface description",
140            "doc": "Interface doc",
141            "mappings": [
142                {
143                    "endpoint": "/%{sensor_id}/value",
144                    "type": "double",
145                    "explicit_timestamp": true,
146                    "description": "Mapping description",
147                    "doc": "Mapping doc"
148                },
149                {
150                    "endpoint": "/%{sensor_id}/otherValue",
151                    "type": "longinteger",
152                    "explicit_timestamp": true,
153                    "description": "Mapping description",
154                    "doc": "Mapping doc"
155                }
156            ]
157        }"#;
158
159        let deser_interface = Interface::from_str(interface_json);
160
161        assert!(deser_interface.is_err());
162    }
163
164    #[test]
165    fn validate_same_interface() {
166        let prev_interface = Interface::from_str(
167            r#"
168        {
169            "interface_name": "org.astarte-platform.genericsensors.Values",
170            "version_major": 1,
171            "version_minor": 0,
172            "type": "datastream",
173            "ownership": "device",
174            "description": "Interface description",
175            "doc": "Interface doc",
176            "mappings": [
177                {
178                    "endpoint": "/%{sensor_id}/value",
179                    "type": "double",
180                    "explicit_timestamp": true,
181                    "description": "Mapping description",
182                    "doc": "Mapping doc"
183                },
184                {
185                    "endpoint": "/%{sensor_id}/otherValue",
186                    "type": "longinteger",
187                    "explicit_timestamp": true,
188                    "description": "Mapping description",
189                    "doc": "Mapping doc"
190                }
191            ]
192        }"#,
193        )
194        .unwrap();
195
196        let new_interface = Interface::from_str(
197            r#"
198        {
199            "interface_name": "org.astarte-platform.genericsensors.Values",
200            "version_major": 1,
201            "version_minor": 0,
202            "type": "datastream",
203            "ownership": "device",
204            "description": "Interface description",
205            "doc": "Interface doc",
206            "mappings": [
207                {
208                    "endpoint": "/%{sensor_id}/value",
209                    "type": "double",
210                    "explicit_timestamp": true,
211                    "description": "Mapping description",
212                    "doc": "Mapping doc"
213                },
214                {
215                    "endpoint": "/%{sensor_id}/otherValue",
216                    "type": "longinteger",
217                    "explicit_timestamp": true,
218                    "description": "Mapping description",
219                    "doc": "Mapping doc"
220                }
221            ]
222        }"#,
223        )
224        .unwrap();
225
226        assert!(new_interface.validate_with(&prev_interface).is_ok());
227    }
228
229    #[test]
230    fn validate_version() {
231        let interfaces = [
232            (make_interface(1, 0), make_interface(1, 1), true),
233            (make_interface(2, 1), make_interface(1, 1), false),
234            (make_interface(1, 2), make_interface(1, 1), false),
235            // Same interface
236            (make_interface(1, 1), make_interface(1, 1), true),
237        ];
238
239        for (prev, new, expected) in interfaces {
240            let res = new.validate_with(&prev);
241
242            assert_eq!(
243                res.is_ok(),
244                expected,
245                "expected to {}: {:?}",
246                if expected { "pass" } else { "fail" },
247                res
248            );
249        }
250    }
251
252    fn make_interface(major: i32, minor: i32) -> Interface {
253        Interface::from_str(&format!(
254            r#"{{
255            "interface_name": "org.astarte-platform.genericsensors.Values",
256            "version_major": {major},
257            "version_minor": {minor},
258            "type": "datastream",
259            "ownership": "device",
260            "description": "Interface description",
261            "doc": "Interface doc",
262            "mappings": [{{
263                "endpoint": "/value",
264                "type": "double",
265                "description": "Mapping description",
266                "doc": "Mapping doc"
267            }}]
268        }}"#
269        ))
270        .unwrap()
271    }
272}