cs/parse/
key_extractor.rs1use crate::error::Result;
4use std::path::Path;
5use walkdir::WalkDir;
6
7use super::json_parser::JsonParser;
8use super::translation::TranslationEntry;
9use super::yaml_parser::YamlParser;
10
11pub struct KeyExtractor;
15
16impl Default for KeyExtractor {
17 fn default() -> Self {
18 Self::new()
19 }
20}
21
22impl KeyExtractor {
23 pub fn new() -> Self {
25 Self
26 }
27
28 pub fn extract(&self, base_dir: &Path, query: &str) -> Result<Vec<TranslationEntry>> {
33 let mut matches = Vec::new();
34 let lowered = query.to_lowercase();
35
36 for entry in WalkDir::new(base_dir)
37 .into_iter()
38 .filter_map(|e| e.ok())
39 .filter(|e| e.file_type().is_file())
40 {
41 let path = entry.path();
42 if let Some(ext) = path.extension() {
43 let ext_str = ext.to_string_lossy();
44 if ext_str == "yml" || ext_str == "yaml" {
45 let entries = YamlParser::parse_file(path)?;
46 for e in entries {
47 if e.value.to_lowercase().contains(&lowered) {
48 matches.push(e);
49 }
50 }
51 } else if ext_str == "json" {
52 let entries = JsonParser::parse_file(path)?;
53 for e in entries {
54 if e.value.to_lowercase().contains(&lowered) {
55 matches.push(e);
56 }
57 }
58 }
59 }
60 }
61 Ok(matches)
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use std::fs;
69
70 use tempfile::tempdir;
71
72 #[test]
73 fn test_key_extractor_simple() -> Result<()> {
74 let dir = tempdir()?;
75 let en_path = dir.path().join("en.yml");
76 let fr_path = dir.path().join("fr.yml");
77
78 fs::write(
80 &en_path,
81 "greeting:\n hello: \"Hello World\"\n goodbye: \"Goodbye\"",
82 )?;
83 fs::write(
84 &fr_path,
85 "greeting:\n hello: \"Bonjour World\"\n goodbye: \"Au revoir\"",
86 )?;
87
88 let extractor = KeyExtractor::new();
89 let results = extractor.extract(dir.path(), "world")?;
90
91 assert_eq!(results.len(), 2);
93 let keys: Vec<_> = results.iter().map(|e| e.key.clone()).collect();
94 assert!(keys.contains(&"greeting.hello".to_string()));
95 Ok(())
96 }
97
98 #[test]
99 fn test_key_extractor_case_insensitive() -> Result<()> {
100 let dir = tempdir()?;
101 let yaml_path = dir.path().join("test.yml");
102
103 fs::write(
104 &yaml_path,
105 "app:\n title: \"My Application\"\n description: \"A great APP for everyone\"",
106 )?;
107
108 let extractor = KeyExtractor::new();
109
110 let results = extractor.extract(dir.path(), "APP")?;
112 assert_eq!(results.len(), 2); let values: Vec<_> = results.iter().map(|e| e.value.clone()).collect();
115 assert!(values.contains(&"My Application".to_string()));
116 assert!(values.contains(&"A great APP for everyone".to_string()));
117
118 Ok(())
119 }
120
121 #[test]
122 fn test_key_extractor_multiple_files() -> Result<()> {
123 let dir = tempdir()?;
124
125 let en_path = dir.path().join("en.yml");
127 let fr_path = dir.path().join("fr.yml");
128 let de_path = dir.path().join("de.yml");
129
130 fs::write(&en_path, "common:\n action: \"Save Data\"")?;
131 fs::write(&fr_path, "common:\n action: \"Sauvegarder Data\"")?;
132 fs::write(&de_path, "common:\n action: \"Speichern Data\"")?;
133
134 let extractor = KeyExtractor::new();
135 let results = extractor.extract(dir.path(), "data")?;
136
137 assert_eq!(results.len(), 3);
139
140 let files: Vec<_> = results
141 .iter()
142 .map(|e| e.file.file_name().unwrap().to_string_lossy().to_string())
143 .collect();
144 assert!(files.contains(&"en.yml".to_string()));
145 assert!(files.contains(&"fr.yml".to_string()));
146 assert!(files.contains(&"de.yml".to_string()));
147
148 Ok(())
149 }
150
151 #[test]
152 fn test_key_extractor_deep_nested() -> Result<()> {
153 let dir = tempdir()?;
154 let yaml_path = dir.path().join("nested.yml");
155
156 fs::write(
157 &yaml_path,
158 "level1:\n level2:\n level3:\n deep_key: \"Deep nested value\"\n another: \"test value\"",
159 )?;
160
161 let extractor = KeyExtractor::new();
162 let results = extractor.extract(dir.path(), "deep")?;
163
164 assert_eq!(results.len(), 1);
165 assert_eq!(results[0].key, "level1.level2.level3.deep_key");
166 assert_eq!(results[0].value, "Deep nested value");
167
168 Ok(())
169 }
170
171 #[test]
172 fn test_key_extractor_no_matches() -> Result<()> {
173 let dir = tempdir()?;
174 let yaml_path = dir.path().join("test.yml");
175
176 fs::write(
177 &yaml_path,
178 "greeting:\n hello: \"Hello\"\n goodbye: \"Goodbye\"",
179 )?;
180
181 let extractor = KeyExtractor::new();
182 let results = extractor.extract(dir.path(), "nonexistent")?;
183
184 assert_eq!(results.len(), 0);
185
186 Ok(())
187 }
188
189 #[test]
190 fn test_key_extractor_supports_json_and_yaml() -> Result<()> {
191 let dir = tempdir()?;
192 let yaml_path = dir.path().join("test.yml");
193 let txt_path = dir.path().join("test.txt");
194 let json_path = dir.path().join("test.json");
195
196 fs::write(&yaml_path, "key: \"test value\"")?;
197 fs::write(&txt_path, "key: test value")?; fs::write(&json_path, "{\"key\": \"test value\"}")?; let extractor = KeyExtractor::new();
201 let results = extractor.extract(dir.path(), "test")?;
202
203 assert_eq!(results.len(), 2);
205 let extensions: Vec<_> = results
206 .iter()
207 .map(|e| e.file.extension().unwrap().to_string_lossy().to_string())
208 .collect();
209 assert!(extensions.contains(&"yml".to_string()));
210 assert!(extensions.contains(&"json".to_string()));
211
212 Ok(())
213 }
214}