systemprompt_identifiers/
locale.rs1use crate::error::IdValidationError;
11use crate::{DbValue, ToDbValue};
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use std::fmt;
15
16const MAX_LEN: usize = 35;
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, JsonSchema)]
19#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
20#[cfg_attr(feature = "sqlx", sqlx(transparent))]
21#[serde(transparent)]
22pub struct LocaleCode(String);
23
24impl LocaleCode {
25 pub fn try_new(value: impl Into<String>) -> Result<Self, IdValidationError> {
26 let value = value.into();
27 if value.is_empty() {
28 return Err(IdValidationError::empty("LocaleCode"));
29 }
30 if value.len() > MAX_LEN {
31 return Err(IdValidationError::invalid(
32 "LocaleCode",
33 "exceeds 35 characters",
34 ));
35 }
36 let mut subtags = value.split('-');
37 let primary = subtags.next().unwrap_or("");
38 let plen = primary.len();
39 if !(2..=3).contains(&plen) || !primary.chars().all(|c| c.is_ascii_lowercase()) {
40 return Err(IdValidationError::invalid(
41 "LocaleCode",
42 "primary subtag must be 2-3 lowercase ASCII letters",
43 ));
44 }
45 for sub in subtags {
46 let len = sub.len();
47 if !(2..=8).contains(&len) || !sub.chars().all(|c| c.is_ascii_alphanumeric()) {
48 return Err(IdValidationError::invalid(
49 "LocaleCode",
50 "subtag must be 2-8 alphanumeric ASCII characters",
51 ));
52 }
53 }
54 Ok(Self(value))
55 }
56
57 #[must_use]
58 #[expect(
59 clippy::expect_used,
60 reason = "infallible constructor reserved for already-validated inputs; untrusted input \
61 must go through try_new"
62 )]
63 pub fn new(value: impl Into<String>) -> Self {
64 Self::try_new(value).expect("LocaleCode validation failed")
69 }
70
71 #[must_use]
72 pub fn as_str(&self) -> &str {
73 &self.0
74 }
75}
76
77impl fmt::Display for LocaleCode {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 write!(f, "{}", self.0)
80 }
81}
82
83impl TryFrom<String> for LocaleCode {
84 type Error = IdValidationError;
85
86 fn try_from(s: String) -> Result<Self, Self::Error> {
87 Self::try_new(s)
88 }
89}
90
91impl TryFrom<&str> for LocaleCode {
92 type Error = IdValidationError;
93
94 fn try_from(s: &str) -> Result<Self, Self::Error> {
95 Self::try_new(s)
96 }
97}
98
99impl std::str::FromStr for LocaleCode {
100 type Err = IdValidationError;
101
102 fn from_str(s: &str) -> Result<Self, Self::Err> {
103 Self::try_new(s)
104 }
105}
106
107impl AsRef<str> for LocaleCode {
108 fn as_ref(&self) -> &str {
109 &self.0
110 }
111}
112
113impl<'de> Deserialize<'de> for LocaleCode {
114 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
115 where
116 D: serde::Deserializer<'de>,
117 {
118 let s = String::deserialize(deserializer)?;
119 Self::try_new(s).map_err(serde::de::Error::custom)
120 }
121}
122
123impl ToDbValue for LocaleCode {
124 fn to_db_value(&self) -> DbValue {
125 DbValue::String(self.0.clone())
126 }
127}
128
129impl ToDbValue for &LocaleCode {
130 fn to_db_value(&self) -> DbValue {
131 DbValue::String(self.0.clone())
132 }
133}