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)]
112#[fluent(display = "fluent")]
113pub enum TestEnum {
114    Variant1,
115    Variant2,
116}
117"#;
118
119        fs::write(&rust_file_path, rust_content).unwrap();
120
121        let result = parse_directory(temp_dir.path());
122        assert!(result.is_ok());
123
124        let type_infos = result.unwrap();
125        assert!(!type_infos.is_empty());
126    }
127
128    #[test]
129    fn test_parse_directory_with_multiple_rust_files() {
130        let temp_dir = TempDir::new().unwrap();
131
132        let rust_file1_path = temp_dir.path().join("test1.rs");
133        let rust_content1 = r#"
134use es_fluent_core::EsFluent;
135
136#[derive(EsFluent)]
137#[fluent(display = "fluent")]
138pub enum TestEnum1 {
139    VariantA,
140}
141"#;
142
143        fs::write(&rust_file1_path, rust_content1).unwrap();
144
145        let rust_file2_path = temp_dir.path().join("test2.rs");
146        let rust_content2 = r#"
147use es_fluent_core::EsFluent;
148
149#[derive(EsFluent)]
150#[fluent(display = "fluent")]
151pub enum TestEnum2 {
152    VariantB,
153}
154"#;
155        fs::write(&rust_file2_path, rust_content2).unwrap();
156
157        let result = parse_directory(temp_dir.path());
158        assert!(result.is_ok());
159
160        let type_infos = result.unwrap();
161        assert!(type_infos.len() >= 2);
162    }
163
164    #[test]
165    fn test_parse_directory_with_non_rust_file() {
166        let temp_dir = TempDir::new().unwrap();
167        let non_rust_file_path = temp_dir.path().join("test.txt");
168        fs::write(&non_rust_file_path, "not a rust file").unwrap();
169
170        let result = parse_directory(temp_dir.path());
171        assert!(result.is_ok());
172        assert_eq!(result.unwrap().len(), 0);
173    }
174}