camber-cli 0.1.6

CLI, project scaffolding, and config-driven proxy for Camber
Documentation
use crate::context;
use crate::template;
use std::fs;
use std::io;
use std::path::Path;

pub fn run(name: &str, tmpl: &str) -> io::Result<()> {
    validate_project_name(name)?;
    let project = Path::new(name);

    if project.exists() {
        return Err(io::Error::new(
            io::ErrorKind::AlreadyExists,
            format!("directory '{name}' already exists"),
        ));
    }

    match tmpl {
        "http" => scaffold_http(project, name),
        "fanout" => scaffold_fanout(project, name),
        "advanced" => scaffold_advanced(project, name),
        _ => Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!(
                "unknown template '{tmpl}'. available templates: {}",
                template::AVAILABLE_TEMPLATES.join(", ")
            ),
        )),
    }
}

fn validate_project_name(name: &str) -> io::Result<()> {
    if name.contains('/') || name.contains('\\') {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("project name '{name}' must not contain path separators"),
        ));
    }

    match name {
        "." | ".." | "" => {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                format!("project name '{name}' is not a valid Cargo package name"),
            ));
        }
        _ => {}
    }

    let mut chars = name.chars();
    let Some(first_char) = chars.next() else {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("project name '{name}' is not a valid Cargo package name"),
        ));
    };

    if !matches!(first_char, 'a'..='z' | 'A'..='Z' | '_') {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("project name '{name}' is not a valid Cargo package name"),
        ));
    }

    if chars.any(|character| !matches!(character, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_')) {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("project name '{name}' is not a valid Cargo package name"),
        ));
    }

    Ok(())
}

fn scaffold(
    project: &Path,
    name: &str,
    cargo_template: &str,
    main_template: &str,
) -> io::Result<()> {
    let src_dir = project.join("src");
    fs::create_dir_all(&src_dir)?;

    let cargo_toml = cargo_template.replace("{{name}}", name);
    fs::write(project.join("Cargo.toml"), cargo_toml)?;
    fs::write(src_dir.join("main.rs"), main_template)?;
    write_llms_txt(project)?;

    print_success(name);
    Ok(())
}

fn scaffold_http(project: &Path, name: &str) -> io::Result<()> {
    scaffold(
        project,
        name,
        template::HTTP_CARGO_TOML,
        template::HTTP_MAIN_RS,
    )
}

fn scaffold_fanout(project: &Path, name: &str) -> io::Result<()> {
    scaffold(
        project,
        name,
        template::FANOUT_CARGO_TOML,
        template::FANOUT_MAIN_RS,
    )
}

fn scaffold_advanced(project: &Path, name: &str) -> io::Result<()> {
    let src_dir = project.join("src");
    let proto_dir = project.join("proto");
    fs::create_dir_all(&src_dir)?;
    fs::create_dir_all(&proto_dir)?;

    let cargo_toml = template::ADVANCED_CARGO_TOML.replace("{{name}}", name);
    fs::write(project.join("Cargo.toml"), cargo_toml)?;
    fs::write(src_dir.join("main.rs"), template::ADVANCED_MAIN_RS)?;
    fs::write(project.join("build.rs"), template::ADVANCED_BUILD_RS)?;
    fs::write(proto_dir.join("echo.proto"), template::ADVANCED_PROTO)?;
    write_llms_txt(project)?;

    print_success(name);
    Ok(())
}

fn write_llms_txt(project: &Path) -> io::Result<()> {
    fs::write(project.join("llms.txt"), context::LLMS_TXT)
}

fn print_success(name: &str) {
    println!("Created project '{name}'");
    println!("  cd {name} && cargo run");
}