Skip to main content

astarte_interfaces/mapping/
endpoint.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//! Endpoint of an interface mapping.
20
21use std::hash::Hash;
22use std::{fmt::Display, slice::Iter as SliceIter, str::FromStr};
23
24use tracing::trace;
25
26use super::path::MappingPath;
27
28/// The maximum length of an endpoint must be 64 levels
29pub const ENDPOINT_MAX_LEN: usize = 64;
30
31/// A mapping endpoint.
32///
33/// - It must be unique within the interface
34/// - Parameters should be separated by a slash (`/`)
35/// - Parameters are equal to any level and each combination of levels should be unique
36/// - Two endpoints are equal if they have the same path
37/// - The path must start with a slash (`/`)
38/// - The minimum length is 2 character
39/// - Each level should start with an ascii letter `[a-zA-Z]`
40/// - A level or parameter can only container ascii alpha-numeric character or an underscore
41///   `[a-zA-Z0-9_]`
42/// - A parameter cannot contain the `/` character
43///
44/// For more information see [Astarte - Docs](https://docs.astarte-platform.org/astarte/latest/030-interface.html#limitations)
45///
46/// The endpoints uses Cow to not allocate the string if an error occurs.
47#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
48pub struct Endpoint<T = String> {
49    levels: Vec<Level<T>>,
50}
51
52impl<T> Endpoint<T> {
53    /// Iter the levels of the endpoint.
54    pub fn iter(&self) -> SliceIter<'_, Level<T>> {
55        self.levels.iter()
56    }
57
58    /// Compare the levels with the one of the endpoint.
59    #[must_use]
60    pub fn eq_mapping<'a>(&self, mapping: &MappingPath<'a>) -> bool
61    where
62        T: PartialEq<&'a str> + Eq,
63    {
64        if self.len() != mapping.len() {
65            return false;
66        }
67
68        self.iter()
69            .zip(mapping.levels.iter())
70            .all(|(endpoint_level, path_level)| endpoint_level == path_level)
71    }
72
73    /// Returns the last level of the endpoint
74    ///
75    /// This API shouldn't be as is, since we should guarantee that the endpoint has at least one
76    /// level.
77    pub(crate) fn last(&self) -> Option<&Level<T>> {
78        self.levels.last()
79    }
80
81    // Returns the number of levels in an endpoint
82    pub(crate) fn len(&self) -> usize {
83        self.levels.len()
84    }
85}
86
87impl<'a> TryFrom<&'a str> for Endpoint<&'a str> {
88    type Error = EndpointError;
89
90    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
91        parse_endpoint(value)
92    }
93}
94
95impl TryFrom<&str> for Endpoint<String> {
96    type Error = EndpointError;
97
98    fn try_from(value: &str) -> Result<Self, Self::Error> {
99        Endpoint::<&str>::try_from(value).map(Endpoint::into)
100    }
101}
102
103impl FromStr for Endpoint<String> {
104    type Err = EndpointError;
105
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        Self::try_from(s)
108    }
109}
110
111impl<T> Display for Endpoint<T>
112where
113    T: Display,
114{
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        for level in &self.levels {
117            write!(f, "/{level}")?;
118        }
119
120        Ok(())
121    }
122}
123
124impl From<Endpoint<&str>> for Endpoint<String> {
125    fn from(value: Endpoint<&str>) -> Self {
126        Self {
127            levels: value.levels.into_iter().map(Level::into).collect(),
128        }
129    }
130}
131
132impl<'a, T> PartialEq<MappingPath<'a>> for Endpoint<T>
133where
134    T: for<'b> PartialEq<&'b str> + Eq,
135{
136    fn eq(&self, other: &MappingPath<'a>) -> bool {
137        self.eq_mapping(other)
138    }
139}
140
141impl<'a, T> IntoIterator for &'a Endpoint<T> {
142    type Item = &'a Level<T>;
143    type IntoIter = std::slice::Iter<'a, Level<T>>;
144    fn into_iter(self) -> Self::IntoIter {
145        self.iter()
146    }
147}
148
149/// Level of an [`Endpoint`].
150///
151/// # Example
152///
153/// ```rust
154/// # use astarte_interfaces::{Endpoint, mapping::endpoint::Level};
155/// let endpoint = Endpoint::try_from("/sensor/%{name}/id").unwrap();
156/// let mut iter = endpoint.iter();
157///
158/// assert_eq!(iter.next(), Some(&Level::Simple("sensor")));
159/// assert_eq!(iter.next(), Some(&Level::Parameter("name")));
160/// assert_eq!(iter.next(), Some(&Level::Simple("id")));
161/// assert_eq!(iter.next(), None);
162/// ```
163#[derive(Debug, Eq, PartialOrd, Ord, Clone, Copy)]
164pub enum Level<T> {
165    /// Simple level without parameters
166    Simple(T),
167    /// Parameter level enclosed in `%{bar}`.
168    Parameter(T),
169}
170
171impl<T> Level<T> {
172    /// This prevents mistakes for recursion in the PartialEq traits.
173    fn eq_str<'a>(&self, other: &'a str) -> bool
174    where
175        T: PartialEq<&'a str> + Eq,
176    {
177        match self {
178            Level::Simple(level) => *level == other,
179            Level::Parameter(_) => true,
180        }
181    }
182}
183
184impl<T> PartialEq for Level<T>
185where
186    T: PartialEq,
187{
188    fn eq(&self, other: &Self) -> bool {
189        match (self, other) {
190            (Self::Simple(l0), Self::Simple(r0)) => l0 == r0,
191            (Self::Parameter(_), Self::Parameter(_)) => true,
192            _ => false,
193        }
194    }
195}
196
197impl<T: Hash> Hash for Level<T> {
198    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
199        core::mem::discriminant(self).hash(state);
200
201        match self {
202            Level::Simple(level) => level.hash(state),
203            Level::Parameter(_) => {}
204        }
205    }
206}
207
208impl<T> Display for Level<T>
209where
210    T: Display,
211{
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        match self {
214            Self::Simple(level) => write!(f, "{level}"),
215            // We want to print the parameter as `%{parameter}`. So we escape the `{` and `}`.
216            Self::Parameter(level) => write!(f, "%{{{level}}}"),
217        }
218    }
219}
220
221impl From<Level<&str>> for Level<String> {
222    fn from(value: Level<&str>) -> Self {
223        match value {
224            Level::Simple(simple) => Level::Simple(simple.into()),
225            Level::Parameter(param) => Level::Parameter(param.into()),
226        }
227    }
228}
229
230impl<'a, T> PartialEq<&'a str> for Level<T>
231where
232    T: PartialEq<&'a str> + Eq,
233{
234    fn eq(&self, other: &&'a str) -> bool {
235        self.eq_str(other)
236    }
237}
238
239impl<'a, T> PartialEq<Level<T>> for &'a str
240where
241    T: PartialEq<&'a str> + Eq,
242{
243    fn eq(&self, other: &Level<T>) -> bool {
244        other.eq_str(self)
245    }
246}
247
248/// Error that can happen when parsing an endpoint.
249#[non_exhaustive]
250#[derive(thiserror::Error, Debug, Clone)]
251pub enum EndpointError {
252    /// Missing forward slash at the beginning of the endpoint.
253    #[error("endpoint must start with a slash, got instead: {0}")]
254    Prefix(String),
255    /// Endpoints must contain at least one level.
256    ///
257    /// The empty endpoint is reserved.
258    #[error("endpoint must contain at least a level: {0}")]
259    Empty(String),
260    /// Endpoints must have at most 64 levels
261    #[error("endpoint must contain at most 64 levels, but it has {0}")]
262    MaxLen(usize),
263    /// Couldn't parse the endpoint's level.
264    #[error("endpoint contains invalid level: {input}")]
265    Level {
266        /// The original endpoint.
267        input: String,
268        /// Reason for the invalid level.
269        #[source]
270        error: LevelError,
271    },
272}
273
274/// Error that can happen when parsing a level.
275#[non_exhaustive]
276#[derive(thiserror::Error, Debug, Clone)]
277pub enum LevelError {
278    /// The level must contain at least one character, it cannot be `//`.
279    #[error("levels must not be empty")]
280    Empty,
281    /// Invalid character in the level.
282    #[error("levels contain an invalid character {0}")]
283    InvalidCharacter(char),
284    /// Mixed characters and parameter in level.
285    ///
286    /// A parameter should incapsulate the whole level (e.g. `/foo%{bar}` is invalid).
287    #[error("the parameter should incapsulate the whole level")]
288    Parameter,
289}
290
291/// Parses an interface endpoint with the following grammar:
292///
293/// ```text
294/// endpoint: '/' level+
295/// # We don't allow ending the endpoint with a '/'
296/// level: (parameter | simple ) ('/' | EOF)
297///
298/// # A parameter is an escaped simple level
299/// parameter: '%{' simple '}
300///
301/// simple: simple_part+
302/// # Make sure there is no parameter inside by escaping the '{'.
303/// # This grammar will not parse a '%' alone at the end of level.
304/// simple_part: '%' escape_param | level_char
305///
306/// # Any UTF-8 character except
307/// # - '/' for the level
308/// # - '+' and '#' since they are MQTT wild card
309/// # - '%' since it is used to escape a parameter
310/// level_char: [^/+#%]
311/// # Same as level_char, but disallowing the '{'
312/// escape_param: [^/+#%{]
313/// ```
314///
315/// Our implementation differs from the grammar in the following ways:
316///
317/// - We allow ending the level with a '%' since we can peek
318///
319fn parse_endpoint(input: &str) -> Result<Endpoint<&str>, EndpointError> {
320    trace!("parsing endpoint: {}", input);
321
322    let endpoint = input
323        .strip_prefix('/')
324        .ok_or_else(|| EndpointError::Prefix(input.to_string()))?;
325
326    let levels = endpoint
327        .split('/')
328        .map(parse_level)
329        .collect::<Result<Vec<_>, LevelError>>()
330        .map_err(|error| EndpointError::Level {
331            input: input.to_string(),
332            error,
333        })?;
334
335    if levels.is_empty() {
336        return Err(EndpointError::Empty(input.to_string()));
337    }
338
339    if levels.len() > ENDPOINT_MAX_LEN {
340        return Err(EndpointError::MaxLen(levels.len()));
341    }
342
343    trace!("levels: {:?}", levels);
344
345    Ok(Endpoint { levels })
346}
347
348fn parse_level(input: &str) -> Result<Level<&str>, LevelError> {
349    trace!("parsing level: {}", input);
350
351    let level = if let Some(param) = parse_parameter(input)? {
352        trace!("level is a parameter: {}", param);
353
354        Level::Parameter(param)
355    } else {
356        let level = parse_simple(input)?;
357
358        trace!("level is simple: {}", level);
359
360        Level::Simple(level)
361    };
362
363    Ok(level)
364}
365
366fn parse_simple(input: &str) -> Result<&str, LevelError> {
367    let mut chars = input.chars().peekable();
368
369    match chars.next() {
370        Some('a'..='z' | 'A'..='Z') => {}
371        Some(c) => return Err(LevelError::InvalidCharacter(c)),
372        None => return Err(LevelError::Empty),
373    }
374
375    while let Some(chr) = chars.next() {
376        match chr {
377            '%' if Some('{') == chars.peek().copied() => {
378                return Err(LevelError::Parameter);
379            }
380            'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => {}
381            c => return Err(LevelError::InvalidCharacter(c)),
382        }
383    }
384
385    Ok(input)
386}
387
388fn parse_parameter(input: &str) -> Result<Option<&str>, LevelError> {
389    let parameter = input
390        .strip_prefix("%{")
391        .and_then(|input| input.strip_suffix('}'));
392
393    let name = match parameter {
394        Some(param) => {
395            let name = parse_simple(param)?;
396
397            Some(name)
398        }
399        None => None,
400    };
401
402    Ok(name)
403}
404
405#[cfg(test)]
406mod tests {
407    use pretty_assertions::assert_eq;
408
409    use super::*;
410
411    #[test]
412    fn endpoint_equals_to_mapping() {
413        let endpoint = Endpoint {
414            levels: vec![
415                Level::Parameter("sensor_id".to_string()),
416                Level::Simple("boolean_endpoint".to_string()),
417            ],
418        };
419
420        let path = MappingPath::try_from("/1/boolean_endpoint").unwrap();
421
422        assert!(endpoint.eq_mapping(&path));
423    }
424
425    #[test]
426    fn test_parse_parameter() {
427        let res = parse_parameter("%{test}");
428
429        assert!(
430            res.is_ok(),
431            "failed to parse parameter: {}",
432            res.unwrap_err()
433        );
434
435        let parameter = res.unwrap();
436
437        assert_eq!(parameter, Some("test"));
438    }
439
440    #[test]
441    fn test_parse_level_parameter() {
442        let level = parse_level("%{test}").unwrap();
443
444        assert_eq!(level, Level::Parameter("test"));
445    }
446
447    #[test]
448    fn test_parse_endpoint() {
449        let res = parse_endpoint("/a/%{b}/c");
450
451        assert!(
452            res.is_ok(),
453            "failed to parse endpoint: {}",
454            res.unwrap_err()
455        );
456
457        let endpoint = res.unwrap();
458
459        let expected = Endpoint {
460            levels: vec![
461                Level::Simple("a"),
462                Level::Parameter("b"),
463                Level::Simple("c"),
464            ],
465        };
466
467        assert_eq!(endpoint, expected);
468    }
469
470    #[test]
471    fn test_parse_endpoint_first() {
472        let res = parse_endpoint("/%{a}/b/c");
473
474        assert!(
475            res.is_ok(),
476            "failed to parse endpoint: {}",
477            res.unwrap_err()
478        );
479
480        let endpoint = res.unwrap();
481
482        let expected = Endpoint {
483            levels: vec![
484                Level::Parameter("a"),
485                Level::Simple("b"),
486                Level::Simple("c"),
487            ],
488        };
489
490        assert_eq!(endpoint, expected);
491    }
492
493    #[test]
494    fn test_parse_endpoint_multi() {
495        let res = parse_endpoint("/a/%{b}/c/%{d}/e");
496
497        assert!(
498            res.is_ok(),
499            "failed to parse endpoint: {}",
500            res.unwrap_err()
501        );
502
503        let endpoint = res.unwrap();
504
505        let expected = Endpoint {
506            levels: vec![
507                Level::Simple("a"),
508                Level::Parameter("b"),
509                Level::Simple("c"),
510                Level::Parameter("d"),
511                Level::Simple("e"),
512            ],
513        };
514
515        assert_eq!(endpoint, expected);
516    }
517
518    #[test]
519    fn test_parse_endpoint_parameters() {
520        let cases = [
521            (
522                "/%{sensor_id}/boolean_endpoint",
523                Endpoint {
524                    levels: vec![
525                        Level::Parameter("sensor_id"),
526                        Level::Simple("boolean_endpoint"),
527                    ],
528                },
529            ),
530            (
531                "/%{sensor_id}/enable",
532                Endpoint {
533                    levels: vec![Level::Parameter("sensor_id"), Level::Simple("enable")],
534                },
535            ),
536        ];
537
538        for (endpoint, expected) in cases {
539            let res = parse_endpoint(endpoint);
540
541            assert!(
542                res.is_ok(),
543                "failed to parse endpoint: {}",
544                res.unwrap_err()
545            );
546
547            let endpoint = res.unwrap();
548
549            assert_eq!(endpoint, expected);
550        }
551    }
552
553    #[test]
554    fn level_eq_str() {
555        let param = Level::Parameter("sensor_id".to_string());
556
557        assert_eq!(param, "some");
558        assert_eq!(param, "foo");
559
560        let simple = Level::Simple("boolean_endpoint".to_string());
561
562        assert_eq!(simple, "boolean_endpoint");
563        assert_ne!(simple, "foo");
564    }
565
566    #[test]
567    fn level_param_eq_ignore_param() {
568        let a = Level::Parameter("foo");
569        let b = Level::Parameter("bar");
570
571        assert_eq!(a, b);
572    }
573}