langcodec_cli/
validation.rs

1use crate::formats::parse_custom_format;
2use std::path::Path;
3use unic_langid::LanguageIdentifier;
4
5/// Validation context for different command types
6pub struct ValidationContext {
7    pub input_files: Vec<String>,
8    pub output_file: Option<String>,
9    pub language_code: Option<String>,
10    pub input_format: Option<String>,
11    pub output_format: Option<String>,
12}
13
14impl ValidationContext {
15    pub fn new() -> Self {
16        Self {
17            input_files: Vec::new(),
18            output_file: None,
19            language_code: None,
20            input_format: None,
21            output_format: None,
22        }
23    }
24
25    pub fn with_input_file(mut self, file: String) -> Self {
26        self.input_files.push(file);
27        self
28    }
29
30    pub fn with_output_file(mut self, file: String) -> Self {
31        self.output_file = Some(file);
32        self
33    }
34
35    pub fn with_language_code(mut self, lang: String) -> Self {
36        self.language_code = Some(lang);
37        self
38    }
39
40    pub fn with_input_format(mut self, format: String) -> Self {
41        self.input_format = Some(format);
42        self
43    }
44
45    pub fn with_output_format(mut self, format: String) -> Self {
46        self.output_format = Some(format);
47        self
48    }
49}
50
51/// Validate file path exists and is readable
52pub fn validate_file_path(path: &str) -> Result<(), String> {
53    let path_obj = Path::new(path);
54
55    if !path_obj.exists() {
56        return Err(format!("File does not exist: {}", path));
57    }
58
59    if !path_obj.is_file() {
60        return Err(format!("Path is not a file: {}", path));
61    }
62
63    if !path_obj.metadata().map(|m| m.is_file()).unwrap_or(false) {
64        return Err(format!("Cannot read file: {}", path));
65    }
66
67    Ok(())
68}
69
70/// Validate output directory exists or can be created
71pub fn validate_output_path(path: &str) -> Result<(), String> {
72    let path_obj = Path::new(path);
73
74    if let Some(parent) = path_obj.parent() {
75        if !parent.exists() {
76            // Try to create the directory
77            if let Err(e) = std::fs::create_dir_all(parent) {
78                return Err(format!("Cannot create output directory: {}", e));
79            }
80        }
81    }
82
83    Ok(())
84}
85
86/// Validate language code format using unic-langid (same as lib crate)
87pub fn validate_language_code(lang: &str) -> Result<(), String> {
88    if lang.is_empty() {
89        return Err("Language code cannot be empty".to_string());
90    }
91
92    // Use the same approach as the lib crate - parse with LanguageIdentifier
93    match lang.parse::<LanguageIdentifier>() {
94        Ok(lang_id) => {
95            // Additional validation: ensure the language code follows expected patterns
96            // Reject codes that are too generic or don't look like real language codes
97            let lang_str = lang_id.to_string();
98            if lang_str == "invalid"
99                || lang_str == "123"
100                || lang_str.starts_with('-')
101                || lang_str.ends_with('-')
102            {
103                return Err(format!(
104                    "Invalid language code format: {}. Expected valid BCP 47 language identifier",
105                    lang
106                ));
107            }
108            Ok(())
109        }
110        Err(_) => Err(format!(
111            "Invalid language code format: {}. Expected valid BCP 47 language identifier",
112            lang
113        )),
114    }
115}
116
117/// Validate custom format string
118pub fn validate_custom_format(format: &str) -> Result<(), String> {
119    if format.is_empty() {
120        return Err("Format cannot be empty".to_string());
121    }
122
123    // Trim whitespace and check if it's a supported custom format
124    let trimmed_format = format.trim();
125    if parse_custom_format(trimmed_format).is_err() {
126        return Err(format!(
127            "Unsupported custom format: {}. Supported formats: {}",
128            format,
129            crate::formats::get_supported_custom_formats()
130        ));
131    }
132
133    Ok(())
134}
135
136/// Validate standard format string
137pub fn validate_standard_format(format: &str) -> Result<(), String> {
138    if format.is_empty() {
139        return Err("Format cannot be empty".to_string());
140    }
141
142    // Trim whitespace and check if it's a supported standard format
143    match format.trim().to_lowercase().as_str() {
144        "android" | "androidstrings" | "xml" => Ok(()),
145        "strings" => Ok(()),
146        "xcstrings" => Ok(()),
147        "csv" => Ok(()),
148        _ => Err(format!(
149            "Unsupported standard format: {}. Supported formats: android, strings, xcstrings, csv",
150            format
151        )),
152    }
153}
154
155/// Validate a complete validation context
156pub fn validate_context(context: &ValidationContext) -> Result<(), String> {
157    // Validate input files
158    for (i, input) in context.input_files.iter().enumerate() {
159        validate_file_path(input)
160            .map_err(|e| format!("Input file {} validation failed: {}", i + 1, e))?;
161    }
162
163    // Validate output file
164    if let Some(ref output) = context.output_file {
165        validate_output_path(output).map_err(|e| format!("Output validation failed: {}", e))?;
166    }
167
168    // Validate language code
169    if let Some(ref lang) = context.language_code {
170        validate_language_code(lang)
171            .map_err(|e| format!("Language code validation failed: {}", e))?;
172    }
173
174    // Validate input format
175    if let Some(ref format) = context.input_format {
176        // Try standard format first, then custom format
177        if validate_standard_format(format).is_err() {
178            validate_custom_format(format)
179                .map_err(|e| format!("Input format validation failed: {}", e))?;
180        }
181    }
182
183    // Validate output format
184    if let Some(ref format) = context.output_format {
185        // Output formats are typically standard formats
186        validate_standard_format(format)
187            .map_err(|e| format!("Output format validation failed: {}", e))?;
188    }
189
190    Ok(())
191}
192
193/// Validate custom format file content and extension
194pub fn validate_custom_format_file(input: &str) -> Result<(), String> {
195    // Validate input file extension for custom formats
196    let input_ext = Path::new(input)
197        .extension()
198        .and_then(|ext| ext.to_str())
199        .unwrap_or("")
200        .to_lowercase();
201
202    match input_ext.as_str() {
203        "json" => {
204            // Validate JSON file exists and is readable
205            validate_file_path(input)?;
206        }
207        "yaml" | "yml" => {
208            // Validate YAML file exists and is readable
209            validate_file_path(input)?;
210        }
211        _ => {
212            return Err(format!(
213                "Unsupported file extension for custom format: {}. Expected: json, yaml, yml",
214                input_ext
215            ));
216        }
217    }
218
219    Ok(())
220}