zngur 0.9.0

A Rust/C++ interoperability tool
//! This crate contains an API for using the Zngur code generator inside build scripts. For more information
//! about the Zngur itself, see [the documentation](https://hkalbasi.github.io/zngur).

use std::{
    fs::File,
    io::Write,
    path::{Path, PathBuf},
};

use zngur_generator::{
    ParsedZngFile, ZngHeaderGenerator, ZngurGenerator,
    cfg::{InMemoryRustCfgProvider, NullCfg, RustCfgProvider},
};

#[must_use]
/// Builder for the Zngur generator.
///
/// Usage:
/// ```ignore
/// let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
/// let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
/// Zngur::from_zng_file(crate_dir.join("main.zng"))
///     .with_cpp_file(out_dir.join("generated.cpp"))
///     .with_h_file(out_dir.join("generated.h"))
///     .with_rs_file(out_dir.join("generated.rs"))
///     .with_depfile(out_dir.join("zngur.d"))
///     .generate();
/// ```
pub struct Zngur {
    zng_file: PathBuf,
    h_file_path: Option<PathBuf>,
    cpp_file_path: Option<PathBuf>,
    rs_file_path: Option<PathBuf>,
    depfile_path: Option<PathBuf>,
    mangling_base: Option<String>,
    cpp_namespace: Option<String>,
    rust_cfg: Option<Box<dyn RustCfgProvider>>,
    zng_header_in_place: bool,
    zng_h_file_path: Option<PathBuf>,
    crate_name: Option<String>,
}

impl Zngur {
    pub fn from_zng_file(zng_file_path: impl AsRef<Path>) -> Self {
        Zngur {
            zng_file: zng_file_path.as_ref().to_owned(),
            h_file_path: None,
            cpp_file_path: None,
            rs_file_path: None,
            depfile_path: None,
            mangling_base: None,
            cpp_namespace: None,
            rust_cfg: None,
            zng_header_in_place: false,
            zng_h_file_path: None,
            crate_name: None,
        }
    }

    pub fn with_zng_header(mut self, zng_header: impl AsRef<Path>) -> Self {
        self.zng_h_file_path.replace(zng_header.as_ref().to_owned());
        self
    }

    pub fn with_h_file(mut self, path: impl AsRef<Path>) -> Self {
        self.h_file_path = Some(path.as_ref().to_owned());
        self
    }

    pub fn with_cpp_file(mut self, path: impl AsRef<Path>) -> Self {
        self.cpp_file_path = Some(path.as_ref().to_owned());
        self
    }

    pub fn with_rs_file(mut self, path: impl AsRef<Path>) -> Self {
        self.rs_file_path = Some(path.as_ref().to_owned());
        self
    }

    /// Set the path for the dependency file (.d file) output.
    ///
    /// The dependency file lists all .zng files that were processed (main file + imports).
    /// This can be used by build systems to detect when regeneration is needed.
    pub fn with_depfile(mut self, path: impl AsRef<Path>) -> Self {
        self.depfile_path = Some(path.as_ref().to_owned());
        self
    }

    pub fn with_mangling_base(mut self, mangling_base: &str) -> Self {
        self.mangling_base = Some(mangling_base.to_owned());
        self
    }

    pub fn with_cpp_namespace(mut self, cpp_namespace: &str) -> Self {
        self.cpp_namespace = Some(cpp_namespace.to_owned());
        self
    }

    pub fn with_crate_name(mut self, crate_name: &str) -> Self {
        self.crate_name = Some(crate_name.to_owned());
        self
    }

    pub fn with_rust_cargo_cfg(mut self) -> Self {
        self.rust_cfg = Some(Box::new(
            InMemoryRustCfgProvider::default().load_from_cargo_env(),
        ));
        self
    }

    pub fn with_zng_header_in_place_as(mut self, value: bool) -> Self {
        self.zng_header_in_place = value;
        self
    }

    pub fn with_zng_header_in_place(self) -> Self {
        self.with_zng_header_in_place_as(true)
    }

