es_fluent_sc_parser/
lib.rs1#![doc = include_str!("../README.md")]
2
3use crate::visitor::FtlVisitor;
4use std::fs;
5use std::path::{Path, PathBuf};
6use walkdir::WalkDir;
7
8pub mod error;
9mod processor;
10mod visitor;
11
12use error::FluentScParserError;
13use es_fluent_core::registry::FtlTypeInfo;
14
15pub fn parse_directory(dir_path: &Path) -> Result<Vec<FtlTypeInfo>, FluentScParserError> {
27 log::info!(
28 "Starting FTL type info parsing in directory: {}",
29 dir_path.display()
30 );
31
32 let rust_files: Vec<PathBuf> = WalkDir::new(dir_path)
33 .into_iter()
34 .filter_map(|entry_result| match entry_result {
35 Ok(entry) => {
36 let path = entry.path();
37 if path.is_file()
38 && let Some(ext) = path.extension()
39 && ext == "rs"
40 {
41 Some(Ok(path.to_path_buf()))
42 } else {
43 None
44 }
45 },
46 Err(e) => Some(Err(FluentScParserError::WalkDir(dir_path.to_path_buf(), e))),
47 })
48 .collect::<Result<Vec<_>, _>>()?;
49
50 log::debug!("Found {} Rust files to parse.", rust_files.len());
51
52 let file_results: Result<Vec<Vec<FtlTypeInfo>>, FluentScParserError> = rust_files
53 .iter()
54 .map(|file_path| {
55 log::trace!("Parsing file: {}", file_path.display());
56 let content = fs::read_to_string(file_path)
57 .map_err(|e| FluentScParserError::Io(file_path.clone(), e))?;
58 let syntax_tree = syn::parse_file(&content)
59 .map_err(|e| FluentScParserError::Syn(file_path.clone(), e))?;
60
61 let mut visitor = FtlVisitor::new(file_path);
62 syn::visit::visit_file(&mut visitor, &syntax_tree);
63 Ok(visitor.type_infos().to_owned())
64 })
65 .collect();
66
67 let results: Vec<FtlTypeInfo> = file_results?
68 .into_iter()
69 .filter(|type_infos| !type_infos.is_empty())
70 .flatten()
71 .collect::<std::collections::HashSet<_>>()
72 .into_iter()
73 .collect();
74
75 log::info!(
76 "Finished parsing. Found {} FTL type info entries.",
77 results.len()
78 );
79 Ok(results)
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use std::fs;
86 use tempfile::TempDir;
87
88 #[test]
89 fn test_parse_directory_empty() {
90 let temp_dir = TempDir::new().unwrap();
91 let result = parse_directory(temp_dir.path());
92 assert!(result.is_ok());
93 assert_eq!(result.unwrap().len(), 0);
94 }
95
96 #[test]
97 fn test_parse_directory_with_nonexistent_path() {
98 let non_existent_path = Path::new("/non/existent/path");
99 let result = parse_directory(non_existent_path);
100 assert!(result.is_err());
101 }
102
103 #[test]
104 fn test_parse_directory_with_rust_file() {
105 let temp_dir = TempDir::new().unwrap();
106 let rust_file_path = temp_dir.path().join("test.rs");
107
108 let rust_content = r#"
109use es_fluent_core::EsFluent;
110
111#[derive(EsFluent)]
112pub enum TestEnum {
113 Variant1,
114 Variant2,
115}
116"#;
117
118 fs::write(&rust_file_path, rust_content).unwrap();
119
120 let result = parse_directory(temp_dir.path());
121 assert!(result.is_ok());
122
123 let type_infos = result.unwrap();
124 assert!(!type_infos.is_empty());
125 }
126
127 #[test]
128 fn test_parse_directory_with_multiple_rust_files() {
129 let temp_dir = TempDir::new().unwrap();
130
131 let rust_file1_path = temp_dir.path().join("test1.rs");
132 let rust_content1 = r#"
133use es_fluent_core::EsFluent;
134
135#[derive(EsFluent)]
136pub enum TestEnum1 {
137 VariantA,
138}
139"#;
140
141 fs::write(&rust_file1_path, rust_content1).unwrap();
142
143 let rust_file2_path = temp_dir.path().join("test2.rs");
144 let rust_content2 = r#"
145use es_fluent_core::EsFluent;
146
147#[derive(EsFluent)]
148pub enum TestEnum2 {
149 VariantB,
150}
151"#;
152 fs::write(&rust_file2_path, rust_content2).unwrap();
153
154 let result = parse_directory(temp_dir.path());
155 assert!(result.is_ok());
156
157 let type_infos = result.unwrap();
158 assert!(type_infos.len() >= 2);
159 }
160
161 #[test]
162 fn test_parse_directory_with_non_rust_file() {
163 let temp_dir = TempDir::new().unwrap();
164 let non_rust_file_path = temp_dir.path().join("test.txt");
165 fs::write(&non_rust_file_path, "not a rust file").unwrap();
166
167 let result = parse_directory(temp_dir.path());
168 assert!(result.is_ok());
169 assert_eq!(result.unwrap().len(), 0);
170 }
171
172 #[test]
173 fn test_parse_enum_with_both_es_fluent_and_es_fluent_kv() {
174 let temp_dir = TempDir::new().unwrap();
175 let rust_file_path = temp_dir.path().join("test.rs");
176
177 let rust_content = r#"
178use es_fluent::{EsFluent, EsFluentKv};
179
180#[derive(EsFluent, EsFluentKv)]
181#[fluent(this)]
182#[fluent_kv(keys = ["description", "label"])]
183pub enum Country {
184 USA(USAState),
185 Canada(CanadaProvince),
186}
187
188pub struct USAState;
189pub struct CanadaProvince;
190"#;
191
192 fs::write(&rust_file_path, rust_content).unwrap();
193
194 let result = parse_directory(temp_dir.path());
195 assert!(result.is_ok());
196
197 let type_infos = result.unwrap();
198
199 let type_names: Vec<_> = type_infos.iter().map(|t| t.type_name.clone()).collect();
204 println!("Found type_infos: {:?}", type_names);
205
206 assert!(
207 type_names.contains(&"Country".to_string()),
208 "Should have Country from EsFluent"
209 );
210 assert!(
211 type_names.contains(&"CountryDescriptionKvFtl".to_string()),
212 "Should have CountryDescriptionKvFtl from EsFluentKv"
213 );
214 assert!(
215 type_names.contains(&"CountryLabelKvFtl".to_string()),
216 "Should have CountryLabelKvFtl from EsFluentKv"
217 );
218 }
219}