Skip to main content

linguasteg_core/
ids.rs

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}