langcodec_cli/
validation.rs1use crate::formats::parse_custom_format;
2use std::path::Path;
3use unic_langid::LanguageIdentifier;
4
5pub 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
51pub 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
70pub 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 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
86pub 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 match lang.parse::<LanguageIdentifier>() {
94 Ok(lang_id) => {
95 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
117pub 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 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
136pub 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 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
155pub fn validate_context(context: &ValidationContext) -> Result<(), String> {
157 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 if let Some(ref output) = context.output_file {
165 validate_output_path(output).map_err(|e| format!("Output validation failed: {}", e))?;
166 }
167
168 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 if let Some(ref format) = context.input_format {
176 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 if let Some(ref format) = context.output_format {
185 validate_standard_format(format)
187 .map_err(|e| format!("Output format validation failed: {}", e))?;
188 }
189
190 Ok(())
191}
192
193pub fn validate_custom_format_file(input: &str) -> Result<(), String> {
195 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_file_path(input)?;
206 }
207 "yaml" | "yml" => {
208 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}