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