1use std::collections::HashMap;
7
8use crate::config::CustomStyle;
9
10#[derive(Debug, Clone)]
12pub struct PresetStyle {
13 pub key: &'static str,
15 pub description: &'static str,
17 pub prompt: &'static str,
19}
20
21pub const PRESETS: &[PresetStyle] = &[
23 PresetStyle {
24 key: "casual",
25 description: "Casual, conversational tone",
26 prompt: "Use a casual, friendly, conversational tone.",
27 },
28 PresetStyle {
29 key: "formal",
30 description: "Formal, business-appropriate",
31 prompt: "Use a formal, polite, business-appropriate tone.",
32 },
33 PresetStyle {
34 key: "literal",
35 description: "Literal, close to source",
36 prompt: "Translate as literally as possible while remaining grammatical.",
37 },
38 PresetStyle {
39 key: "natural",
40 description: "Natural, idiomatic",
41 prompt: "Translate naturally, prioritizing idiomatic expressions over literal accuracy.",
42 },
43];
44
45#[derive(Debug, Clone)]
47pub enum ResolvedStyle {
48 Preset(&'static PresetStyle),
50 Custom { key: String, prompt: String },
52}
53
54impl ResolvedStyle {
55 pub fn prompt(&self) -> &str {
57 match self {
58 Self::Preset(preset) => preset.prompt,
59 Self::Custom { prompt, .. } => prompt,
60 }
61 }
62
63 pub fn key(&self) -> &str {
65 match self {
66 Self::Preset(preset) => preset.key,
67 Self::Custom { key, .. } => key,
68 }
69 }
70}
71
72pub fn get_preset(key: &str) -> Option<&'static PresetStyle> {
74 PRESETS.iter().find(|p| p.key == key)
75}
76
77pub fn is_preset(key: &str) -> bool {
79 get_preset(key).is_some()
80}
81
82#[allow(clippy::implicit_hasher)]
84pub fn sorted_custom_keys(styles: &HashMap<String, CustomStyle>) -> Vec<&String> {
85 let mut keys: Vec<_> = styles.keys().collect();
86 keys.sort();
87 keys
88}
89
90#[allow(clippy::implicit_hasher)]
95pub fn resolve_style(
96 key: &str,
97 custom_styles: &HashMap<String, CustomStyle>,
98) -> Result<ResolvedStyle, StyleError> {
99 if let Some(preset) = get_preset(key) {
101 return Ok(ResolvedStyle::Preset(preset));
102 }
103
104 if let Some(custom) = custom_styles.get(key) {
106 return Ok(ResolvedStyle::Custom {
107 key: key.to_string(),
108 prompt: custom.prompt.clone(),
109 });
110 }
111
112 let custom_keys: Vec<String> = sorted_custom_keys(custom_styles)
113 .into_iter()
114 .cloned()
115 .collect();
116 Err(StyleError::NotFound {
117 key: key.to_string(),
118 custom_keys,
119 })
120}
121
122#[derive(Debug, Clone)]
124pub enum StyleError {
125 NotFound {
127 key: String,
128 custom_keys: Vec<String>,
129 },
130 PresetImmutable(String),
132 AlreadyExists(String),
134 InvalidKey(String),
136}
137
138impl std::fmt::Display for StyleError {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 match self {
141 Self::NotFound { key, custom_keys } => {
142 let preset_keys: Vec<_> = PRESETS.iter().map(|p| p.key).collect();
143 let mut all_keys: Vec<&str> = preset_keys;
144 all_keys.extend(custom_keys.iter().map(String::as_str));
145 write!(
146 f,
147 "Style '{key}' not found\n\nAvailable styles: {}",
148 all_keys.join(", ")
149 )
150 }
151 Self::PresetImmutable(key) => {
152 write!(f, "Cannot modify preset style '{key}'")
153 }
154 Self::AlreadyExists(key) => {
155 write!(f, "Style '{key}' already exists")
156 }
157 Self::InvalidKey(key) => {
158 write!(
159 f,
160 "Invalid style key '{key}': must start with a letter and contain only alphanumeric characters and underscores"
161 )
162 }
163 }
164 }
165}
166
167impl std::error::Error for StyleError {}
168
169pub fn validate_custom_key(key: &str) -> Result<(), StyleError> {
174 if key.is_empty() {
176 return Err(StyleError::InvalidKey(key.to_string()));
177 }
178
179 if !key.chars().next().is_some_and(|c| c.is_ascii_alphabetic()) {
181 return Err(StyleError::InvalidKey(key.to_string()));
182 }
183
184 if !key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
186 return Err(StyleError::InvalidKey(key.to_string()));
187 }
188
189 if is_preset(key) {
191 return Err(StyleError::PresetImmutable(key.to_string()));
192 }
193
194 Ok(())
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_preset_count() {
203 assert_eq!(PRESETS.len(), 4);
204 }
205
206 #[test]
207 fn test_get_preset_exists() {
208 assert!(get_preset("casual").is_some());
209 assert!(get_preset("formal").is_some());
210 assert!(get_preset("literal").is_some());
211 assert!(get_preset("natural").is_some());
212 }
213
214 #[test]
215 fn test_get_preset_not_exists() {
216 assert!(get_preset("nonexistent").is_none());
217 }
218
219 #[test]
220 fn test_is_preset() {
221 assert!(is_preset("casual"));
222 assert!(!is_preset("my_custom"));
223 }
224
225 #[test]
226 fn test_sorted_custom_keys() {
227 let mut styles = HashMap::new();
228 styles.insert(
229 "zebra".to_string(),
230 CustomStyle {
231 description: "z desc".to_string(),
232 prompt: "z prompt".to_string(),
233 },
234 );
235 styles.insert(
236 "alpha".to_string(),
237 CustomStyle {
238 description: "a desc".to_string(),
239 prompt: "a prompt".to_string(),
240 },
241 );
242 styles.insert(
243 "beta".to_string(),
244 CustomStyle {
245 description: "b desc".to_string(),
246 prompt: "b prompt".to_string(),
247 },
248 );
249
250 let keys = sorted_custom_keys(&styles);
251 assert_eq!(keys, vec!["alpha", "beta", "zebra"]);
252 }
253
254 #[test]
255 fn test_sorted_custom_keys_empty() {
256 let styles: HashMap<String, CustomStyle> = HashMap::new();
257 let keys = sorted_custom_keys(&styles);
258 assert!(keys.is_empty());
259 }
260
261 #[test]
262 fn test_resolve_style_preset() {
263 let custom: HashMap<String, CustomStyle> = HashMap::new();
264 let resolved = resolve_style("casual", &custom);
265 assert!(resolved.is_ok());
266 assert_eq!(
267 resolved.as_ref().ok().map(ResolvedStyle::key),
268 Some("casual")
269 );
270 }
271
272 #[test]
273 fn test_resolve_style_custom() {
274 let mut custom = HashMap::new();
275 custom.insert(
276 "my_style".to_string(),
277 CustomStyle {
278 description: "My description".to_string(),
279 prompt: "My custom prompt".to_string(),
280 },
281 );
282
283 let resolved = resolve_style("my_style", &custom);
284 assert!(resolved.is_ok());
285 assert_eq!(
286 resolved.as_ref().ok().map(ResolvedStyle::key),
287 Some("my_style")
288 );
289 assert_eq!(
290 resolved.as_ref().ok().map(ResolvedStyle::prompt),
291 Some("My custom prompt")
292 );
293 }
294
295 #[test]
296 fn test_resolve_style_not_found() {
297 let custom: HashMap<String, CustomStyle> = HashMap::new();
298 let resolved = resolve_style("nonexistent", &custom);
299 assert!(resolved.is_err());
300 }
301
302 #[test]
303 fn test_validate_custom_key_valid() {
304 assert!(validate_custom_key("my_style").is_ok());
305 assert!(validate_custom_key("style123").is_ok());
306 assert!(validate_custom_key("MyStyle").is_ok());
307 }
308
309 #[test]
310 fn test_validate_custom_key_preset() {
311 let result = validate_custom_key("casual");
312 assert!(matches!(result, Err(StyleError::PresetImmutable(_))));
313 }
314
315 #[test]
316 fn test_validate_custom_key_invalid() {
317 assert!(validate_custom_key("").is_err());
318 assert!(validate_custom_key("123start").is_err());
319 assert!(validate_custom_key("has-dash").is_err());
320 assert!(validate_custom_key("has space").is_err());
321 }
322
323 #[test]
324 fn test_validate_custom_key_underscore_prefix() {
325 assert!(matches!(
327 validate_custom_key("_style"),
328 Err(StyleError::InvalidKey(_))
329 ));
330 }
331
332 #[test]
333 fn test_validate_custom_key_single_letter() {
334 assert!(validate_custom_key("a").is_ok());
336 }
337
338 #[test]
341 fn test_style_error_not_found_display_shows_presets() {
342 let error = StyleError::NotFound {
343 key: "unknown".to_string(),
344 custom_keys: vec![],
345 };
346 let msg = error.to_string();
347 assert!(msg.contains("Style 'unknown' not found"));
348 assert!(msg.contains("casual"));
349 assert!(msg.contains("formal"));
350 assert!(msg.contains("literal"));
351 assert!(msg.contains("natural"));
352 }
353
354 #[test]
355 fn test_style_error_not_found_display_includes_custom() {
356 let error = StyleError::NotFound {
357 key: "unknown".to_string(),
358 custom_keys: vec!["my_custom".to_string(), "another".to_string()],
359 };
360 let msg = error.to_string();
361 assert!(msg.contains("Available styles:"));
362 assert!(msg.contains("casual"));
363 assert!(msg.contains("my_custom"));
364 assert!(msg.contains("another"));
365 }
366
367 #[test]
368 fn test_style_error_preset_immutable_display() {
369 let error = StyleError::PresetImmutable("casual".to_string());
370 let msg = error.to_string();
371 assert!(msg.contains("Cannot modify preset style 'casual'"));
372 }
373
374 #[test]
375 fn test_style_error_already_exists_display() {
376 let error = StyleError::AlreadyExists("my_style".to_string());
377 let msg = error.to_string();
378 assert!(msg.contains("Style 'my_style' already exists"));
379 }
380
381 #[test]
382 fn test_style_error_invalid_key_display() {
383 let error = StyleError::InvalidKey("123bad".to_string());
384 let msg = error.to_string();
385 assert!(msg.contains("Invalid style key '123bad'"));
386 assert!(msg.contains("must start with a letter"));
387 }
388
389 #[test]
390 fn test_resolve_style_error_includes_custom_keys() {
391 let mut custom = HashMap::new();
392 custom.insert(
393 "my_style".to_string(),
394 CustomStyle {
395 description: "desc".to_string(),
396 prompt: "prompt".to_string(),
397 },
398 );
399
400 let result = resolve_style("nonexistent", &custom);
401 match result {
402 Err(StyleError::NotFound { custom_keys, .. }) => {
403 assert!(custom_keys.contains(&"my_style".to_string()));
404 }
405 _ => panic!("Expected StyleError::NotFound"),
406 }
407 }
408}