utoipauto_core/
file_utils.rs

1use std::{
2    fs::{self, File},
3    io::{self, Read},
4    iter,
5    path::{Path, PathBuf},
6};
7
8pub fn parse_file<T: Into<PathBuf>>(filepath: T) -> Result<syn::File, io::Error> {
9    let pb: PathBuf = filepath.into();
10
11    if !pb.is_file() {
12        panic!("File not found: {:?}", pb);
13    }
14
15    let mut file = File::open(&pb)?;
16    let mut content = String::new();
17    file.read_to_string(&mut content)?;
18
19    Ok(syn::parse_file(&content).unwrap_or_else(move |_| panic!("Failed to parse file {:?}", pb)))
20}
21
22/// Parse all the files in the given path
23pub fn parse_files<T: Into<PathBuf>>(path: T) -> Result<Vec<(String, syn::File)>, io::Error> {
24    let mut files: Vec<(String, syn::File)> = vec![];
25
26    let pb: PathBuf = path.into();
27    if pb.is_file() {
28        // we only parse rust files
29        if is_rust_file(&pb) {
30            files.push((pb.to_str().unwrap().to_string(), parse_file(pb)?));
31        }
32    } else {
33        for entry in fs::read_dir(pb)? {
34            let entry = entry?;
35            let path = entry.path();
36            if path.is_file() && is_rust_file(&path) {
37                files.push((path.to_str().unwrap().to_string(), parse_file(path)?));
38            } else {
39                files.append(&mut parse_files(path)?);
40            }
41        }
42    }
43    Ok(files)
44}
45
46fn is_rust_file(path: &Path) -> bool {
47    path.is_file()
48        && match path.extension() {
49            Some(ext) => match ext.to_str() {
50                Some(ext) => ext.eq("rs"),
51                None => false,
52            },
53            None => false,
54        }
55}
56
57/// Extract the module name from the file path
58/// # Example
59/// ```
60/// use utoipauto_core::file_utils::extract_module_name_from_path;
61/// let module_name = extract_module_name_from_path(
62///    &"./utoipa-auto-macro/tests/controllers/controller1.rs".to_string(),
63/// "crate"
64/// );
65/// assert_eq!(
66///  module_name,
67/// "crate::controllers::controller1".to_string()
68/// );
69/// ```
70pub fn extract_module_name_from_path(path: &str, crate_name: &str) -> String {
71    let path = path.replace('\\', "/");
72    let path = path
73        .trim_end_matches(".rs")
74        .trim_end_matches("/mod")
75        .trim_end_matches("/lib")
76        .trim_end_matches("/main")
77        .trim_start_matches("./");
78    let segments: Vec<_> = path.split('/').collect();
79
80    // In general, paths will look like `./src/my/module`, which should turn into `crate::my::module`.
81    // When using cargo workspaces, paths may look like `./subcrate/src/my/module`,
82    // `./crates/subcrate/src/my/module`, etc., so we need to remove anything up to `src`
83    // (or `tests`) to still produce `crate::my::module`.
84    // So we split the segments by the last occurrence of `src` or `tests` and take the last part.
85    let segments_inside_crate = find_segment_and_skip(&segments, &["src", "tests"], 1);
86
87    // Also skip fragments that are already out of the crate name. For example,
88    // `./src/lib/my/module/name from crate::my::module` should turn into `crate::my::module:name`,
89    // and not into `crate::lib::my::module::name`.
90    let crate_name = crate_name.replace("-", "_");
91    let mut crate_segments = crate_name.split("::");
92    let first_crate_fragment = crate_segments.next().expect("Crate should not be empty");
93    let segments_inside_crate = match crate_segments.next() {
94        Some(crate_fragment) => find_segment_and_skip(segments_inside_crate, &[crate_fragment], 0),
95        None => segments_inside_crate,
96    };
97
98    let full_crate_path: Vec<_> = iter::once(first_crate_fragment)
99        .chain(segments_inside_crate.iter().copied())
100        .collect();
101    full_crate_path.join("::")
102}
103
104fn find_segment_and_skip<'a>(segments: &'a [&str], to_find: &[&str], to_skip: usize) -> &'a [&'a str] {
105    match segments.iter().rposition(|segment| to_find.contains(segment)) {
106        Some(idx) => &segments[(idx + to_skip)..],
107        None => segments,
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_extract_module_name_from_path() {
117        assert_eq!(
118            extract_module_name_from_path("./utoipa-auto-macro/tests/controllers/controller1.rs", "crate"),
119            "crate::controllers::controller1"
120        );
121    }
122
123    #[test]
124    fn test_extract_module_name_from_path_windows() {
125        assert_eq!(
126            extract_module_name_from_path(".\\utoipa-auto-macro\\tests\\controllers\\controller1.rs", "crate"),
127            "crate::controllers::controller1"
128        );
129    }
130
131    #[test]
132    fn test_extract_module_name_from_mod() {
133        assert_eq!(
134            extract_module_name_from_path("./utoipa-auto-macro/tests/controllers/mod.rs", "crate"),
135            "crate::controllers"
136        );
137    }
138
139    #[test]
140    fn test_extract_module_name_from_lib() {
141        assert_eq!(extract_module_name_from_path("./src/lib.rs", "crate"), "crate");
142    }
143
144    #[test]
145    fn test_extract_module_name_from_main() {
146        assert_eq!(extract_module_name_from_path("./src/main.rs", "crate"), "crate");
147    }
148
149    #[test]
150    fn test_extract_module_name_from_workspace() {
151        assert_eq!(
152            extract_module_name_from_path("./server/src/routes/asset.rs", "crate"),
153            "crate::routes::asset"
154        );
155    }
156
157    #[test]
158    fn test_extract_module_name_from_workspace_nested() {
159        assert_eq!(
160            extract_module_name_from_path("./crates/server/src/routes/asset.rs", "crate"),
161            "crate::routes::asset"
162        );
163    }
164
165    #[test]
166    fn test_extract_module_name_from_folders() {
167        assert_eq!(
168            extract_module_name_from_path("./src/routing/api/audio.rs", "crate"),
169            "crate::routing::api::audio"
170        );
171    }
172
173    #[test]
174    fn test_extract_module_name_from_folders_nested() {
175        assert_eq!(
176            extract_module_name_from_path("./src/applications/src/retail_api/controllers/mod.rs", "crate"),
177            "crate::retail_api::controllers"
178        );
179    }
180
181    #[test]
182    fn test_extract_module_name_from_folders_nested_external_crate() {
183        assert_eq!(
184            extract_module_name_from_path("./src/applications/src/retail_api/controllers/mod.rs", "other_crate"),
185            "other_crate::retail_api::controllers"
186        );
187    }
188
189    #[test]
190    fn test_extract_module_name_from_workspace_with_prefix_path() {
191        assert_eq!(
192            extract_module_name_from_path("./crates/server/src/routes_lib/routes/asset.rs", "crate::routes"),
193            "crate::routes::asset"
194        );
195    }
196
197    #[test]
198    fn test_extract_module_name_from_workspace_with_external_crate_and_underscore() {
199        assert_eq!(
200            extract_module_name_from_path("./src/applications/src/retail-api/controllers/mod.rs", "other-crate"),
201            "other_crate::retail-api::controllers"
202        );
203    }
204}