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(
37    Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, JsonSchema,
38)]
39#[schemars(rename = "PrefixedUuid")]
40pub struct PrefixedUuid<const PFX_1: char, const PFX_2: char, const PFX_3: char>
41{
42    uuid: uuid::Uuid,
43}
44
45impl<const PFX_1: char, const PFX_2: char, const PFX_3: char> From<uuid::Uuid>
46    for PrefixedUuid<PFX_1, PFX_2, PFX_3>
47{
48    fn from(uuid: uuid::Uuid) -> Self {
49        PrefixedUuid { uuid }
50    }
51}
52
53/// Error type for parsing prefixed UUIDs from strings.
54///
55/// This enum represents the two possible failure modes when parsing
56/// a prefixed UUID: an invalid prefix or an invalid UUID portion.
57#[derive(Debug, Clone, thiserror::Error)]
58pub enum ParseError<const PFX_1: char, const PFX_2: char, const PFX_3: char> {
59    /// The string did not start with the expected prefix.
60    #[error(
61        "invalid prefix: expected {}{}{} but got {}",
62        PFX_1,
63        PFX_2,
64        PFX_3,
65        _0
66    )]
67    InvalidPrefix(String),
68    /// The UUID portion of the string was invalid.
69    #[error("invalid UUID: {0}")]
70    InvalidUuid(uuid::Error),
71}
72
73impl<const PFX_1: char, const PFX_2: char, const PFX_3: char> FromStr
74    for PrefixedUuid<PFX_1, PFX_2, PFX_3>
75{
76    type Err = ParseError<PFX_1, PFX_2, PFX_3>;
77    fn from_str(s: &str) -> Result<Self, Self::Err> {
78        if s.len() >= 3 + uuid::fmt::Simple::LENGTH && {
79            let s_bytes = s.as_bytes();
80            s_bytes[0] == (PFX_1 as u8)
81                && s_bytes[1] == (PFX_2 as u8)
82                && s_bytes[2] == (PFX_3 as u8)
83        } {
84            match uuid::Uuid::parse_str(&s[3..]) {
85                Ok(uuid) => Ok(PrefixedUuid { uuid }),
86                Err(e) => Err(ParseError::InvalidUuid(e)),
87            }
88        } else {
89            Err(ParseError::InvalidPrefix(s.to_string()))
90        }
91    }
92}
93
94impl<const PFX_1: char, const PFX_2: char, const PFX_3: char>
95    PrefixedUuid<PFX_1, PFX_2, PFX_3>
96{
97    /// Creates a new prefixed UUID with a random v4 UUID.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use objectiveai_sdk::prefixed_uuid::PrefixedUuid;
103    ///
104    /// type ApiKey = PrefixedUuid<'a', 'p', 'k'>;
105    /// let key = ApiKey::new();
106    /// ```
107    pub fn new() -> Self {
108        PrefixedUuid {
109            uuid: uuid::Uuid::new_v4(),
110        }
111    }
112
113    /// Returns the underlying UUID without the prefix.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use objectiveai_sdk::prefixed_uuid::PrefixedUuid;
119    ///
120    /// type ApiKey = PrefixedUuid<'a', 'p', 'k'>;
121    /// let key = ApiKey::new();
122    /// let uuid = key.uuid();
123    /// ```
124    pub fn uuid(&self) -> uuid::Uuid {
125        self.uuid
126    }
127}
128
129impl<const PFX_1: char, const PFX_2: char, const PFX_3: char> std::fmt::Display
130    for PrefixedUuid<PFX_1, PFX_2, PFX_3>
131{
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        write!(
134            f,
135            "{}{}{}{}",
136            PFX_1,
137            PFX_2,
138            PFX_3,
139            self.uuid
140                .simple()
141                .encode_lower(&mut [0; uuid::fmt::Simple::LENGTH])
142        )
143    }
144}
145
146impl<const PFX_1: char, const PFX_2: char, const PFX_3: char> serde::Serialize
147    for PrefixedUuid<PFX_1, PFX_2, PFX_3>
148{
149    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
150    where
151        S: serde::Serializer,
152    {
153        serializer.serialize_str(&self.to_string())
154    }
155}
156
157impl<'de, const PFX_1: char, const PFX_2: char, const PFX_3: char>
158    serde::Deserialize<'de> for PrefixedUuid<PFX_1, PFX_2, PFX_3>
159{
160    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
161    where
162        D: serde::Deserializer<'de>,
163    {
164        let s = String::deserialize(deserializer)?;
165        PrefixedUuid::from_str(&s).map_err(serde::de::Error::custom)
166    }
167}