es_fluent_sc_parser/
lib.rs

1#![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
15/// Parses a directory of Rust source code and returns a list of `FtlTypeInfo`
16/// objects.
17///
18/// # Arguments
19///
20/// * `dir_path` - The path to the directory to parse.
21///
22/// # Errors
23///
24/// This function will return an error if the directory cannot be read, or if
25/// any of the files in the directory cannot be parsed.
26pub 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        // Should have entries for:
200        // 1. Country (from EsFluent)
201        // 2. CountryDescriptionKvFtl (from EsFluentKv)
202        // 3. CountryLabelKvFtl (from EsFluentKv)
203        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}