codex_auth_manager/
identity.rs1use std::{fmt, path::PathBuf, str::FromStr};
2
3use super::Error;
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct IdentityName(String);
8
9impl IdentityName {
10 #[must_use]
12 pub fn as_str(&self) -> &str {
13 &self.0
14 }
15}
16
17impl TryFrom<&str> for IdentityName {
18 type Error = Error;
19
20 fn try_from(value: &str) -> Result<Self, Self::Error> {
21 validate_identity_name(value)?;
22 Ok(Self(value.to_owned()))
23 }
24}
25
26impl TryFrom<String> for IdentityName {
27 type Error = Error;
28
29 fn try_from(value: String) -> Result<Self, Self::Error> {
30 validate_identity_name(&value)?;
31 Ok(Self(value))
32 }
33}
34
35impl FromStr for IdentityName {
36 type Err = Error;
37
38 fn from_str(value: &str) -> Result<Self, Self::Err> {
39 Self::try_from(value)
40 }
41}
42
43impl AsRef<str> for IdentityName {
44 fn as_ref(&self) -> &str {
45 self.as_str()
46 }
47}
48
49impl fmt::Display for IdentityName {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 f.write_str(&self.0)
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Identity {
58 pub name: IdentityName,
60 pub path: PathBuf,
62 pub active: bool,
64 pub broken: bool,
66}
67
68fn validate_identity_name(value: &str) -> Result<(), Error> {
69 let mut chars = value.chars();
70 let Some(first) = chars.next() else {
71 return Err(Error::InvalidIdentityName {
72 name: value.to_owned(),
73 });
74 };
75 if !first.is_ascii_alphanumeric()
76 || !chars.all(|char| char.is_ascii_alphanumeric() || matches!(char, '.' | '_' | '-'))
77 {
78 return Err(Error::InvalidIdentityName {
79 name: value.to_owned(),
80 });
81 }
82 Ok(())
83}
84
85#[cfg(test)]
86mod tests {
87 use crate::{Error, IdentityName};
88
89 #[test]
90 fn identity_name_validation_accepts_portable_names() {
91 for name in ["personal", "OpenAI-Work", "org.dev", "test_2", "work.json"] {
92 assert!(IdentityName::try_from(name).is_ok());
93 }
94 }
95
96 #[test]
97 fn identity_name_validation_rejects_paths_and_shellish_names() {
98 for name in ["", "-prod", "my work", "../auth", "work/main", "work\\main"] {
99 assert!(matches!(
100 IdentityName::try_from(name),
101 Err(Error::InvalidIdentityName { .. })
102 ));
103 }
104 }
105}