odos_sdk/
api_key.rs

1use std::{fmt, str::FromStr};
2
3use uuid::Uuid;
4
5use crate::{OdosError, Result};
6
7/// API key for authenticating with the Odos API
8///
9/// Wraps a UUID-formatted API key in a type-safe manner with secure debug formatting
10/// that redacts the key value to prevent accidental logging.
11///
12/// # Examples
13///
14/// ```rust
15/// use odos_sdk::ApiKey;
16/// use std::str::FromStr;
17///
18/// let api_key = ApiKey::from_str("11111111-1a11-1111-a11a-aaa11a111a1a").unwrap();
19///
20/// // The key is redacted in debug output
21/// println!("{:?}", api_key); // Prints: ApiKey([REDACTED])
22/// ```
23#[derive(Clone, Copy, PartialEq, Eq)]
24pub struct ApiKey(Uuid);
25
26impl ApiKey {
27    /// Create a new API key from a UUID
28    ///
29    /// # Arguments
30    ///
31    /// * `uuid` - The API key as a UUID
32    ///
33    /// # Examples
34    ///
35    /// ```rust
36    /// use odos_sdk::ApiKey;
37    /// use uuid::Uuid;
38    /// use std::str::FromStr;
39    ///
40    /// let uuid = Uuid::from_str("11111111-1a11-1111-a11a-aaa11a111a1a").unwrap();
41    /// let api_key = ApiKey::new(uuid);
42    /// ```
43    pub fn new(uuid: Uuid) -> Self {
44        Self(uuid)
45    }
46
47    /// Get the underlying UUID
48    ///
49    /// # Security
50    ///
51    /// Be careful when using this method - avoid logging or displaying
52    /// the raw key value in production.
53    pub fn as_uuid(&self) -> &Uuid {
54        &self.0
55    }
56
57    /// Get the API key as a string
58    ///
59    /// # Security
60    ///
61    /// Be careful when using this method - avoid logging or displaying
62    /// the raw key value in production.
63    pub fn as_str(&self) -> String {
64        self.0.to_string()
65    }
66}
67
68impl FromStr for ApiKey {
69    type Err = OdosError;
70
71    fn from_str(s: &str) -> Result<Self> {
72        let uuid = Uuid::from_str(s).map_err(|e| {
73            OdosError::invalid_input(format!("Invalid API key format (expected UUID): {}", e))
74        })?;
75        Ok(Self(uuid))
76    }
77}
78
79impl From<Uuid> for ApiKey {
80    fn from(uuid: Uuid) -> Self {
81        Self(uuid)
82    }
83}
84
85/// Secure Debug implementation that redacts the API key
86impl fmt::Debug for ApiKey {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        f.write_str("ApiKey([REDACTED])")
89    }
90}
91
92/// Display implementation that redacts the API key
93impl fmt::Display for ApiKey {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        f.write_str("[REDACTED]")
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_api_key_new() {
105        let uuid = Uuid::new_v4();
106        let api_key = ApiKey::new(uuid);
107        assert_eq!(api_key.as_uuid(), &uuid);
108    }
109
110    #[test]
111    fn test_api_key_from_uuid() {
112        let uuid = Uuid::new_v4();
113        let api_key = ApiKey::from(uuid);
114        assert_eq!(api_key.as_uuid(), &uuid);
115    }
116
117    #[test]
118    fn test_api_key_from_str() {
119        let uuid = Uuid::new_v4();
120        let key = uuid.to_string();
121        let api_key = ApiKey::from_str(&key).unwrap();
122        assert_eq!(api_key.as_str(), key);
123    }
124
125    #[test]
126    fn test_api_key_from_str_invalid() {
127        let result = ApiKey::from_str("not-a-uuid");
128        assert!(result.is_err());
129        if let Err(e) = result {
130            let error_msg = e.to_string();
131            assert!(error_msg.contains("Invalid API key format"));
132        }
133    }
134
135    #[test]
136    fn test_api_key_debug_redacted() {
137        let uuid = Uuid::new_v4();
138        let api_key = ApiKey::new(uuid);
139        let debug_output = format!("{:?}", api_key);
140        assert_eq!(debug_output, "ApiKey([REDACTED])");
141        let uuid_str = uuid.to_string();
142        assert!(!debug_output.contains(&uuid_str));
143    }
144
145    #[test]
146    fn test_api_key_display_redacted() {
147        let uuid = Uuid::new_v4();
148        let api_key = ApiKey::new(uuid);
149        let display_output = format!("{}", api_key);
150        assert_eq!(display_output, "[REDACTED]");
151        let uuid_str = uuid.to_string();
152        assert!(!display_output.contains(&uuid_str));
153    }
154}