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        "csv" => Ok(()),
154        _ => Err(format!(
155            "Unsupported standard format: {}. Supported formats: android, strings, xcstrings, csv",
156            format
157        )),
158    }
159}
160
161/// Validate a complete validation context
162pub fn validate_context(context: &ValidationContext) -> Result<(), String> {
163    // Validate input files
164    for (i, input) in context.input_files.iter().enumerate() {
165        validate_file_path(input)
166            .map_err(|e| format!("Input file {} validation failed: {}", i + 1, e))?;
167    }
168
169    // Validate output file
170    if let Some(ref output) = context.output_file {
171        validate_output_path(output).map_err(|e| format!("Output validation failed: {}", e))?;
172    }
173
174    // Validate language code
175    if let Some(ref lang) = context.language_code {
176        validate_language_code(lang)
177            .map_err(|e| format!("Language code validation failed: {}", e))?;
178    }
179
180    // Validate input format
181    if let Some(ref format) = context.input_format {
182        // Try standard format first, then custom format
183        if validate_standard_format(format).is_err() {
184            validate_custom_format(format)
185                .map_err(|e| format!("Input format validation failed: {}", e))?;
186        }
187    }
188
189    // Validate output format
190    if let Some(ref format) = context.output_format {
191        // Output formats are typically standard formats
192        validate_standard_format(format)
193            .map_err(|e| format!("Output format validation failed: {}", e))?;
194    }
195
196    Ok(())
197}
198
199/// Validate custom format file content and extension
200pub fn validate_custom_format_file(input: &str) -> Result<(), String> {
201    // Validate input file extension for custom formats
202    let input_ext = Path::new(input)
203        .extension()
204        .and_then(|ext| ext.to_str())
205        .unwrap_or("")
206        .to_lowercase();
207
208    match input_ext.as_str() {
209        "json" => {
210            // Validate JSON file exists and is readable
211            validate_file_path(input)?;
212        }
213        "yaml" | "yml" => {
214            // Validate YAML file exists and is readable
215            validate_file_path(input)?;
216        }
217        "langcodec" => {
218            // Validate langcodec file exists and is readable
219            validate_file_path(input)?;
220        }
221        _ => {
222            return Err(format!(
223                "Unsupported file extension for custom format: {}. Expected: json, yaml, yml, langcodec",
224                input_ext
225            ));
226        }
227    }
228
229    Ok(())
230}