1use crate::{CoreError, CoreResult};
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
4pub struct LanguageTag(String);
5
6impl LanguageTag {
7 pub fn new(value: impl Into<String>) -> CoreResult<Self> {
8 let value = normalize_identifier(value.into())?;
9 Ok(Self(value))
10 }
11
12 pub fn as_str(&self) -> &str {
13 &self.0
14 }
15}
16
17impl core::fmt::Display for LanguageTag {
18 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
19 f.write_str(self.as_str())
20 }
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
24pub struct StrategyId(String);
25
26impl StrategyId {
27 pub fn new(value: impl Into<String>) -> CoreResult<Self> {
28 let value = normalize_identifier(value.into())?;
29 Ok(Self(value))
30 }
31
32 pub fn as_str(&self) -> &str {
33 &self.0
34 }
35}
36
37impl core::fmt::Display for StrategyId {
38 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
39 f.write_str(self.as_str())
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
44pub struct ProviderId(String);
45
46impl ProviderId {
47 pub fn new(value: impl Into<String>) -> CoreResult<Self> {
48 let value = normalize_identifier(value.into())?;
49 Ok(Self(value))
50 }
51
52 pub fn as_str(&self) -> &str {
53 &self.0
54 }
55}
56
57impl core::fmt::Display for ProviderId {
58 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
59 f.write_str(self.as_str())
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
64pub struct ModelId(String);
65
66impl ModelId {
67 pub fn new(value: impl Into<String>) -> CoreResult<Self> {
68 let value = normalize_identifier(value.into())?;
69 Ok(Self(value))
70 }
71
72 pub fn as_str(&self) -> &str {
73 &self.0
74 }
75}
76
77impl core::fmt::Display for ModelId {
78 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
79 f.write_str(self.as_str())
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
84pub struct StyleProfileId(String);
85
86impl StyleProfileId {
87 pub fn new(value: impl Into<String>) -> CoreResult<Self> {
88 let value = normalize_identifier(value.into())?;
89 Ok(Self(value))
90 }
91
92 pub fn as_str(&self) -> &str {
93 &self.0
94 }
95}
96
97impl core::fmt::Display for StyleProfileId {
98 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
99 f.write_str(self.as_str())
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
104pub struct TemplateId(String);
105
106impl TemplateId {
107 pub fn new(value: impl Into<String>) -> CoreResult<Self> {
108 let value = normalize_identifier(value.into())?;
109 Ok(Self(value))
110 }
111
112 pub fn as_str(&self) -> &str {
113 &self.0
114 }
115}
116
117impl core::fmt::Display for TemplateId {
118 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
119 f.write_str(self.as_str())
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
124pub struct SlotId(String);
125
126impl SlotId {
127 pub fn new(value: impl Into<String>) -> CoreResult<Self> {
128 let value = normalize_identifier(value.into())?;
129 Ok(Self(value))
130 }
131
132 pub fn as_str(&self) -> &str {
133 &self.0
134 }
135}
136
137impl core::fmt::Display for SlotId {
138 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
139 f.write_str(self.as_str())
140 }
141}
142
143fn normalize_identifier(value: String) -> CoreResult<String> {
144 let normalized = value.trim().to_ascii_lowercase();
145 let is_valid = !normalized.is_empty()
146 && normalized
147 .bytes()
148 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-');
149
150 if is_valid {
151 Ok(normalized)
152 } else {
153 Err(CoreError::InvalidIdentifier(value))
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::{LanguageTag, ModelId, ProviderId, SlotId, StrategyId, StyleProfileId, TemplateId};
160
161 #[test]
162 fn language_tag_normalizes_ascii_input() {
163 let tag = LanguageTag::new(" FA ").expect("tag should normalize");
164 assert_eq!(tag.as_str(), "fa");
165 }
166
167 #[test]
168 fn strategy_id_rejects_invalid_characters() {
169 let strategy = StrategyId::new("synonym_v1");
170 assert!(strategy.is_err());
171 }
172
173 #[test]
174 fn provider_id_normalizes_ascii_input() {
175 let provider = ProviderId::new(" OpenAI ").expect("provider should normalize");
176 assert_eq!(provider.as_str(), "openai");
177 }
178
179 #[test]
180 fn model_id_accepts_dash_and_digits() {
181 let model = ModelId::new("gpt-4o-mini").expect("model id should be valid");
182 assert_eq!(model.as_str(), "gpt-4o-mini");
183 }
184
185 #[test]
186 fn style_profile_id_normalizes_ascii_input() {
187 let profile = StyleProfileId::new(" FA-Formal ").expect("profile id should normalize");
188 assert_eq!(profile.as_str(), "fa-formal");
189 }
190
191 #[test]
192 fn template_id_normalizes_ascii_input() {
193 let template = TemplateId::new(" FA-Template-01 ").expect("template id should normalize");
194 assert_eq!(template.as_str(), "fa-template-01");
195 }
196
197 #[test]
198 fn slot_id_normalizes_ascii_input() {
199 let slot = SlotId::new(" Subject-Main ").expect("slot id should normalize");
200 assert_eq!(slot.as_str(), "subject-main");
201 }
202}