cargo-get 1.4.0

Cargo plugin to easily query information from Cargo.toml files
mod cli;
mod delimiter;
mod error;
mod output_format;
mod terminator;

use cargo_toml::Manifest;
use clap::Parser;
use cli::MaybeCommand;
use delimiter::Delimiter;
use error::NotSpecified;
use std::{error::Error, path::PathBuf};
use terminator::Terminator;

fn main() -> Result<(), Box<dyn Error>> {
    let mut args: Vec<_> = std::env::args().collect();

    if let Some("get") = args.get(1).map(String::as_ref) {
        args.remove(1);
    }

    let cli = cli::Cli::parse_from(args);

    match output(cli) {
        Ok(out) => print!("{out}"),
        Err(err) => {
            eprintln!("Error: {err}");
            std::process::exit(1);
        }
    }

    Ok(())
}

/// Generate output string that will be printed to the console
pub fn output(cli: cli::Cli) -> Result<String, Box<dyn Error>> {
    let entry_point = match cli.entry.clone() {
        Some(p) => p,
        None => std::env::current_dir()?,
    };

    let entry_point_absolute =
        std::fs::canonicalize(entry_point).map_err(|_| "No such file or directory")?;

    let manifest_path = search_manifest_path(&entry_point_absolute).ok_or("No manifest found")?;

    let manifest = Manifest::from_path(manifest_path)?;

    let package = || manifest.package.clone().ok_or(NotSpecified("package"));
    let workspace = || manifest.workspace.clone().ok_or(NotSpecified("workspace"));
    let ws_package =
        || workspace().and_then(|ws| ws.package.ok_or(NotSpecified("workspace.package")));

    let delimiter: Delimiter = cli.delimiter.unwrap_or_default();
    let delim_string = delimiter.to_string();
    let terminator: Terminator = cli.terminator.unwrap_or_default();

    let get_output = |cmd: &cli::Command| {
        let output = match cmd {
            cli::Command::PackageVersion { inner } => {
                let v: semver::Version = package()?.version().parse()?;
                inner.match_version(v, &delimiter)?
            }
            cli::Command::PackageAuthors => package()?.authors().join(&delim_string),

            cli::Command::PackageEdition => package()?.edition().to_string(),
            cli::Command::PackageName => package()?.name().to_string(),
            cli::Command::PackageHomepage => package()?
                .homepage()
                .ok_or(NotSpecified("package.homepage"))?
                .to_string(),
            cli::Command::PackageKeywords => package()?.keywords().join(&delim_string),
            cli::Command::PackageLicense => package()?
                .license()
                .ok_or(NotSpecified("package.license"))?
                .to_string(),
            cli::Command::PackageLinks => package()?
                .links()
                .ok_or(NotSpecified("package.links"))?
                .to_string(),
            cli::Command::PackageDescription => package()?
                .description()
                .ok_or(NotSpecified("package.description"))?
                .to_string(),
            cli::Command::PackageCategories => package()?.categories().join(&delim_string),
            cli::Command::PackageRustVersion => package()?
                .rust_version()
                .ok_or(NotSpecified("package.rust_version"))?
                .to_string(),
            cli::Command::PackageBuild => package()?
                .build
                .ok_or(NotSpecified("package.build"))?
                .as_path()
                .unwrap()
                .to_string_lossy()
                .to_string(),

            cli::Command::PackageWorkspace => package()?
                .workspace
                .ok_or(NotSpecified("package.workspace"))?
                .to_string_lossy()
                .to_string(),

            cli::Command::PackageReadme => package()?
                .readme()
                .as_path()
                .ok_or(NotSpecified("package.readme"))?
                .to_string_lossy()
                .to_string(),

            cli::Command::PackageExclude => package()?.exclude().join(&delim_string),
            cli::Command::PackageInclude => package()?.include().join(&delim_string),
            cli::Command::PackageLicenseFile => package()?
                .license_file()
                .ok_or(NotSpecified("package.license_file"))?
                .to_string_lossy()
                .to_string(),

            cli::Command::PackageRepository => package()?
                .repository()
                .ok_or(NotSpecified("package.repository"))?
                .to_string(),

            cli::Command::PackageDefaultRun => package()?
                .default_run
                .ok_or(NotSpecified("package.default_run"))?
                .to_string(),

            cli::Command::PackagePublish => match package()?.publish() {
                cargo_toml::Publish::Flag(flag) => flag.to_string(),
                cargo_toml::Publish::Registry(list) => list.join(&delim_string),
            },
            cli::Command::PackageResolver => package()?
                .resolver
                .ok_or(NotSpecified("package.resolver"))?
                .to_string(),

            cli::Command::PackageMetadata => package()?
                .metadata
                .ok_or(NotSpecified("package.metadata"))?
                .to_string(),

            cli::Command::WorkspaceMembers => workspace()?.members.join(&delim_string),

            cli::Command::WorkspaceDefaultMembers => {
                workspace()?.default_members.join(&delim_string)
            }

            cli::Command::WorkspacePackageVersion { inner } => {
                let v: semver::Version = ws_package()?
                    .version
                    .ok_or(NotSpecified("workspace.package.version"))?
                    .parse()?;
                inner.match_version(v, &delimiter)?
            }

            cli::Command::WorkspacePackageAuthors => ws_package()?
                .authors
                .ok_or(NotSpecified("workspace.package.authors"))?
                .join(&delim_string),

            cli::Command::WorkspacePackageEdition => ws_package()?
                .edition
                .map(|edition| edition.to_string())
                .ok_or(NotSpecified("workspace.package.edition"))?
                .to_string(),

            cli::Command::WorkspacePackageHomepage => ws_package()?
                .homepage
                .ok_or(NotSpecified("workspace.package.homepage"))?,

            cli::Command::WorkspacePackageKeywords => ws_package()?
                .keywords
                .ok_or(NotSpecified("workspace.package.keywords"))?
                .join(&delim_string),

            cli::Command::WorkspacePackageLicense => ws_package()?
                .license
                .ok_or(NotSpecified("workspace.package.license"))?,

            cli::Command::WorkspacePackageDescription => ws_package()?
                .description
                .ok_or(NotSpecified("workspace.package.license"))?,

            cli::Command::WorkspacePackageCategories => ws_package()?
                .categories
                .ok_or(NotSpecified("workspace.package.categories"))?
                .join(&delim_string),
            cli::Command::WorkspacePackageDocumentation => ws_package()?
                .documentation
                .ok_or(NotSpecified("workspace.package.documentation"))?,

            cli::Command::WorkspacePackageExclude => ws_package()?
                .exclude
                .ok_or(NotSpecified("workspace.package.exclude"))?
                .join(&delim_string),

            cli::Command::WorkspacePackageInclude => ws_package()?
                .include
                .ok_or(NotSpecified("workspace.package.include"))?
                .join(&delim_string),

            cli::Command::WorkspacePackageLicenseFile => ws_package()?
                .license_file
                .ok_or(NotSpecified("workspace.package.license_file"))?
                .to_string_lossy()
                .to_string(),

            cli::Command::WorkspacePackagePublish => match ws_package()?.publish {
                cargo_toml::Publish::Flag(flag) => flag.to_string(),
                cargo_toml::Publish::Registry(list) => list.join(&delim_string),
            },
            cli::Command::WorkspacePackageReadme => ws_package()?
                .readme
                .as_path()
                .ok_or(NotSpecified("workspace.package.readme"))?
                .to_string_lossy()
                .to_string(),

            cli::Command::WorkspacePackageRepository => ws_package()?
                .repository
                .ok_or(NotSpecified("workspace.package.repository"))?,

            cli::Command::WorkspacePackageRustVersion => ws_package()?
                .rust_version
                .ok_or(NotSpecified("workspace.package.rust_version"))?
                .to_string(),
        };

        Result::<_, Box<dyn Error>>::Ok(output)
    };

    let output = match cli.command {
        MaybeCommand::Command(cmd) => {
            let mut output = get_output(&cmd)?;
            output.push_str(terminator.to_string().as_ref());
            output
        }
        MaybeCommand::All(all) => {
            use strum::IntoEnumIterator;

            cli::Command::iter()
                .filter_map(|cmd| {
                    let output = get_output(&cmd).ok()?;
                    Some((cmd, output))
                })
                .map(|(cmd, res)| all.output_format.format_pair(cmd, &res))
                .collect()
        }
    };

    Ok(output)
}

/// Search the given directory for Cargo.toml, recursively searching upwards
fn search_manifest_path(dir: &std::path::Path) -> Option<PathBuf> {
    let manifest = dir.join("Cargo.toml");

    if std::fs::metadata(&manifest).is_ok() {
        Some(manifest)
    } else {
        dir.parent().and_then(search_manifest_path)
    }
}