odos_sdk/
api_key.rs

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