Skip to main content

systemprompt_identifiers/
profile.rs

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