    pub fn with_rust_in_memory_cfg<'a, CfgPairs, CfgKey, CfgValues>(
        mut self,
        cfg_values: CfgPairs,
    ) -> Self
    where
        CfgPairs: IntoIterator<Item = (CfgKey, CfgValues)>,
        CfgKey: AsRef<str> + 'a,
        CfgValues: Clone + IntoIterator + 'a,
        <CfgValues as IntoIterator>::Item: AsRef<str>,
    {
        self.rust_cfg = Some(Box::new(
            InMemoryRustCfgProvider::default().with_values(cfg_values),
        ));
        self
    }

    pub fn generate(self) {
        let rust_cfg = self.rust_cfg.unwrap_or_else(|| Box::new(NullCfg));
        let parse_result = ParsedZngFile::parse(self.zng_file, rust_cfg);
        let crate_name = self
            .crate_name
            .or_else(|| std::env::var("CARGO_PKG_NAME").ok())
            .unwrap_or_else(|| "crate".to_owned());
        // We will pass crate_name to ZngurGenerator instead of mutating spec.
        let panic_to_exception = parse_result.spec.convert_panic_to_exception.0;
        let mut file = ZngurGenerator::build_from_zng(parse_result.spec, crate_name);

        let rs_file_path = self.rs_file_path.expect("No rs file path provided");
        let h_file_path = self.h_file_path.expect("No h file path provided");

        file.0.cpp_include_header_name = h_file_path
            .file_name()
            .unwrap()
            .to_string_lossy()
            .into_owned();

        if let Some(cpp_namespace) = &self.cpp_namespace {
            file.0.mangling_base = cpp_namespace.clone();
            file.0.cpp_namespace = Some(cpp_namespace.clone());
        }

        if let Some(mangling_base) = self.mangling_base.or_else(|| file.0.cpp_namespace.clone()) {
            // println!("Mangling: {mangling_base}");
            file.0.mangling_base = mangling_base;
        }
        let (rust, h, cpp) = file.render(self.zng_header_in_place);

        File::create(&rs_file_path)
            .unwrap()
            .write_all(rust.as_bytes())
            .unwrap();
        File::create(&h_file_path)
            .unwrap()
            .write_all(h.as_bytes())
            .unwrap();
        if let Some(cpp) = &cpp {
            let cpp_file_path = self
                .cpp_file_path
                .as_ref()
                .expect("No cpp file path provided");
            File::create(cpp_file_path)
                .unwrap()
                .write_all(cpp.as_bytes())
                .unwrap();
        }

        // Write dependency file if requested
        if let Some(depfile_path) = self.depfile_path {
            let mut targets = vec![
                h_file_path.display().to_string(),
                rs_file_path.display().to_string(),
            ];
            if let Some(cpp_path) = self.cpp_file_path {
                if cpp.is_some() {
                    targets.push(cpp_path.display().to_string());
                }
            }

            // Format: "target1 target2: dep1 dep2 dep3"
            let deps: Vec<String> = parse_result
                .processed_files
                .iter()
                .map(|p| p.display().to_string())
                .collect();

            let depfile_content = format!("{}: {}\n", targets.join(" "), deps.join(" "));

            File::create(depfile_path)
                .unwrap()
                .write_all(depfile_content.as_bytes())
                .unwrap();
        }
        if let Some(zng_h) = self.zng_h_file_path {
            let mut zng = ZngurHdr::new()
                .with_panic_to_exception_as(panic_to_exception)
                .with_zng_header(zng_h);
            if let Some(cpp_namespace) = &self.cpp_namespace {
                zng = zng.with_cpp_namespace(&cpp_namespace);
            }
            zng.generate();
        }
    }
}

#[derive(Debug)]
pub struct ZngurHdr {
    panic_to_exception: bool,
    zng_header_file: Option<PathBuf>,
    cpp_namespace: Option<String>,
}

impl ZngurHdr {
    pub const fn new() -> Self {
        Self {
            panic_to_exception: false,
            zng_header_file: None,
            cpp_namespace: None,
        }
    }

    pub fn with_panic_to_exception(self) -> Self {
        self.with_panic_to_exception_as(true)
    }

    pub fn without_panic_to_exception(self) -> Self {
        self.with_panic_to_exception_as(false)
    }

    pub fn with_panic_to_exception_as(mut self, panic_to_exception: bool) -> Self {
        self.panic_to_exception = panic_to_exception;
        self
    }

    pub fn with_zng_header(mut self, zng_header: impl Into<PathBuf>) -> Self {
        self.zng_header_file.replace(zng_header.into());
        self
    }

    pub fn with_cpp_namespace(mut self, cpp_namespace: &str) -> Self {
        self.cpp_namespace = Some(cpp_namespace.to_owned());
        self
    }

    pub fn generate(self) {
        let generator = ZngHeaderGenerator {
            panic_to_exception: self.panic_to_exception,
            cpp_namespace: self.cpp_namespace.unwrap_or_else(|| "rust".to_owned()),
        };

        let out_h = self
            .zng_header_file
            .expect("Missing zng header output file");
        let rendered = generator.render();
        std::fs::write(&out_h, rendered)
            .unwrap_or_else(|_| panic!("Couldn't write contents to {}", out_h.display()));
    }
}