mod_rs_migrator/
lib.rs

1use std::ffi::OsStr;
2use std::path::{Path, PathBuf};
3
4use clap::Parser;
5
6#[derive(Debug, Default, Clone, Parser)]
7#[clap(rename_all = "kebab-case")]
8pub struct Config {
9    /// If set will follow symlinks as if they were directories - I don't recommend using this
10    #[clap(short, long)]
11    follow_symlinks: bool,
12
13    /// If set will not delete directories that would end up empty after a run
14    #[clap(short, long)]
15    leave_empty_dirs: bool,
16
17    /// If set will not apply special treatment to directories named `tests`
18    ///
19    /// By default, if a `mod.rs` file would be moved to a directory named `tests` it will be preserved as a mod.rs file
20    #[clap(short, long)]
21    no_special_treatment_for_tests_dir: bool,
22}
23
24pub async fn find_mod_named_modules(
25    path: impl AsRef<Path>,
26    config: &Config,
27) -> anyhow::Result<Vec<PathBuf>> {
28    let path = path.as_ref();
29
30    let mut dirs_to_process = vec![path.to_owned()];
31    let mut results = vec![];
32
33    while let Some(dir) = dirs_to_process.pop() {
34        let mut read_dir = tokio::fs::read_dir(dir).await?;
35
36        while let Some(entry) = read_dir.next_entry().await? {
37            let file_type = entry.file_type().await?;
38
39            if file_type.is_dir() {
40                dirs_to_process.push(entry.path());
41            }
42
43            if config.follow_symlinks && file_type.is_symlink() {
44                dirs_to_process.push(entry.path());
45            }
46
47            if file_type.is_file() {
48                let p = entry.path();
49
50                if p.file_name() == Some(OsStr::new("mod.rs")) {
51                    results.push(p);
52                }
53            }
54        }
55    }
56
57    Ok(results)
58}
59
60pub async fn move_mod_rs_outside_of_dir(
61    mod_files: Vec<PathBuf>,
62    config: &Config,
63) -> anyhow::Result<()> {
64    for mod_file in mod_files {
65        let parent_dir = mod_file
66            .parent()
67            .ok_or_else(|| anyhow::anyhow!("Missing parent"))?;
68
69        if !config.no_special_treatment_for_tests_dir {
70            let parent_of_parent = parent_dir
71                .parent()
72                .ok_or_else(|| anyhow::anyhow!("Missing parent"))?;
73
74            if parent_of_parent.file_name() == Some(OsStr::new("tests")) {
75                continue;
76            }
77        }
78
79        let new_path = parent_dir.with_extension("rs");
80
81        move_file(&mod_file, new_path).await?;
82
83        if !config.leave_empty_dirs && is_dir_empty(parent_dir).await? {
84            tokio::fs::remove_dir(parent_dir).await?;
85        }
86    }
87
88    Ok(())
89}
90
91async fn is_dir_empty(dir: impl AsRef<Path>) -> anyhow::Result<bool> {
92    let mut read_dir = tokio::fs::read_dir(dir).await?;
93
94    Ok(read_dir.next_entry().await?.is_none())
95}
96
97async fn move_file(
98    source_path: impl AsRef<Path>,
99    target_path: impl AsRef<Path>,
100) -> anyhow::Result<()> {
101    let source_path = source_path.as_ref();
102
103    tokio::fs::copy(source_path, target_path).await?;
104
105    tokio::fs::remove_file(source_path).await?;
106
107    Ok(())
108}