Skip to main content

cli/actions/
mod.rs

1//! Runtime actions for CLI commands.
2//!
3//! These are initial data models for upstream `nest-cli/actions/*.action.ts`.
4//! They describe which action a parsed command should invoke and the broad
5//! side effects each action performs.
6
7use crate::commands::{CommandName, Input};
8
9pub mod abstract_action;
10pub mod add_action;
11pub mod build_action;
12pub mod build_executor;
13pub mod generate_action;
14pub mod info_action;
15pub mod new_action;
16pub mod start_action;
17pub mod start_executor;
18
19/// Runtime action classes declared by upstream Nest CLI.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21pub enum ActionKind {
22    Add,
23    Build,
24    Generate,
25    Info,
26    New,
27    Start,
28}
29
30impl ActionKind {
31    pub const fn as_str(self) -> &'static str {
32        match self {
33            Self::Add => "add",
34            Self::Build => "build",
35            Self::Generate => "generate",
36            Self::Info => "info",
37            Self::New => "new",
38            Self::Start => "start",
39        }
40    }
41
42    pub const fn command(self) -> CommandName {
43        match self {
44            Self::Add => CommandName::Add,
45            Self::Build => CommandName::Build,
46            Self::Generate => CommandName::Generate,
47            Self::Info => CommandName::Info,
48            Self::New => CommandName::New,
49            Self::Start => CommandName::Start,
50        }
51    }
52}
53
54impl From<CommandName> for ActionKind {
55    fn from(command: CommandName) -> Self {
56        match command {
57            CommandName::Add => Self::Add,
58            CommandName::Build => Self::Build,
59            CommandName::Generate => Self::Generate,
60            CommandName::Info => Self::Info,
61            CommandName::New => Self::New,
62            CommandName::Start => Self::Start,
63        }
64    }
65}
66
67/// High-level side effects visible in upstream action implementations.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum ActionEffect {
70    ExecuteSchematic,
71    InstallPackage,
72    RunBuild,
73    SpawnApplication,
74    InitializeGitRepository,
75    WriteGitIgnore,
76    DisplayProjectInformation,
77    PromptForMissingInput,
78}
79
80/// Static descriptor for an upstream action class.
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub struct ActionSpec {
83    pub kind: ActionKind,
84    pub upstream_class: &'static str,
85    pub effects: &'static [ActionEffect],
86    pub accepts_extra_flags: bool,
87}
88
89/// Data passed to an action `handle` method after command parsing.
90#[derive(Clone, Debug, PartialEq, Eq)]
91pub struct ActionInvocation {
92    pub kind: ActionKind,
93    pub inputs: Vec<Input>,
94    pub options: Vec<Input>,
95    pub extra_flags: Vec<String>,
96}
97
98impl ActionInvocation {
99    pub fn new(kind: ActionKind) -> Self {
100        Self {
101            kind,
102            inputs: Vec::new(),
103            options: Vec::new(),
104            extra_flags: Vec::new(),
105        }
106    }
107
108    pub fn for_command(command: CommandName) -> Self {
109        Self::new(command.into())
110    }
111}
112
113const ADD_EFFECTS: &[ActionEffect] = &[
114    ActionEffect::InstallPackage,
115    ActionEffect::PromptForMissingInput,
116    ActionEffect::ExecuteSchematic,
117];
118
119const BUILD_EFFECTS: &[ActionEffect] = &[ActionEffect::RunBuild];
120
121const GENERATE_EFFECTS: &[ActionEffect] = &[
122    ActionEffect::PromptForMissingInput,
123    ActionEffect::ExecuteSchematic,
124];
125
126const INFO_EFFECTS: &[ActionEffect] = &[ActionEffect::DisplayProjectInformation];
127
128const NEW_EFFECTS: &[ActionEffect] = &[
129    ActionEffect::PromptForMissingInput,
130    ActionEffect::ExecuteSchematic,
131    ActionEffect::InstallPackage,
132    ActionEffect::InitializeGitRepository,
133    ActionEffect::WriteGitIgnore,
134];
135
136const START_EFFECTS: &[ActionEffect] = &[ActionEffect::RunBuild, ActionEffect::SpawnApplication];
137
138pub const ACTION_SPECS: &[ActionSpec] = &[
139    ActionSpec {
140        kind: ActionKind::Add,
141        upstream_class: "AddAction",
142        effects: ADD_EFFECTS,
143        accepts_extra_flags: true,
144    },
145    ActionSpec {
146        kind: ActionKind::Build,
147        upstream_class: "BuildAction",
148        effects: BUILD_EFFECTS,
149        accepts_extra_flags: false,
150    },
151    ActionSpec {
152        kind: ActionKind::Generate,
153        upstream_class: "GenerateAction",
154        effects: GENERATE_EFFECTS,
155        accepts_extra_flags: false,
156    },
157    ActionSpec {
158        kind: ActionKind::Info,
159        upstream_class: "InfoAction",
160        effects: INFO_EFFECTS,
161        accepts_extra_flags: false,
162    },
163    ActionSpec {
164        kind: ActionKind::New,
165        upstream_class: "NewAction",
166        effects: NEW_EFFECTS,
167        accepts_extra_flags: false,
168    },
169    ActionSpec {
170        kind: ActionKind::Start,
171        upstream_class: "StartAction",
172        effects: START_EFFECTS,
173        accepts_extra_flags: true,
174    },
175];
176
177pub fn action_specs() -> &'static [ActionSpec] {
178    ACTION_SPECS
179}
180
181pub fn action_spec(kind: ActionKind) -> Option<&'static ActionSpec> {
182    ACTION_SPECS.iter().find(|action| action.kind == kind)
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn action_kind_maps_to_command_name() {
191        assert_eq!(ActionKind::Generate.command(), CommandName::Generate);
192        assert_eq!(ActionKind::Start.command().as_str(), "start");
193    }
194
195    #[test]
196    fn captures_actions_that_accept_extra_flags() {
197        let actions_with_extra_flags: Vec<ActionKind> = action_specs()
198            .iter()
199            .filter(|action| action.accepts_extra_flags)
200            .map(|action| action.kind)
201            .collect();
202
203        assert_eq!(
204            actions_with_extra_flags,
205            [ActionKind::Add, ActionKind::Start]
206        );
207    }
208
209    #[test]
210    fn invocation_starts_empty_for_command() {
211        let invocation = ActionInvocation::for_command(CommandName::Build);
212
213        assert_eq!(invocation.kind, ActionKind::Build);
214        assert!(invocation.inputs.is_empty());
215        assert!(invocation.options.is_empty());
216        assert!(invocation.extra_flags.is_empty());
217    }
218}