genotype_cli 0.16.1

Genotype language CLI
use crate::prelude::internal::*;

#[derive(Args)]
pub struct GtInitCommand {
    /// Where to initialize the project, by default it will be the current directory.
    #[arg(default_value = ".")]
    path: GtpCwdRelativeOrAbsoluteStringPath,
}

pub fn init_command(args: &GtInitCommand) -> Result<()> {
    let mut config = GtpConfig::default();

    let name = configure_name(&mut config)?;

    config.version = Some(Version::parse("0.1.0").into_diagnostic()?);

    configure_targets(&mut config, &name)?;

    let base_path: GtpCwdRelativePath = (&args.path).try_into()?;

    let backend = GtbSystem::new(&args.path).wrap_err("Failed to create system backend")?;
    let config_path: GtpConfigFilePath = base_path.join_str(GTCONFIG_FILE).into();
    block_on(backend.save_config(&config_path, &config))?;

    let src_path = base_path.join_relative_path(config.src.relative_path());

    create_dir_all(src_path.as_str())
        .map_err(|_| GtCliError::FailedCreateDir(src_path.as_str().into()))?;

    for (file, content) in GUIDE_FILES {
        write(src_path.join_str(file).as_str(), content)
            .map_err(|_| GtCliError::FailedWrite(src_path.join_str(file).as_str().into()))?;
    }

    println!(
        "{generated} project at {path:?}, run `{command}` to build the project",
        generated = "Generated".green().bold(),
        path = base_path.as_str(),
        command = "gt build".yellow().bold()
    );

    Ok(())
}

const GUIDE_FILES: &[(&str, &str)] = &[
    (
        "guide.type",
        include_str!("../../examples/guide/guide.type"),
    ),
    (
        "module.type",
        include_str!("../../examples/guide/module.type"),
    ),
];

fn configure_name(config: &mut GtpConfig) -> Result<String> {
    let cd_name = std::env::current_dir()
        .map(|path| path.file_name().unwrap().to_string_lossy().to_string())
        .unwrap_or_default();

    let name = Text::new("Name your project:")
        .with_default(&cd_name)
        .with_validator(required!("Please provide the project name"))
        .with_validator(min_length!(1, "Please provide the project name"))
        .prompt()
        .map_err(|_| GtCliError::FailedReadline("project name"))?;

    config.name = Some(name.clone());

    Ok(name)
}

fn configure_targets(config: &mut GtpConfig, name: &str) -> Result<()> {
    let targets = MultiSelect::new(
        "Choose the languages you want to target:",
        Target::VARIANTS.to_vec(),
    )
    .with_validator(|targets: &[ListOption<&Target>]| {
        Ok(if targets.is_empty() {
            Validation::Invalid("Please select at least one language".into())
        } else {
            Validation::Valid
        })
    })
    .prompt()
    .map_err(|_| GtCliError::FailedReadline("targets"))?;

    for target in targets {
        match target {
            Target::TypeScript => configure_ts(config, name)?,
            Target::Python => configure_py(config, name)?,
            Target::Rust => configure_rs(config, name)?,
        }
    }

    Ok(())
}

fn configure_ts(config: &mut GtpConfig, name: &str) -> Result<()> {
    let mut ts = TsConfig::default();
    ts.common.enabled = true;

    let default_name = name.to_kebab_case();
    let name = Text::new("Name the TypeScript package:")
        .with_default(&default_name)
        .with_validator(required!("Please provide the TypeScript package name"))
        .with_validator(min_length!(1, "Please provide the TypeScript package name"))
        .with_validator(|name: &str| {
            let re = Regex::new(r"^(@[a-z0-9][a-z0-9\-_\.]*\/)?[a-z0-9][a-z0-9\-_\.]*$")?;
            Ok(if re.is_match(name) {
                Validation::Valid
            } else {
                Validation::Invalid("Invalid package name".into())
            })
        })
        .prompt()
        .map_err(|_| GtCliError::FailedReadline("TypeScript package name"))?;

    let manifest =
        toml::map::Map::from_iter(vec![("name".into(), toml::Value::String(name.clone()))]);

    ts.common.manifest = manifest;

    config.ts = ts;

    Ok(())
}

fn configure_py(config: &mut GtpConfig, name: &str) -> Result<()> {
    let mut py = PyConfig::default();
    py.common.enabled = true;

    let default_name = name.to_kebab_case();
    let name = Text::new("Name the Python package:")
        .with_default(&default_name)
        .with_validator(required!("Please provide the Python package name"))
        .with_validator(min_length!(1, "Please provide the Python package name"))
        .with_validator(|name: &str| {
            let re = Regex::new(r"^[A-Za-z0-9][A-Za-z0-9_-]*$")?;
            Ok(if re.is_match(name) {
                Validation::Valid
            } else {
                Validation::Invalid("Invalid package name".into())
            })
        })
        .prompt()
        .map_err(|_| GtCliError::FailedReadline("Python package name"))?;

    py.lang.manager = PyPackageManager::Uv;

    let project = toml::map::Map::from_iter(vec![("name".into(), toml::Value::String(name))]);
    let manifest = toml::map::Map::from_iter(vec![("project".into(), toml::Value::Table(project))]);

    py.common.manifest = manifest;

    config.py = py;

    Ok(())
}

fn configure_rs(config: &mut GtpConfig, name: &str) -> Result<()> {
    let mut rs = RsConfig::default();
    rs.common.enabled = true;

    let default_name = name.to_snake_case();
    let name = Text::new("Name the Rust crate:")
        .with_default(&default_name)
        .with_validator(required!("Please provide the Rust crate name"))
        .with_validator(min_length!(1, "Please provide the Rust crate name"))
        .with_validator(|name: &str| {
            let re = Regex::new(r"^[A-Za-z0-9][A-Za-z0-9_-]*$")?;
            Ok(if re.is_match(name) {
                Validation::Valid
            } else {
                Validation::Invalid("Invalid crate name".into())
            })
        })
        .prompt()
        .map_err(|_| GtCliError::FailedReadline("Rust package name"))?;

    let package = toml::map::Map::from_iter(vec![
        ("name".into(), toml::Value::String(name.clone())),
        (
            "edition".into(),
            toml::Value::String(RsConfigLang::DEFAULT_EDITION.into()),
        ),
    ]);

    let manifest = toml::map::Map::from_iter(vec![("package".into(), toml::Value::Table(package))]);

    rs.common.manifest = manifest;

    config.rs = rs;

    Ok(())
}

#[derive(Debug, Clone, Copy)]
enum Target {
    TypeScript,
    Python,
    Rust,
}

impl Target {
    const VARIANTS: &'static [Target] = &[Self::TypeScript, Self::Python, Self::Rust];
}

impl Display for Target {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
        write!(f, "{self:?}")
    }
}