nativ 0.2.0

Nativ CLI — compile .nativ DSL to real SwiftUI and Jetpack Compose
use clap::Args;
use nativ_config::NativConfig;
use nativ_pipeline::{self, Target};
use std::path::Path;
use std::time::Instant;

#[derive(Args)]
pub struct BuildArgs {
    /// Build only for iOS
    #[arg(long)]
    pub ios: bool,

    /// Build only for Android
    #[arg(long)]
    pub android: bool,

    /// Project directory (default: current directory)
    #[arg(short, long, default_value = ".")]
    pub dir: String,
}

pub fn run(args: BuildArgs, verbose: bool, quiet: bool) -> Result<(), Box<dyn std::error::Error>> {
    let project_dir = Path::new(&args.dir);
    let config_path = project_dir.join("nativ.toml");

    let config = NativConfig::load(&config_path)?;

    let targets = nativ_pipeline::resolve_targets(args.ios, args.android, &config);

    if targets.is_empty() {
        return Err("No target platform specified. Enable ios or android in nativ.toml".into());
    }

    if !quiet {
        let target_names: Vec<&str> = targets
            .iter()
            .map(|t| match t {
                Target::Ios => "iOS",
                Target::Android => "Android",
            })
            .collect();
        println!(
            "Compiling: {} -> {}",
            config.app.name,
            target_names.join(", ")
        );
    }

    let start = Instant::now();

    let results = nativ_pipeline::build(project_dir, &config, &targets)?;

    let elapsed = start.elapsed();

    if !quiet {
        for result in &results {
            let platform = match result.target {
                Target::Ios => "iOS",
                Target::Android => "Android",
            };
            println!(
                "  {platform}: {} files generated",
                result.generated_files.len()
            );

            if verbose {
                for file in &result.generated_files {
                    println!("    -> {}", file.display());
                }
            }
        }

        let total: usize = results.iter().map(|r| r.generated_files.len()).sum();
        println!(
            "Build complete: {total} files, {:.2}s",
            elapsed.as_secs_f64()
        );
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn args_for(dir: &Path, ios: bool, android: bool) -> BuildArgs {
        BuildArgs {
            ios,
            android,
            dir: dir.display().to_string(),
        }
    }

    #[test]
    fn fails_when_config_is_missing() {
        let tmp = tempfile::tempdir().unwrap();

        let err = run(args_for(tmp.path(), true, false), false, true).unwrap_err();
        assert!(err.to_string().contains("nativ.toml"), "{err}");
    }

    #[test]
    fn fails_when_no_target_platform_enabled() {
        let tmp = tempfile::tempdir().unwrap();
        std::fs::write(
            tmp.path().join("nativ.toml"),
            "[app]\nname = \"t\"\n\n[build]\nios = false\nandroid = false\n",
        )
        .unwrap();

        let err = run(args_for(tmp.path(), false, false), false, true).unwrap_err();
        assert!(err.to_string().contains("No target platform"), "{err}");
    }

    #[test]
    fn fails_when_src_dir_is_missing() {
        let tmp = tempfile::tempdir().unwrap();
        std::fs::write(tmp.path().join("nativ.toml"), "[app]\nname = \"t\"\n").unwrap();

        let err = run(args_for(tmp.path(), true, false), false, true).unwrap_err();
        assert!(err.to_string().contains("not found"), "{err}");
    }
}