up_rust/
uattributes.rs

1/********************************************************************************
2 * Copyright (c) 2023 Contributors to the Eclipse Foundation
3 *
4 * See the NOTICE file(s) distributed with this work for additional
5 * information regarding copyright ownership.
6 *
7 * This program and the accompanying materials are made available under the
8 * terms of the Apache License Version 2.0 which is available at
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * SPDX-License-Identifier: Apache-2.0
12 ********************************************************************************/
13
14mod uattributesvalidator;
15mod upayloadformat;
16mod upriority;
17
18use std::time::SystemTime;
19
20pub use uattributesvalidator::*;
21pub use upriority::*;
22
23pub use crate::up_core_api::uattributes::*;
24use crate::UUID;
25
26pub(crate) const UPRIORITY_DEFAULT: UPriority = UPriority::UPRIORITY_CS1;
27
28#[derive(Debug)]
29pub enum UAttributesError {
30    ValidationError(String),
31    ParsingError(String),
32}
33
34impl UAttributesError {
35    pub fn validation_error<T>(message: T) -> UAttributesError
36    where
37        T: Into<String>,
38    {
39        Self::ValidationError(message.into())
40    }
41
42    pub fn parsing_error<T>(message: T) -> UAttributesError
43    where
44        T: Into<String>,
45    {
46        Self::ParsingError(message.into())
47    }
48}
49
50impl std::fmt::Display for UAttributesError {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        match self {
53            Self::ValidationError(e) => f.write_fmt(format_args!("Validation failure: {e}")),
54            Self::ParsingError(e) => f.write_fmt(format_args!("Parsing error: {e}")),
55        }
56    }
57}
58
59impl std::error::Error for UAttributesError {}
60
61impl UAttributes {
62    /// Checks if a given priority class is the default priority class.
63    ///
64    /// Messages that do not have a priority class set explicity, are assigned to
65    /// the default priority class.
66    pub(crate) fn is_default_priority(prio: UPriority) -> bool {
67        prio == UPRIORITY_DEFAULT
68    }
69
70    /// Checks if these are the attributes for a Publish message.
71    ///
72    /// # Examples
73    ///
74    /// ```rust
75    /// use up_rust::{UAttributes, UMessageType};
76    ///
77    /// let attribs = UAttributes {
78    ///   type_: UMessageType::UMESSAGE_TYPE_PUBLISH.into(),
79    ///   ..Default::default()
80    /// };
81    /// assert!(attribs.is_publish());
82    /// ```
83    pub fn is_publish(&self) -> bool {
84        self.type_.enum_value() == Ok(UMessageType::UMESSAGE_TYPE_PUBLISH)
85    }
86
87    /// Checks if these are the attributes for an RPC Request message.
88    ///
89    /// # Examples
90    ///
91    /// ```rust
92    /// use up_rust::{UAttributes, UMessageType};
93    ///
94    /// let attribs = UAttributes {
95    ///   type_: UMessageType::UMESSAGE_TYPE_REQUEST.into(),
96    ///   ..Default::default()
97    /// };
98    /// assert!(attribs.is_request());
99    /// ```
100    pub fn is_request(&self) -> bool {
101        self.type_.enum_value() == Ok(UMessageType::UMESSAGE_TYPE_REQUEST)
102    }
103
104    /// Checks if these are the attributes for an RPC Response message.
105    ///
106    /// # Examples
107    ///
108    /// ```rust
109    /// use up_rust::{UAttributes, UMessageType};
110    ///
111    /// let attribs = UAttributes {
112    ///   type_: UMessageType::UMESSAGE_TYPE_RESPONSE.into(),
113    ///   ..Default::default()
114    /// };
115    /// assert!(attribs.is_response());
116    /// ```
117    pub fn is_response(&self) -> bool {
118        self.type_.enum_value() == Ok(UMessageType::UMESSAGE_TYPE_RESPONSE)
119    }
120
121    /// Checks if these are the attributes for a Notification message.
122    ///
123    /// # Examples
124    ///
125    /// ```rust
126    /// use up_rust::{UAttributes, UMessageType};
127    ///
128    /// let attribs = UAttributes {
129    ///   type_: UMessageType::UMESSAGE_TYPE_NOTIFICATION.into(),
130    ///   ..Default::default()
131    /// };
132    /// assert!(attribs.is_notification());
133    /// ```
134    pub fn is_notification(&self) -> bool {
135        self.type_.enum_value() == Ok(UMessageType::UMESSAGE_TYPE_NOTIFICATION)
136    }
137
138    /// Checks if the message that is described by these attributes should be considered expired.
139    ///
140    /// # Errors
141    ///
142    /// Returns an error if [`Self::ttl`] (time-to-live) contains a value greater than 0, but
143    /// * the current system time cannot be determined, or
144    /// * the message has expired according to the timestamp extracted from [`Self::id`] and the time-to-live value.
145    pub fn check_expired(&self) -> Result<(), UAttributesError> {
146        if let Some(ttl) = self.ttl {
147            if ttl == 0 {
148                return Ok(());
149            }
150        }
151        SystemTime::now()
152            .duration_since(SystemTime::UNIX_EPOCH)
153            .map_err(|_e| {
154                UAttributesError::validation_error("Cannot determine current system time")
155            })
156            .and_then(|duration_since_epoch| {
157                self.check_expired_for_reference(duration_since_epoch.as_millis())
158            })
159    }
160
161    /// Checks if the message that is described by these attributes should be considered expired.
162    ///
163    /// # Arguments
164    /// * `reference_time` - The reference time as a `Duration` since UNIX epoch. The check will be performed in relation to this point in time.
165    ///
166    /// # Errors
167    ///
168    /// Returns an error if [`Self::ttl`] (time-to-live) contains a value greater than 0, but
169    /// the message has expired according to the timestamp extracted from [`Self::id`], the
170    /// time-to-live value and the provided reference time.
171    pub fn check_expired_for_reference(
172        &self,
173        reference_time: u128,
174    ) -> Result<(), UAttributesError> {
175        let ttl = match self.ttl {
176            Some(t) if t > 0 => u128::from(t),
177            _ => return Ok(()),
178        };
179
180        if let Some(creation_time) = self.id.as_ref().and_then(UUID::get_time) {
181            if (creation_time as u128).saturating_add(ttl) <= reference_time {
182                return Err(UAttributesError::validation_error("Message has expired"));
183            }
184        }
185        Ok(())
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use std::time::UNIX_EPOCH;
192
193    use super::*;
194    use test_case::test_case;
195
196    /// Creates a UUID for a given creation time offset.
197    ///
198    /// # Note
199    ///
200    /// For internal testing purposes only. For end-users, please use [`UUID::build()`]
201    fn build_for_time_offset(offset_millis: i64) -> UUID {
202        let duration_since_unix_epoch = SystemTime::now()
203            .duration_since(UNIX_EPOCH)
204            .expect("current system time is set to a point in time before UNIX Epoch");
205        let now_as_millis_since_epoch: u64 = u64::try_from(duration_since_unix_epoch.as_millis())
206            .expect("current system time is too far in the future");
207        let creation_timestamp = now_as_millis_since_epoch
208            .checked_add_signed(offset_millis)
209            .unwrap();
210        UUID::build_for_timestamp_millis(creation_timestamp)
211    }
212
213    #[test_case(None, None, false; "for message without ID nor TTL")]
214    #[test_case(None, Some(0), false; "for message without ID with TTL 0")]
215    #[test_case(None, Some(500), false; "for message without ID with TTL")]
216    #[test_case(Some(build_for_time_offset(-1000)), None, false; "for past message without TTL")]
217    #[test_case(Some(build_for_time_offset(-1000)), Some(0), false; "for past message with TTL 0")]
218    #[test_case(Some(build_for_time_offset(-1000)), Some(500), true; "for past message with expired TTL")]
219    #[test_case(Some(build_for_time_offset(-1000)), Some(2000), false; "for past message with non-expired TTL")]
220    #[test_case(Some(build_for_time_offset(1000)), Some(2000), false; "for future message with TTL")]
221    #[test_case(Some(build_for_time_offset(1000)), None, false; "for future message without TTL")]
222    fn test_is_expired(id: Option<UUID>, ttl: Option<u32>, should_be_expired: bool) {
223        let attributes = UAttributes {
224            type_: UMessageType::UMESSAGE_TYPE_NOTIFICATION.into(),
225            priority: UPriority::UPRIORITY_CS1.into(),
226            id: id.into(),
227            ttl,
228            ..Default::default()
229        };
230
231        assert!(attributes.check_expired().is_err() == should_be_expired);
232    }
233}