Skip to main content

lastid_sdk/types/
client_id.rs

1//! OAuth client identifier newtype.
2//!
3//! Provides a validated client ID type that ensures non-empty values.
4
5use std::fmt;
6use std::str::FromStr;
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::LastIDError;
11
12/// A validated OAuth client identifier.
13///
14/// Client IDs must be non-empty strings. This newtype ensures validation
15/// on construction to catch empty client IDs early in the configuration
16/// process rather than at request time.
17///
18/// # Examples
19///
20/// ```rust
21/// use lastid_sdk::types::ClientId;
22///
23/// // Valid client ID
24/// let client_id = ClientId::new("my-app").unwrap();
25/// assert_eq!(client_id.as_str(), "my-app");
26///
27/// // Empty client ID fails validation
28/// assert!(ClientId::new("").is_err());
29/// assert!(ClientId::new("   ").is_err());
30/// ```
31#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[serde(try_from = "String", into = "String")]
33pub struct ClientId(String);
34
35impl ClientId {
36    /// Creates a new `ClientId` after validating it's non-empty.
37    ///
38    /// # Errors
39    ///
40    /// Returns `LastIDError::EmptyClientId` if the string is empty or
41    /// contains only whitespace.
42    pub fn new(value: impl Into<String>) -> Result<Self, LastIDError> {
43        let s = value.into();
44        Self::validate(&s)?;
45        Ok(Self(s))
46    }
47
48    /// Returns the client ID as a string slice.
49    #[must_use]
50    pub fn as_str(&self) -> &str {
51        &self.0
52    }
53
54    /// Validates a client ID string.
55    fn validate(s: &str) -> Result<(), LastIDError> {
56        if s.is_empty() || s.trim().is_empty() {
57            return Err(LastIDError::EmptyClientId);
58        }
59        Ok(())
60    }
61}
62
63impl fmt::Display for ClientId {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "{}", self.0)
66    }
67}
68
69impl AsRef<str> for ClientId {
70    fn as_ref(&self) -> &str {
71        &self.0
72    }
73}
74
75impl From<ClientId> for String {
76    fn from(client_id: ClientId) -> Self {
77        client_id.0
78    }
79}
80
81impl TryFrom<String> for ClientId {
82    type Error = LastIDError;
83
84    fn try_from(value: String) -> Result<Self, Self::Error> {
85        Self::new(value)
86    }
87}
88
89impl TryFrom<&str> for ClientId {
90    type Error = LastIDError;
91
92    fn try_from(value: &str) -> Result<Self, Self::Error> {
93        Self::new(value)
94    }
95}
96
97impl FromStr for ClientId {
98    type Err = LastIDError;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        Self::new(s)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_valid_client_id() {
111        let id = ClientId::new("my-app").unwrap();
112        assert_eq!(id.as_str(), "my-app");
113    }
114
115    #[test]
116    fn test_valid_client_id_with_special_chars() {
117        let id = ClientId::new("app_123-test.com").unwrap();
118        assert_eq!(id.as_str(), "app_123-test.com");
119    }
120
121    #[test]
122    fn test_invalid_empty() {
123        assert!(ClientId::new("").is_err());
124    }
125
126    #[test]
127    fn test_invalid_whitespace_only() {
128        assert!(ClientId::new("   ").is_err());
129        assert!(ClientId::new("\t\n").is_err());
130    }
131
132    #[test]
133    fn test_display() {
134        let id = ClientId::new("test-app").unwrap();
135        assert_eq!(format!("{id}"), "test-app");
136    }
137
138    #[test]
139    fn test_from_str() {
140        let id: ClientId = "my-client".parse().unwrap();
141        assert_eq!(id.as_str(), "my-client");
142    }
143
144    #[test]
145    fn test_try_from_string() {
146        let id = ClientId::try_from("test".to_string()).unwrap();
147        assert_eq!(id.as_str(), "test");
148    }
149
150    #[test]
151    fn test_into_string() {
152        let id = ClientId::new("test").unwrap();
153        let s: String = id.into();
154        assert_eq!(s, "test");
155    }
156
157    #[test]
158    fn test_serde_roundtrip() {
159        let id = ClientId::new("my-app").unwrap();
160        let json = serde_json::to_string(&id).unwrap();
161        assert_eq!(json, "\"my-app\"");
162
163        let deserialized: ClientId = serde_json::from_str(&json).unwrap();
164        assert_eq!(deserialized, id);
165    }
166
167    #[test]
168    fn test_serde_invalid_empty() {
169        let result: Result<ClientId, _> = serde_json::from_str("\"\"");
170        assert!(result.is_err());
171    }
172}