cmake-parser 0.1.0-beta.1

The library to parse cmake language.
Documentation
use std::{path::Path, process::ExitCode};

fn main() -> ExitCode {
    let Some(gen) = args() else {
        eprintln!("Usage: gen <command_type> <command> [comment] [command_rust_type]");
        return ExitCode::FAILURE;
    };

    match gen.run() {
        Ok(_) => ExitCode::SUCCESS,
        Err(err) => {
            eprintln!("{err}");
            ExitCode::FAILURE
        }
    }
}

struct Gen {
    command_type: String,
    command: String,
    comment: Option<String>,
    command_name: Option<String>,
}

impl Gen {
    fn run(&self) -> Result<(), std::io::Error> {
        let Self {
            command_type,
            command,
            comment,
            command_name,
        } = self;

        let command_safe = {
            use check_keyword::CheckKeyword as _;
            command.as_str().into_safe()
        };

        {
            let fixture_path = Path::new("fixture")
                .join("commands")
                .join(command_type)
                .join(command);

            if fixture_path.exists() {
                eprintln!(
                    "Skipping existing fixture {}... ok",
                    fixture_path.to_string_lossy()
                );
            } else {
                self.write_file(&fixture_path, format!("{command}(name)\n"))?;
            }
        }

        let (command_name, command_type_name) = {
            use inflections::Inflect as _;
            (
                command_name.clone().unwrap_or_else(|| {
                    command
                        .to_pascal_case()
                        .replace("Ctest", "CTest")
                        .replace("Cmake", "CMake")
                }),
                if command_type != "ctest" {
                    command_type.to_pascal_case()
                } else {
                    "CTest".into()
                },
            )
        };

        {
            let command_mod_rs_path = Path::new("lib")
                .join("src")
                .join("doc")
                .join("command")
                .join("mod.rs");
            let content = std::fs::read_to_string(&command_mod_rs_path)?;

            let mut lines = content.lines().map(str::to_string).collect::<Vec<_>>();

            let declaration =
                format!("    {command_name}(Box<{command_type}::{command_name}<'t>>),");

            if let Some(declaration_pos) = lines.iter().position(|l| l == &declaration) {
                if let Some(new_comment) = self.comment.as_deref() {
                    eprint!("Command declaration is found, updating comment...");
                    if declaration_pos != 0 {
                        if let Some(comment) = lines
                            .get_mut(declaration_pos - 1)
                            .filter(|line| line.trim_start().starts_with("///"))
                        {
                            *comment = format!("    /// {new_comment}")
                        }
                    }
                    eprintln!(" ok");
                } else {
                    eprintln!("Command declaration is found, skipping... ok");
                }
            } else if let Some(closing_bracket_pos) = lines.iter().position(|l| l == "}") {
                eprint!("Command declaration is not found, inserting...");
                lines.insert(closing_bracket_pos, declaration);
                let comment = format!("    /// {}", comment.as_deref().unwrap_or_default());
                lines.insert(closing_bracket_pos, comment);
                eprintln!(" ok");
            } else {
                eprintln!(
                    "Could not update {}!",
                    command_mod_rs_path.to_string_lossy()
                );
                return Err(std::io::Error::new(
                    std::io::ErrorKind::Other,
                    "failed to update Command struct",
                ));
            }
            self.write_if_changed(lines, content, &command_mod_rs_path)?;
        }

        {
            let command_type_mod_rs_path = Path::new("lib")
                .join("src")
                .join("doc")
                .join("command")
                .join(command_type)
                .join("mod.rs");
            let content = std::fs::read_to_string(&command_type_mod_rs_path)?;

            let mut lines = content.lines().map(str::to_string).collect::<Vec<_>>();

            let declaration_mod = format!("pub mod {command_safe};");

            if lines.contains(&declaration_mod) {
                eprintln!("Module declaration is found, skipping... ok");
            } else {
                eprint!("Module declaration not found, adding...");
                let empty_line_pos = lines.iter().position(|s| s.is_empty()).unwrap_or_default();
                lines.insert(empty_line_pos, declaration_mod);
                eprintln!(" ok");
            }

            let declaration_pub_use = format!("pub use {command_safe}::{command_name};");

            if lines.contains(&declaration_pub_use) {
                eprintln!("Public use declaration is found, skipping... ok");
            } else {
                eprint!("Public use declaration not found, adding...");
                lines.push(declaration_pub_use);
                eprintln!(" ok");
            }

            self.write_if_changed(lines, content, &command_type_mod_rs_path)?;
        }

        {
            let command_rs_path = Path::new("lib")
                .join("src")
                .join("doc")
                .join("command")
                .join(command_type)
                .join(format!("{command}.rs"));
            if command_rs_path.exists() {
                eprintln!(
                    "Skipping existing command mod {}... ok",
                    command_rs_path.to_string_lossy()
                );
            } else {
                eprint!("Writing {}...", command_rs_path.to_string_lossy());
                let comment = comment.as_deref().unwrap_or_default();
                let content = format!(
                    include_str!("command_rs.template"),
                    command = command,
                    command_name = command_name,
                    command_type = command_type,
                    command_type_name = command_type_name,
                    command_safe = command_safe,
                    comment = comment,
                );
                std::fs::write(command_rs_path, content)?;
                eprintln!(" ok");
            }
        }

        {
            let doc_mod_rs_path = Path::new("lib").join("src").join("doc").join("mod.rs");
            let content = std::fs::read_to_string(&doc_mod_rs_path)?;

            let mut lines = content.lines().map(str::to_string).collect::<Vec<_>>();

            let to_command = format!("to_command(tokens, Command::{command_name})");
            let match_case = format!("                b\"{command}\" => {to_command},");

            if lines.iter().any(|l| l.contains(&to_command)) {
                eprintln!("Match is found, skipping... ok");
            } else {
                eprint!("Match is not found, adding...");
                if let Some(unknown_pos) = lines
                    .iter()
                    .position(|l| l.trim_start().starts_with("unknown =>"))
                {
                    lines.insert(unknown_pos, match_case);
                    eprintln!(" ok");
                } else {
                    eprintln!(" fail: `unknown =>` not found");
                }
            }

            self.write_if_changed(lines, content, &doc_mod_rs_path)?;
        }

        {
            let readme_md_path = Path::new("README.md");
            let content = std::fs::read_to_string(readme_md_path)?;
            let mut lines = content.lines().map(str::to_string).collect::<Vec<_>>();

            let checkbox_empty = format!("- [ ] {command}");

            if let Some(checkbox_line) = lines.iter_mut().find(|l| l == &&checkbox_empty) {
                eprint!("Updating checkbox...");
                checkbox_line.replace_range(3..4, "x");
                eprintln!(" ok");
            }

            let count_checked = lines.iter().filter(|l| l.starts_with("- [x]")).count();
            let count_unchecked = lines.iter().filter(|l| l.starts_with("- [ ]")).count();
            let count_total = count_checked + count_unchecked;

            let implemented = format!("Implemented: {count_checked} of {count_total}.");
            if let Some(implemented_line) = lines
                .iter_mut()
                .find(|l| l.starts_with("Implemented: ") && l != &&implemented)
            {
                eprint!("Updating implemented count...");
                *implemented_line = implemented;
                eprintln!(" ok");
            }

            self.write_if_changed(lines, content, readme_md_path)?;
        }
        Ok(())
    }

    fn write_if_changed(
        &self,
        lines: Vec<String>,
        content: String,
        path: &Path,
    ) -> Result<(), std::io::Error> {
        let mut result_content = lines.join("\n");

        if content.ends_with('\n') && !result_content.ends_with('\n') {
            result_content.push('\n');
        }

        if content != result_content {
            self.write_file(path, result_content)?;
        }
        Ok(())
    }

    fn write_file(&self, p: &Path, content: String) -> Result<(), std::io::Error> {
        eprint!("Writing {}...", p.to_string_lossy());
        std::fs::write(p, content)?;
        eprintln!(" ok");
        Ok(())
    }
}

fn args() -> Option<Gen> {
    let mut args = std::env::args().skip(1);
    let command_type = args.next()?;
    let command = args.next()?;
    let comment = args.next();
    let command_name = args.next();
    Some(Gen {
        command_type,
        command,
        comment,
        command_name,
    })
}