nestrs-cli-rs 0.1.0

Rust port of the Nest CLI for the nestrs organization.
Documentation
//! Runtime actions for CLI commands.
//!
//! These are initial data models for upstream `nest-cli/actions/*.action.ts`.
//! They describe which action a parsed command should invoke and the broad
//! side effects each action performs.

use crate::commands::{CommandName, Input};

pub mod abstract_action;
pub mod add_action;
pub mod build_action;
pub mod build_executor;
pub mod generate_action;
pub mod info_action;
pub mod new_action;
pub mod start_action;
pub mod start_executor;

/// Runtime action classes declared by upstream Nest CLI.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ActionKind {
    Add,
    Build,
    Generate,
    Info,
    New,
    Start,
}

impl ActionKind {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Add => "add",
            Self::Build => "build",
            Self::Generate => "generate",
            Self::Info => "info",
            Self::New => "new",
            Self::Start => "start",
        }
    }

    pub const fn command(self) -> CommandName {
        match self {
            Self::Add => CommandName::Add,
            Self::Build => CommandName::Build,
            Self::Generate => CommandName::Generate,
            Self::Info => CommandName::Info,
            Self::New => CommandName::New,
            Self::Start => CommandName::Start,
        }
    }
}

impl From<CommandName> for ActionKind {
    fn from(command: CommandName) -> Self {
        match command {
            CommandName::Add => Self::Add,
            CommandName::Build => Self::Build,
            CommandName::Generate => Self::Generate,
            CommandName::Info => Self::Info,
            CommandName::New => Self::New,
            CommandName::Start => Self::Start,
        }
    }
}

/// High-level side effects visible in upstream action implementations.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ActionEffect {
    ExecuteSchematic,
    InstallPackage,
    RunBuild,
    SpawnApplication,
    InitializeGitRepository,
    WriteGitIgnore,
    DisplayProjectInformation,
    PromptForMissingInput,
}

/// Static descriptor for an upstream action class.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ActionSpec {
    pub kind: ActionKind,
    pub upstream_class: &'static str,
    pub effects: &'static [ActionEffect],
    pub accepts_extra_flags: bool,
}

/// Data passed to an action `handle` method after command parsing.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ActionInvocation {
    pub kind: ActionKind,
    pub inputs: Vec<Input>,
    pub options: Vec<Input>,
    pub extra_flags: Vec<String>,
}

impl ActionInvocation {
    pub fn new(kind: ActionKind) -> Self {
        Self {
            kind,
            inputs: Vec::new(),
            options: Vec::new(),
            extra_flags: Vec::new(),
        }
    }

    pub fn for_command(command: CommandName) -> Self {
        Self::new(command.into())
    }
}

const ADD_EFFECTS: &[ActionEffect] = &[
    ActionEffect::InstallPackage,
    ActionEffect::PromptForMissingInput,
    ActionEffect::ExecuteSchematic,
];

const BUILD_EFFECTS: &[ActionEffect] = &[ActionEffect::RunBuild];

const GENERATE_EFFECTS: &[ActionEffect] = &[
    ActionEffect::PromptForMissingInput,
    ActionEffect::ExecuteSchematic,
];

const INFO_EFFECTS: &[ActionEffect] = &[ActionEffect::DisplayProjectInformation];

const NEW_EFFECTS: &[ActionEffect] = &[
    ActionEffect::PromptForMissingInput,
    ActionEffect::ExecuteSchematic,
    ActionEffect::InstallPackage,
    ActionEffect::InitializeGitRepository,
    ActionEffect::WriteGitIgnore,
];

const START_EFFECTS: &[ActionEffect] = &[ActionEffect::RunBuild, ActionEffect::SpawnApplication];

pub const ACTION_SPECS: &[ActionSpec] = &[
    ActionSpec {
        kind: ActionKind::Add,
        upstream_class: "AddAction",
        effects: ADD_EFFECTS,
        accepts_extra_flags: true,
    },
    ActionSpec {
        kind: ActionKind::Build,
        upstream_class: "BuildAction",
        effects: BUILD_EFFECTS,
        accepts_extra_flags: false,
    },
    ActionSpec {
        kind: ActionKind::Generate,
        upstream_class: "GenerateAction",
        effects: GENERATE_EFFECTS,
        accepts_extra_flags: false,
    },
    ActionSpec {
        kind: ActionKind::Info,
        upstream_class: "InfoAction",
        effects: INFO_EFFECTS,
        accepts_extra_flags: false,
    },
    ActionSpec {
        kind: ActionKind::New,
        upstream_class: "NewAction",
        effects: NEW_EFFECTS,
        accepts_extra_flags: false,
    },
    ActionSpec {
        kind: ActionKind::Start,
        upstream_class: "StartAction",
        effects: START_EFFECTS,
        accepts_extra_flags: true,
    },
];

pub fn action_specs() -> &'static [ActionSpec] {
    ACTION_SPECS
}

pub fn action_spec(kind: ActionKind) -> Option<&'static ActionSpec> {
    ACTION_SPECS.iter().find(|action| action.kind == kind)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn action_kind_maps_to_command_name() {
        assert_eq!(ActionKind::Generate.command(), CommandName::Generate);
        assert_eq!(ActionKind::Start.command().as_str(), "start");
    }

    #[test]
    fn captures_actions_that_accept_extra_flags() {
        let actions_with_extra_flags: Vec<ActionKind> = action_specs()
            .iter()
            .filter(|action| action.accepts_extra_flags)
            .map(|action| action.kind)
            .collect();

        assert_eq!(
            actions_with_extra_flags,
            [ActionKind::Add, ActionKind::Start]
        );
    }

    #[test]
    fn invocation_starts_empty_for_command() {
        let invocation = ActionInvocation::for_command(CommandName::Build);

        assert_eq!(invocation.kind, ActionKind::Build);
        assert!(invocation.inputs.is_empty());
        assert!(invocation.options.is_empty());
        assert!(invocation.extra_flags.is_empty());
    }
}