nestrs-cli-rs 0.1.0

Rust port of the Nest CLI for the nestrs organization.
Documentation
//! Port plan for upstream `../nest-cli/actions/info.action.ts`.

use crate::actions::abstract_action::AbstractAction;
use crate::actions::{ActionInvocation, ActionKind, ActionSpec, action_spec};
use indexmap::IndexMap;

/// Typed wrapper for upstream `InfoAction`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct InfoAction;

impl InfoAction {
    pub const fn new() -> Self {
        Self
    }

    pub fn spec(&self) -> &'static ActionSpec {
        action_spec(ActionKind::Info).expect("info action spec")
    }

    pub fn handle_invocation(&self) -> ActionInvocation {
        <Self as AbstractAction>::handle(self, Vec::new(), Vec::new(), Vec::new())
    }

    pub fn create_plan(
        &self,
        system: SystemInformation,
        package_manager: PackageManagerInformation,
        cli_version: impl Into<String>,
        dependencies: &[PackageJsonDependency],
    ) -> InfoActionPlan {
        create_info_action_plan(system, package_manager, cli_version, dependencies)
    }
}

impl AbstractAction for InfoAction {
    fn kind(&self) -> ActionKind {
        ActionKind::Info
    }
}

pub const WARNING_MESSAGE_DEPENDENCIES_WHITE_LIST: &[&str] = &[
    "@nestrs/core",
    "@nestrs/common",
    "@nestrs/schematics",
    "@nestrs/platform-axum",
    "@nestrs/platform-actix",
    "@nestrs/platform-socket.io",
    "@nestrs/platform-ws",
    "@nestrs/websockets",
];

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InfoActionPlan {
    pub system: SystemInformation,
    pub package_manager: PackageManagerInformation,
    pub cli_version: String,
    pub nest_dependencies: Vec<NestDependency>,
    pub warnings: NestDependencyWarnings,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SystemInformation {
    pub os_version: String,
    pub node_version: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PackageManagerInformation {
    pub name: String,
    pub version: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PackageJsonDependency {
    pub package_name: String,
    pub declared_version: String,
    pub resolved_version: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NestDependency {
    pub name: String,
    pub value: String,
    pub package_name: String,
}

pub type NestDependencyWarnings = IndexMap<String, Vec<NestDependency>>;

pub fn create_info_action_plan(
    system: SystemInformation,
    package_manager: PackageManagerInformation,
    cli_version: impl Into<String>,
    dependencies: &[PackageJsonDependency],
) -> InfoActionPlan {
    let nest_dependencies = build_nest_versions_message(dependencies);
    let warnings = build_nest_versions_warning_message(&nest_dependencies);
    InfoActionPlan {
        system,
        package_manager,
        cli_version: cli_version.into(),
        nest_dependencies,
        warnings,
    }
}

pub fn build_nest_versions_message(dependencies: &[PackageJsonDependency]) -> Vec<NestDependency> {
    format(collect_nest_dependencies(dependencies))
}

pub fn collect_nest_dependencies(dependencies: &[PackageJsonDependency]) -> Vec<NestDependency> {
    dependencies
        .iter()
        .filter(|dependency| dependency.package_name.contains("@nestrs"))
        .map(|dependency| {
            let value = dependency
                .resolved_version
                .clone()
                .unwrap_or_else(|| dependency.declared_version.clone());
            NestDependency {
                name: format!(
                    "{} version",
                    dependency
                        .package_name
                        .replace("@nestrs/", "")
                        .split('@')
                        .next()
                        .unwrap_or(&dependency.package_name)
                ),
                value,
                package_name: dependency.package_name.clone(),
            }
        })
        .collect()
}

pub fn format(mut dependencies: Vec<NestDependency>) -> Vec<NestDependency> {
    dependencies.sort_by(|left, right| right.name.len().cmp(&left.name.len()));
    let Some(length) = dependencies.first().map(|dependency| dependency.name.len()) else {
        return dependencies;
    };

    dependencies
        .into_iter()
        .map(|mut dependency| {
            dependency.name = right_pad(dependency.name, length);
            dependency.name.push_str(" :");
            dependency.value = dependency.value.replace(['^', '~'], "");
            dependency
        })
        .collect()
}

pub fn build_nest_versions_warning_message(
    nest_dependencies: &[NestDependency],
) -> NestDependencyWarnings {
    let mut unsorted_warnings: NestDependencyWarnings = IndexMap::new();

    for dependency in nest_dependencies {
        if !WARNING_MESSAGE_DEPENDENCIES_WHITE_LIST.contains(&dependency.package_name.as_str()) {
            continue;
        }

        let major = dependency
            .value
            .chars()
            .filter(|character| character.is_ascii_digit() || *character == '.')
            .collect::<String>()
            .split('.')
            .next()
            .unwrap_or_default()
            .to_string();

        unsorted_warnings
            .entry(major)
            .or_default()
            .push(dependency.clone());
    }

    if unsorted_warnings.len() <= 1 {
        return IndexMap::new();
    }

    let mut entries = unsorted_warnings.into_iter().collect::<Vec<_>>();
    entries.sort_by(|(left, _), (right, _)| compare_versions_desc(left, right));
    entries.into_iter().collect()
}

pub fn right_pad(mut name: String, length: usize) -> String {
    while name.len() < length {
        name.push(' ');
    }
    name
}

fn compare_versions_desc(left: &str, right: &str) -> std::cmp::Ordering {
    let left = left.parse::<f64>();
    let right = right.parse::<f64>();

    match (left, right) {
        (Ok(left), Ok(right)) => right
            .partial_cmp(&left)
            .unwrap_or(std::cmp::Ordering::Equal),
        (Err(_), Err(_)) => std::cmp::Ordering::Equal,
        (Err(_), Ok(_)) => std::cmp::Ordering::Greater,
        (Ok(_), Err(_)) => std::cmp::Ordering::Less,
    }
}