nestrs-cli-rs 0.1.0

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

use crate::actions::abstract_action::AbstractAction;
use crate::actions::{ActionInvocation, ActionKind, ActionSpec, action_spec};
use crate::commands::Input;
use crate::configuration::Configuration;
use crate::package_managers::{PackageManager, PackageManagerClient};
pub use crate::runners::schematic_runner::SCHEMATICS_CLI_RELATIVE_PATH;
pub use crate::runners::schematic_runner::find_closest_schematics_binary;
use crate::runners::{Runner, RunnerCommand, RunnerFactory};
use crate::schematics::{NESTJS_COLLECTION_NAME, SchematicOption};
use schematics::nest_add::{has_native_nest_add, unsupported_native_nest_add_reason};
use std::path::{Path, PathBuf};

pub const SCHEMATIC_NAME: &str = "nest-add";
/// Typed wrapper for upstream `AddAction`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct AddAction;

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

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

    pub fn handle_invocation(
        &self,
        inputs: Vec<Input>,
        options: Vec<Input>,
        extra_flags: Vec<String>,
    ) -> ActionInvocation {
        <Self as AbstractAction>::handle(self, inputs, options, extra_flags)
    }

    pub fn create_plan(&self, request: AddActionRequest) -> AddActionPlan {
        create_add_action_plan(request)
    }
}

impl AbstractAction for AddAction {
    fn kind(&self) -> ActionKind {
        ActionKind::Add
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AddActionRequest {
    pub library: String,
    pub skip_install: bool,
    pub dry_run: bool,
    pub project: Option<String>,
    pub source_root: Option<String>,
    pub extra_flags: Vec<String>,
    pub package_manager: PackageManager,
    pub configuration: Configuration,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AddActionPlan {
    pub library_name: String,
    pub package_name: String,
    pub collection_name: String,
    pub tag_name: String,
    pub source_root: String,
    pub install_command: Option<RunnerCommand>,
    pub schematic_command: String,
    pub dry_run: bool,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AddSchematicExecutionPlan {
    pub schematics_binary: PathBuf,
    pub command: RunnerCommand,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AddExecutionPlan {
    Native(AddNativeExecutionPlan),
    Node(AddSchematicExecutionPlan),
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AddNativeExecutionPlan {
    pub collection_name: String,
    pub source_root: String,
    pub extra_flags: Vec<String>,
    pub dry_run: bool,
}

pub fn create_add_action_plan(request: AddActionRequest) -> AddActionPlan {
    let package_name = get_package_name(&request.library);
    let collection_name = get_collection_name(&request.library, &package_name);
    let tag_name = get_tag_name(&request.library)
        .filter(|tag| !tag.is_empty())
        .unwrap_or_else(|| "latest".to_string());
    let source_root = resolve_source_root(
        request.source_root,
        request.project.as_deref(),
        &request.configuration,
    );
    let install_command = (!request.skip_install
        && !request.dry_run
        && !has_native_nest_add(&collection_name))
    .then(|| {
        PackageManagerClient::new(request.package_manager)
            .add_production_command(&[collection_name.as_str()], &tag_name)
    });
    let mut options = vec![SchematicOption::new("sourceRoot", source_root.as_str())];
    if request.dry_run {
        options.push(SchematicOption::new("dry-run", true));
    }
    let mut schematic_command = format!(
        "{collection_name}:{SCHEMATIC_NAME}{}",
        options.iter().fold(String::new(), |mut output, option| {
            output.push(' ');
            output.push_str(&option.to_command_string());
            output
        })
    );
    if !request.extra_flags.is_empty() {
        schematic_command.push(' ');
        schematic_command.push_str(&request.extra_flags.join(" "));
    }

    AddActionPlan {
        library_name: request.library,
        package_name,
        collection_name,
        tag_name,
        source_root,
        install_command,
        schematic_command,
        dry_run: request.dry_run,
    }
}

pub fn create_add_schematic_execution_plan(
    plan: &AddActionPlan,
    cwd: impl AsRef<Path>,
) -> Result<AddSchematicExecutionPlan, String> {
    let cwd = cwd.as_ref();
    let schematics_binary = find_closest_schematics_binary(cwd)?;
    let command = RunnerFactory::create_schematic(&schematics_binary).describe(
        &plan.schematic_command,
        false,
        Some(cwd.to_path_buf()),
    );

    Ok(AddSchematicExecutionPlan {
        schematics_binary,
        command,
    })
}

pub fn create_add_execution_plan(
    plan: &AddActionPlan,
    extra_flags: &[String],
    cwd: impl AsRef<Path>,
) -> Result<AddExecutionPlan, String> {
    if has_native_nest_add(&plan.collection_name) {
        return Ok(AddExecutionPlan::Native(AddNativeExecutionPlan {
            collection_name: plan.collection_name.clone(),
            source_root: plan.source_root.clone(),
            extra_flags: extra_flags.to_vec(),
            dry_run: plan.dry_run,
        }));
    }

    if let Some(reason) = unsupported_native_nest_add_reason(&plan.collection_name) {
        return Err(reason.to_string());
    }

    create_add_schematic_execution_plan(plan, cwd).map(AddExecutionPlan::Node)
}

pub fn has_native_add_handler(collection_name: &str) -> bool {
    has_native_nest_add(collection_name)
}

pub fn unsupported_native_add_reason(collection_name: &str) -> Option<&'static str> {
    unsupported_native_nest_add_reason(collection_name)
}

pub fn get_package_name(library: &str) -> String {
    let end = package_end_index(library);
    library[..end].to_string()
}

pub fn get_collection_name(library: &str, package_name: &str) -> String {
    if let Some(tag_start) = package_tag_index(library) {
        let tag_end = library[tag_start + 1..]
            .find('/')
            .map(|index| tag_start + 1 + index)
            .unwrap_or(library.len());
        format!("{}{}", &library[..tag_start], &library[tag_end..])
    } else if let Some(suffix) = library.strip_prefix(package_name) {
        format!("{package_name}{suffix}")
    } else {
        library.to_string()
    }
}

pub fn get_tag_name(library: &str) -> Option<String> {
    package_tag_index(library).and_then(|tag_start| {
        let tag = library[tag_start + 1..]
            .split('/')
            .next()
            .unwrap_or_default();
        (!tag.is_empty()).then(|| tag.to_string())
    })
}

fn package_end_index(library: &str) -> usize {
    if library.starts_with('@') {
        let slash = match library.find('/') {
            Some(index) => index,
            None => return library.len(),
        };
        let after_name = slash + 1;
        let scoped_name_len = library[after_name..]
            .find(['@', '/'])
            .map(|index| after_name + index)
            .unwrap_or(library.len());
        scoped_name_len
    } else {
        library.find(['@', '/']).unwrap_or(library.len())
    }
}

fn package_tag_index(library: &str) -> Option<usize> {
    if library.starts_with('@') {
        let slash = library.find('/')?;
        library[slash + 1..]
            .find('@')
            .map(|index| slash + 1 + index)
    } else {
        library.find('@')
    }
}

fn resolve_source_root(
    source_root: Option<String>,
    project: Option<&str>,
    configuration: &Configuration,
) -> String {
    if let Some(source_root) = source_root {
        return source_root;
    }

    if let Some(project) = project {
        if let Some(project_configuration) = configuration.projects.get(project) {
            if let Some(source_root) = &project_configuration.source_root {
                return source_root.clone();
            }
        }
    }

    configuration.source_root.clone()
}

pub fn default_collection_name() -> &'static str {
    NESTJS_COLLECTION_NAME
}