Skip to main content

cloud_sdk/operation/
operation_id.rs

1//! Bounded provider operation identifiers.
2
3use core::fmt;
4
5/// Maximum bytes in a provider operation identifier.
6pub const MAX_OPERATION_ID_BYTES: usize = 128;
7
8/// Invalid provider operation identifier.
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum OperationIdError {
11    /// The identifier is empty.
12    Empty,
13    /// The identifier exceeds [`MAX_OPERATION_ID_BYTES`].
14    TooLong,
15    /// The identifier contains a byte outside lowercase ASCII, digits, or `_`.
16    InvalidByte,
17}
18
19impl fmt::Display for OperationIdError {
20    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
21        formatter.write_str(match self {
22            Self::Empty => "operation identifier is empty",
23            Self::TooLong => "operation identifier is too long",
24            Self::InvalidByte => "operation identifier contains an invalid byte",
25        })
26    }
27}
28
29impl core::error::Error for OperationIdError {}
30
31/// Validated static identifier assigned by a provider specification.
32#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33pub struct OperationId(&'static str);
34
35impl OperationId {
36    /// Validates a provider operation identifier.
37    pub const fn new(value: &'static str) -> Result<Self, OperationIdError> {
38        if value.is_empty() {
39            return Err(OperationIdError::Empty);
40        }
41        if value.len() > MAX_OPERATION_ID_BYTES {
42            return Err(OperationIdError::TooLong);
43        }
44        let mut remaining = value.as_bytes();
45        while let Some((byte, tail)) = remaining.split_first() {
46            if !byte.is_ascii_lowercase() && !byte.is_ascii_digit() && *byte != b'_' {
47                return Err(OperationIdError::InvalidByte);
48            }
49            remaining = tail;
50        }
51        Ok(Self(value))
52    }
53
54    /// Returns the validated identifier.
55    #[must_use]
56    pub const fn as_str(self) -> &'static str {
57        self.0
58    }
59}
60
61/// Creates a compile-time validated [`OperationId`].
62///
63/// Invalid literals fail during compilation:
64///
65/// ```compile_fail
66/// use cloud_sdk::operation_id;
67///
68/// let _ = operation_id!("Get-Server");
69/// ```
70#[macro_export]
71macro_rules! operation_id {
72    ($value:literal) => {{
73        const VALUE: $crate::operation::OperationId =
74            match $crate::operation::OperationId::new($value) {
75                Ok(value) => value,
76                Err(_) => panic!("invalid operation identifier literal"),
77            };
78        VALUE
79    }};
80}
81
82#[cfg(test)]
83mod tests {
84    use super::{OperationId, OperationIdError};
85
86    const GET_SERVER: OperationId = operation_id!("get_server");
87
88    #[test]
89    fn accepts_source_style_identifiers_and_rejects_ambiguous_text() {
90        assert_eq!(GET_SERVER.as_str(), "get_server");
91        assert_eq!(
92            OperationId::new("get_server").map(OperationId::as_str),
93            Ok("get_server")
94        );
95        assert_eq!(OperationId::new(""), Err(OperationIdError::Empty));
96        assert_eq!(
97            OperationId::new("Get-Server"),
98            Err(OperationIdError::InvalidByte)
99        );
100    }
101}