Skip to main content

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