Skip to main content

objectiveai_sdk/
prefixed_uuid.rs

1//! Prefixed UUID type for ObjectiveAI identifiers.
2//!
3//! This module provides a generic UUID type with a 3-character prefix,
4//! used throughout the ObjectiveAI API for type-safe identifiers.
5//! For example, API keys use the prefix "apk" (e.g., `apk1234...`).
6
7use schemars::JsonSchema;
8use std::str::FromStr;
9
10/// A UUID with a 3-character prefix for type-safe identifiers.
11///
12/// This struct wraps a standard UUID and adds a compile-time prefix,
13/// ensuring that different types of identifiers (API keys, swarm IDs, etc.)
14/// cannot be confused at the type level.
15///
16/// The prefix is specified as three `const char` generic parameters.
17///
18/// # Type Parameters
19///
20/// * `PFX_1` - First character of the prefix
21/// * `PFX_2` - Second character of the prefix
22/// * `PFX_3` - Third character of the prefix
23///
24/// # Examples
25///
26/// ```
27/// use objectiveai_sdk::prefixed_uuid::PrefixedUuid;
28///
29/// // Define an API key type with prefix "apk"
30/// type ApiKey = PrefixedUuid<'a', 'p', 'k'>;
31///
32/// // Create a new API key
33/// let key = ApiKey::new();
34/// println!("{}", key); // Outputs: apk<uuid>
35/// ```
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, JsonSchema)]
37#[schemars(rename = "PrefixedUuid")]
38pub struct PrefixedUuid<const PFX_1: char, const PFX_2: char, const PFX_3: char>
39{
40    uuid: uuid::Uuid,
41}
42
43impl<const PFX_1: char, const PFX_2: char, const PFX_3: char> From<uuid::Uuid>
44    for PrefixedUuid<PFX_1, PFX_2, PFX_3>
45{
46    fn from(uuid: uuid::Uuid) -> Self {
47        PrefixedUuid { uuid }
48    }
49}
50
51/// Error type for parsing prefixed UUIDs from strings.
52///
53/// This enum represents the two possible failure modes when parsing
54/// a prefixed UUID: an invalid prefix or an invalid UUID portion.
55#[derive(Debug, Clone, thiserror::Error)]
56pub enum ParseError<const PFX_1: char, const PFX_2: char, const PFX_3: char> {
57    /// The string did not start with the expected prefix.
58    #[error(
59        "invalid prefix: expected {}{}{} but got {}",
60        PFX_1,
61        PFX_2,
62        PFX_3,
63        _0
64    )]
65    InvalidPrefix(String),
66    /// The UUID portion of the string was invalid.
67    #[error("invalid UUID: {0}")]
68    InvalidUuid(uuid::Error),
69}
70
71impl<const PFX_1: char, const PFX_2: char, const PFX_3: char> FromStr
72    for PrefixedUuid<PFX_1, PFX_2, PFX_3>
73{
74    type Err = ParseError<PFX_1, PFX_2, PFX_3>;
75    fn from_str(s: &str) -> Result<Self, Self::Err> {
76        if s.len() >= 3 + uuid::fmt::Simple::LENGTH && {
77            let s_bytes = s.as_bytes();
78            s_bytes[0] == (PFX_1 as u8)
79                && s_bytes[1] == (PFX_2 as u8)
80                && s_bytes[2] == (PFX_3 as u8)
81        } {
82            match uuid::Uuid::parse_str(&s[3..]) {
83                Ok(uuid) => Ok(PrefixedUuid { uuid }),
84                Err(e) => Err(ParseError::InvalidUuid(e)),
85            }
86        } else {
87            Err(ParseError::InvalidPrefix(s.to_string()))
88        }
89    }
90}
91
92impl<const PFX_1: char, const PFX_2: char, const PFX_3: char>
93    PrefixedUuid<PFX_1, PFX_2, PFX_3>
94{
95    /// Creates a new prefixed UUID with a random v4 UUID.
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use objectiveai_sdk::prefixed_uuid::PrefixedUuid;
101    ///
102    /// type ApiKey = PrefixedUuid<'a', 'p', 'k'>;
103    /// let key = ApiKey::new();
104    /// ```
105    pub fn new() -> Self {
106        PrefixedUuid {
107            uuid: uuid::Uuid::new_v4(),
108        }
109    }
110
111    /// Returns the underlying UUID without the prefix.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use objectiveai_sdk::prefixed_uuid::PrefixedUuid;
117    ///
118    /// type ApiKey = PrefixedUuid<'a', 'p', 'k'>;
119    /// let key = ApiKey::new();
120    /// let uuid = key.uuid();
121    /// ```
122    pub fn uuid(&self) -> uuid::Uuid {
123        self.uuid
124    }
125}
126
127impl<const PFX_1: char, const PFX_2: char, const PFX_3: char> std::fmt::Display
128    for PrefixedUuid<PFX_1, PFX_2, PFX_3>
129{
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        write!(
132            f,
133            "{}{}{}{}",
134            PFX_1,
135            PFX_2,
136            PFX_3,
137            self.uuid
138                .simple()
139                .encode_lower(&mut [0; uuid::fmt::Simple::LENGTH])
140        )
141    }
142}
143
144impl<const PFX_1: char, const PFX_2: char, const PFX_3: char> serde::Serialize
145    for PrefixedUuid<PFX_1, PFX_2, PFX_3>
146{
147    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
148    where
149        S: serde::Serializer,
150    {
151        serializer.serialize_str(&self.to_string())
152    }
153}
154
155impl<'de, const PFX_1: char, const PFX_2: char, const PFX_3: char>
156    serde::Deserialize<'de> for PrefixedUuid<PFX_1, PFX_2, PFX_3>
157{
158    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
159    where
160        D: serde::Deserializer<'de>,
161    {
162        let s = String::deserialize(deserializer)?;
163        PrefixedUuid::from_str(&s).map_err(serde::de::Error::custom)
164    }
165}