Skip to main content

uptrakit_surfaces/
ids.rs

1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4pub const MAX_IDENTIFIER_LEN: usize = 128;
5
6#[derive(Debug, Clone, PartialEq, Eq, Error)]
7pub enum IdentifierError {
8    #[error("identifier cannot be empty")]
9    Empty,
10    #[error("identifier exceeds maximum length of {MAX_IDENTIFIER_LEN} bytes")]
11    TooLong,
12    #[error("identifier must start with an ASCII lowercase letter")]
13    InvalidStart,
14    #[error("identifier contains invalid character `{ch}` at byte index {index}")]
15    InvalidCharacter { ch: char, index: usize },
16}
17
18/// Validates a surface identifier against the shared contract.
19///
20/// # Errors
21/// Returns [`IdentifierError::Empty`] when `value` is empty.
22/// Returns [`IdentifierError::TooLong`] when `value` exceeds [`MAX_IDENTIFIER_LEN`] bytes.
23/// Returns [`IdentifierError::InvalidStart`] when the first character is not an ASCII lowercase letter.
24/// Returns [`IdentifierError::InvalidCharacter`] when any subsequent character is outside `[a-z0-9._-]`.
25pub fn validate_surface_identifier(value: &str) -> Result<(), IdentifierError> {
26    if value.is_empty() {
27        return Err(IdentifierError::Empty);
28    }
29    if value.len() > MAX_IDENTIFIER_LEN {
30        return Err(IdentifierError::TooLong);
31    }
32
33    let mut chars = value.char_indices();
34    let (_, first) = chars.next().ok_or(IdentifierError::Empty)?;
35    if !first.is_ascii_lowercase() {
36        return Err(IdentifierError::InvalidStart);
37    }
38
39    for (index, ch) in chars {
40        if ch.is_ascii_lowercase() || ch.is_ascii_digit() || matches!(ch, '.' | '_' | '-') {
41            continue;
42        }
43        return Err(IdentifierError::InvalidCharacter { ch, index });
44    }
45
46    Ok(())
47}
48
49#[must_use]
50pub fn is_valid_surface_identifier(value: &str) -> bool {
51    validate_surface_identifier(value).is_ok()
52}
53
54macro_rules! identifier_type {
55    ($name:ident) => {
56        #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
57        #[serde(transparent)]
58        pub struct $name(String);
59
60        impl $name {
61            /// Constructs a validated identifier instance.
62            ///
63            /// # Errors
64            /// Returns any [`IdentifierError`] produced by
65            /// [`validate_surface_identifier`] when `value` does not satisfy
66            /// the identifier contract.
67            pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
68                let value = value.into();
69                validate_surface_identifier(&value)?;
70                Ok(Self(value))
71            }
72
73            #[must_use]
74            pub const fn as_str(&self) -> &str {
75                self.0.as_str()
76            }
77        }
78
79        impl std::fmt::Display for $name {
80            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81                f.write_str(self.as_str())
82            }
83        }
84
85        impl std::str::FromStr for $name {
86            type Err = IdentifierError;
87
88            fn from_str(value: &str) -> Result<Self, Self::Err> {
89                Self::new(value.to_owned())
90            }
91        }
92
93        impl TryFrom<String> for $name {
94            type Error = IdentifierError;
95
96            fn try_from(value: String) -> Result<Self, Self::Error> {
97                Self::new(value)
98            }
99        }
100
101        impl<'de> Deserialize<'de> for $name {
102            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
103            where
104                D: serde::Deserializer<'de>,
105            {
106                let value = String::deserialize(deserializer)?;
107                Self::new(value).map_err(serde::de::Error::custom)
108            }
109        }
110    };
111}
112
113identifier_type!(SurfaceId);
114identifier_type!(InteractionId);
115identifier_type!(DataSourceId);
116identifier_type!(ControllerQueryId);
117identifier_type!(BuiltInApiOperationId);
118identifier_type!(SurfaceTabId);