Skip to main content

systemprompt_identifiers/
profile.rs

1//! Profile name identifier type with validation.
2
3use crate::error::IdValidationError;
4use crate::{DbValue, ToDbValue};
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
9#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
10#[cfg_attr(feature = "sqlx", sqlx(transparent))]
11#[serde(transparent)]
12pub struct ProfileName(String);
13
14impl ProfileName {
15    pub fn try_new(value: impl Into<String>) -> Result<Self, IdValidationError> {
16        let value = value.into();
17        if value.is_empty() {
18            return Err(IdValidationError::empty("ProfileName"));
19        }
20        if value.contains('/') {
21            return Err(IdValidationError::invalid(
22                "ProfileName",
23                "cannot contain path separator '/'",
24            ));
25        }
26        if !value
27            .chars()
28            .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
29        {
30            return Err(IdValidationError::invalid(
31                "ProfileName",
32                "can only contain alphanumeric characters, hyphens, and underscores",
33            ));
34        }
35        Ok(Self(value))
36    }
37
38    #[must_use]
39    #[allow(clippy::expect_used)]
40    pub fn new(value: impl Into<String>) -> Self {
41        // SAFETY: `new` is the infallible constructor reserved for inputs the caller
42        // has already validated (compile-time literals, values that
43        // round-tripped through `try_new` at a boundary). Untrusted input must
44        // go through `try_new`.
45        Self::try_new(value).expect("ProfileName validation failed")
46    }
47
48    #[must_use]
49    pub fn as_str(&self) -> &str {
50        &self.0
51    }
52
53    #[must_use]
54    pub fn default_profile() -> Self {
55        Self("default".to_string())
56    }
57}
58
59impl fmt::Display for ProfileName {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(f, "{}", self.0)
62    }
63}
64
65impl TryFrom<String> for ProfileName {
66    type Error = IdValidationError;
67
68    fn try_from(s: String) -> Result<Self, Self::Error> {
69        Self::try_new(s)
70    }
71}
72
73impl TryFrom<&str> for ProfileName {
74    type Error = IdValidationError;
75
76    fn try_from(s: &str) -> Result<Self, Self::Error> {
77        Self::try_new(s)
78    }
79}
80
81impl std::str::FromStr for ProfileName {
82    type Err = IdValidationError;
83
84    fn from_str(s: &str) -> Result<Self, Self::Err> {
85        Self::try_new(s)
86    }
87}
88
89impl AsRef<str> for ProfileName {
90    fn as_ref(&self) -> &str {
91        &self.0
92    }
93}
94
95impl<'de> Deserialize<'de> for ProfileName {
96    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
97    where
98        D: serde::Deserializer<'de>,
99    {
100        let s = String::deserialize(deserializer)?;
101        Self::try_new(s).map_err(serde::de::Error::custom)
102    }
103}
104
105impl ToDbValue for ProfileName {
106    fn to_db_value(&self) -> DbValue {
107        DbValue::String(self.0.clone())
108    }
109}
110
111impl ToDbValue for &ProfileName {
112    fn to_db_value(&self) -> DbValue {
113        DbValue::String(self.0.clone())
114    }
115}