use core::fmt;
use std::collections::{HashMap, HashSet};
use crate::actions::ActionInvocation;
pub mod abstract_command;
pub mod add_command;
pub mod build_command;
pub mod command_input;
pub mod command_loader;
pub mod generate_command;
pub mod info_command;
pub mod new_command;
pub mod start_command;
pub use command_input::{Input, InputOptions, InputValue};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum CommandName {
Add,
Build,
Generate,
Info,
New,
Start,
}
impl CommandName {
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",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CommandParseError {
MissingCommand,
UnknownCommand(String),
UnknownOption {
command: CommandName,
option: String,
},
MissingRequiredArgument {
command: CommandName,
argument: &'static str,
},
MissingRequiredOptionValue {
command: CommandName,
option: &'static str,
},
TooManyArguments {
command: CommandName,
extra: Vec<String>,
},
InvalidBuilder(String),
InvalidLanguage(String),
}
impl fmt::Display for CommandParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingCommand => formatter.write_str("missing command"),
Self::UnknownCommand(command) => write!(formatter, "invalid command: {command}"),
Self::UnknownOption { command, option } => {
write!(formatter, "unknown option for {command}: {option}")
}
Self::MissingRequiredArgument { command, argument } => {
write!(
formatter,
"missing required argument {argument} for {command}"
)
}
Self::MissingRequiredOptionValue { command, option } => {
write!(
formatter,
"missing required value for {command} option --{option}"
)
}
Self::TooManyArguments { command, extra } => write!(
formatter,
"too many arguments for {command}: {}",
extra.join(", ")
),
Self::InvalidBuilder(builder) => write!(
formatter,
"Invalid builder option: {builder}. Available builder: cargo"
),
Self::InvalidLanguage(language) => write!(
formatter,
"Invalid language \"{language}\" selected. Available language is \"rust\""
),
}
}
}
impl std::error::Error for CommandParseError {}
impl fmt::Display for CommandName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ArgumentArity {
Required,
Optional,
OptionalVariadic,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CommandArgumentSpec {
pub name: &'static str,
pub arity: ArgumentArity,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OptionValueKind {
Bool,
OptionalString,
RequiredString,
StringList,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OptionDefault {
None,
Bool(bool),
String(&'static str),
StringListEmpty,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CommandOptionSpec {
pub short: Option<char>,
pub long: &'static str,
pub input_name: &'static str,
pub value_kind: OptionValueKind,
pub default: OptionDefault,
pub negates: Option<&'static str>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CommandSpec {
pub name: CommandName,
pub signature: &'static str,
pub aliases: &'static [&'static str],
pub description: &'static str,
pub usage: Option<&'static str>,
pub allow_unknown_options: bool,
pub args: &'static [CommandArgumentSpec],
pub options: &'static [CommandOptionSpec],
}
impl CommandSpec {
pub fn option(self, long: &str) -> Option<&'static CommandOptionSpec> {
self.options.iter().find(|option| option.long == long)
}
}
const ADD_ARGS: &[CommandArgumentSpec] = &[CommandArgumentSpec {
name: "library",
arity: ArgumentArity::Required,
}];
const ADD_OPTIONS: &[CommandOptionSpec] = &[
CommandOptionSpec {
short: Some('d'),
long: "dry-run",
input_name: "dry-run",
value_kind: OptionValueKind::Bool,
default: OptionDefault::None,
negates: None,
},
CommandOptionSpec {
short: Some('s'),
long: "skip-install",
input_name: "skip-install",
value_kind: OptionValueKind::Bool,
default: OptionDefault::Bool(false),
negates: None,
},
CommandOptionSpec {
short: Some('p'),
long: "project",
input_name: "project",
value_kind: OptionValueKind::OptionalString,
default: OptionDefault::None,
negates: None,
},
];
const BUILD_ARGS: &[CommandArgumentSpec] = &[CommandArgumentSpec {
name: "apps",
arity: ArgumentArity::OptionalVariadic,
}];
const BUILD_OPTIONS: &[CommandOptionSpec] = &[
option('c', "config", "config", OptionValueKind::OptionalString),
option('p', "path", "path", OptionValueKind::OptionalString),
option('w', "watch", "watch", OptionValueKind::Bool),
option('b', "builder", "builder", OptionValueKind::OptionalString),
option_no_short("watchAssets", "watchAssets", OptionValueKind::Bool),
option_no_short("webpack", "webpack", OptionValueKind::Bool),
option_no_short("type-check", "typeCheck", OptionValueKind::Bool),
option_no_short(
"webpackPath",
"webpackPath",
OptionValueKind::OptionalString,
),
option_no_short("tsc", "tsc", OptionValueKind::Bool),
option_no_short(
"preserveWatchOutput",
"preserveWatchOutput",
OptionValueKind::Bool,
),
option_no_short("all", "all", OptionValueKind::Bool),
];
const GENERATE_ARGS: &[CommandArgumentSpec] = &[
CommandArgumentSpec {
name: "schematic",
arity: ArgumentArity::Required,
},
CommandArgumentSpec {
name: "name",
arity: ArgumentArity::Optional,
},
CommandArgumentSpec {
name: "path",
arity: ArgumentArity::Optional,
},
];
const GENERATE_OPTIONS: &[CommandOptionSpec] = &[
option('d', "dry-run", "dry-run", OptionValueKind::Bool),
option('p', "project", "project", OptionValueKind::OptionalString),
CommandOptionSpec {
short: None,
long: "flat",
input_name: "flat",
value_kind: OptionValueKind::Bool,
default: OptionDefault::None,
negates: None,
},
CommandOptionSpec {
short: None,
long: "no-flat",
input_name: "flat",
value_kind: OptionValueKind::Bool,
default: OptionDefault::None,
negates: Some("flat"),
},
CommandOptionSpec {
short: None,
long: "spec",
input_name: "spec",
value_kind: OptionValueKind::Bool,
default: OptionDefault::Bool(true),
negates: None,
},
option_no_short(
"spec-file-suffix",
"specFileSuffix",
OptionValueKind::OptionalString,
),
CommandOptionSpec {
short: None,
long: "skip-import",
input_name: "skipImport",
value_kind: OptionValueKind::Bool,
default: OptionDefault::Bool(false),
negates: None,
},
CommandOptionSpec {
short: None,
long: "no-spec",
input_name: "spec",
value_kind: OptionValueKind::Bool,
default: OptionDefault::None,
negates: Some("spec"),
},
option(
'c',
"collection",
"collection",
OptionValueKind::OptionalString,
),
option_no_short("type", "type", OptionValueKind::RequiredString),
option_no_short("crud", "crud", OptionValueKind::OptionalString),
];
const INFO_ARGS: &[CommandArgumentSpec] = &[];
const INFO_OPTIONS: &[CommandOptionSpec] = &[];
const NEW_ARGS: &[CommandArgumentSpec] = &[CommandArgumentSpec {
name: "name",
arity: ArgumentArity::Optional,
}];
const NEW_OPTIONS: &[CommandOptionSpec] = &[
option_no_short("directory", "directory", OptionValueKind::OptionalString),
CommandOptionSpec {
short: Some('d'),
long: "dry-run",
input_name: "dry-run",
value_kind: OptionValueKind::Bool,
default: OptionDefault::Bool(false),
negates: None,
},
CommandOptionSpec {
short: Some('g'),
long: "skip-git",
input_name: "skip-git",
value_kind: OptionValueKind::Bool,
default: OptionDefault::Bool(false),
negates: None,
},
CommandOptionSpec {
short: Some('s'),
long: "skip-install",
input_name: "skip-install",
value_kind: OptionValueKind::Bool,
default: OptionDefault::Bool(false),
negates: None,
},
option(
'p',
"package-manager",
"packageManager",
OptionValueKind::OptionalString,
),
CommandOptionSpec {
short: Some('l'),
long: "language",
input_name: "language",
value_kind: OptionValueKind::OptionalString,
default: OptionDefault::String("Rust"),
negates: None,
},
CommandOptionSpec {
short: Some('c'),
long: "collection",
input_name: "collection",
value_kind: OptionValueKind::OptionalString,
default: OptionDefault::String("@nestrs/schematics"),
negates: None,
},
CommandOptionSpec {
short: None,
long: "strict",
input_name: "strict",
value_kind: OptionValueKind::Bool,
default: OptionDefault::Bool(false),
negates: None,
},
];
const START_ARGS: &[CommandArgumentSpec] = &[CommandArgumentSpec {
name: "app",
arity: ArgumentArity::Optional,
}];
const START_OPTIONS: &[CommandOptionSpec] = &[
option('c', "config", "config", OptionValueKind::OptionalString),
option('p', "path", "path", OptionValueKind::OptionalString),
option('w', "watch", "watch", OptionValueKind::Bool),
option('b', "builder", "builder", OptionValueKind::OptionalString),
option_no_short("watchAssets", "watchAssets", OptionValueKind::Bool),
option('d', "debug", "debug", OptionValueKind::OptionalString),
option_no_short("webpack", "webpack", OptionValueKind::Bool),
option_no_short(
"webpackPath",
"webpackPath",
OptionValueKind::OptionalString,
),
option_no_short("type-check", "typeCheck", OptionValueKind::Bool),
option_no_short("tsc", "tsc", OptionValueKind::Bool),
option_no_short("sourceRoot", "sourceRoot", OptionValueKind::OptionalString),
option_no_short("entryFile", "entryFile", OptionValueKind::OptionalString),
option('e', "exec", "exec", OptionValueKind::OptionalString),
option_no_short(
"preserveWatchOutput",
"preserveWatchOutput",
OptionValueKind::Bool,
),
CommandOptionSpec {
short: None,
long: "shell",
input_name: "shell",
value_kind: OptionValueKind::Bool,
default: OptionDefault::Bool(true),
negates: None,
},
CommandOptionSpec {
short: None,
long: "no-shell",
input_name: "shell",
value_kind: OptionValueKind::Bool,
default: OptionDefault::None,
negates: Some("shell"),
},
CommandOptionSpec {
short: None,
long: "env-file",
input_name: "envFile",
value_kind: OptionValueKind::StringList,
default: OptionDefault::StringListEmpty,
negates: None,
},
];
pub const COMMAND_SPECS: &[CommandSpec] = &[
CommandSpec {
name: CommandName::New,
signature: "new [name]",
aliases: &["n"],
description: "Generate Nest application.",
usage: None,
allow_unknown_options: false,
args: NEW_ARGS,
options: NEW_OPTIONS,
},
CommandSpec {
name: CommandName::Build,
signature: "build [apps...]",
aliases: &[],
description: "Build Nest application.",
usage: None,
allow_unknown_options: false,
args: BUILD_ARGS,
options: BUILD_OPTIONS,
},
CommandSpec {
name: CommandName::Start,
signature: "start [app]",
aliases: &[],
description: "Run Nest application.",
usage: None,
allow_unknown_options: true,
args: START_ARGS,
options: START_OPTIONS,
},
CommandSpec {
name: CommandName::Info,
signature: "info",
aliases: &["i"],
description: "Display Nest project details.",
usage: None,
allow_unknown_options: false,
args: INFO_ARGS,
options: INFO_OPTIONS,
},
CommandSpec {
name: CommandName::Add,
signature: "add <library>",
aliases: &[],
description: "Adds support for an external library to your project.",
usage: Some("<library> [options] [library-specific-options]"),
allow_unknown_options: true,
args: ADD_ARGS,
options: ADD_OPTIONS,
},
CommandSpec {
name: CommandName::Generate,
signature: "generate <schematic> [name] [path]",
aliases: &["g"],
description: "Generate a Nest element.",
usage: None,
allow_unknown_options: false,
args: GENERATE_ARGS,
options: GENERATE_OPTIONS,
},
];
pub fn command_specs() -> &'static [CommandSpec] {
COMMAND_SPECS
}
pub fn command_spec(name: CommandName) -> Option<&'static CommandSpec> {
COMMAND_SPECS.iter().find(|command| command.name == name)
}
pub fn resolve_command_name(name_or_alias: &str) -> Option<CommandName> {
COMMAND_SPECS
.iter()
.find(|command| {
command.name.as_str() == name_or_alias || command.aliases.contains(&name_or_alias)
})
.map(|command| command.name)
}
pub fn resolve_command_spec(name_or_alias: &str) -> Option<&'static CommandSpec> {
resolve_command_name(name_or_alias).and_then(command_spec)
}
pub fn load_command_invocation<I, S>(args: I) -> Result<ActionInvocation, CommandParseError>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let args: Vec<String> = args
.into_iter()
.map(|argument| argument.as_ref().to_owned())
.collect();
let (command_name, rest) = args
.split_first()
.ok_or(CommandParseError::MissingCommand)?;
let spec = resolve_command_spec(command_name)
.ok_or_else(|| CommandParseError::UnknownCommand(command_name.clone()))?;
let parsed = parse_tokens(spec, rest)?;
match spec.name {
CommandName::Add => build_add_invocation(parsed),
CommandName::Build => build_build_invocation(parsed),
CommandName::Generate => build_generate_invocation(parsed),
CommandName::Info => build_info_invocation(parsed),
CommandName::New => build_new_invocation(parsed),
CommandName::Start => build_start_invocation(parsed),
}
}
#[derive(Clone, Debug)]
struct ParsedCommand {
positionals: Vec<String>,
options: HashMap<&'static str, InputValue>,
explicit_options: HashSet<&'static str>,
extra_flags: Vec<String>,
}
fn parse_tokens(
spec: &'static CommandSpec,
tokens: &[String],
) -> Result<ParsedCommand, CommandParseError> {
let mut parsed = ParsedCommand {
positionals: Vec::new(),
options: HashMap::new(),
explicit_options: HashSet::new(),
extra_flags: Vec::new(),
};
let mut index = 0;
while index < tokens.len() {
let token = &tokens[index];
if token == "--" {
if spec.allow_unknown_options {
parsed
.extra_flags
.extend(tokens[index + 1..].iter().cloned());
} else {
parsed
.positionals
.extend(tokens[index + 1..].iter().cloned());
}
break;
}
if let Some(raw) = token.strip_prefix("--") {
let (long, inline_value) = split_long_option(raw);
if let Some(option) = spec.option(long) {
index = parse_known_option(spec, option, inline_value, tokens, index, &mut parsed)?;
} else if spec.allow_unknown_options {
parsed.extra_flags.push(token.clone());
if inline_value.is_none()
&& index + 1 < tokens.len()
&& !tokens[index + 1].starts_with('-')
{
index += 1;
parsed.extra_flags.push(tokens[index].clone());
}
} else {
return Err(CommandParseError::UnknownOption {
command: spec.name,
option: token.clone(),
});
}
} else if token.starts_with('-') && token.len() > 1 {
let (short, inline_value) = split_short_option(token);
if let Some(option) = spec
.options
.iter()
.find(|option| option.short == Some(short))
{
index = parse_known_option(spec, option, inline_value, tokens, index, &mut parsed)?;
} else if spec.allow_unknown_options {
parsed.extra_flags.push(token.clone());
if inline_value.is_none()
&& index + 1 < tokens.len()
&& !tokens[index + 1].starts_with('-')
{
index += 1;
parsed.extra_flags.push(tokens[index].clone());
}
} else {
return Err(CommandParseError::UnknownOption {
command: spec.name,
option: token.clone(),
});
}
} else {
parsed.positionals.push(token.clone());
}
index += 1;
}
validate_positionals(spec, &parsed.positionals)?;
Ok(parsed)
}
fn parse_known_option(
spec: &CommandSpec,
option: &'static CommandOptionSpec,
inline_value: Option<String>,
tokens: &[String],
index: usize,
parsed: &mut ParsedCommand,
) -> Result<usize, CommandParseError> {
let mut next_index = index;
let value = match option.value_kind {
OptionValueKind::Bool => InputValue::Bool(option.negates.is_none()),
OptionValueKind::OptionalString => {
if let Some(value) = inline_value {
InputValue::String(value)
} else if index + 1 < tokens.len() && !tokens[index + 1].starts_with('-') {
next_index += 1;
InputValue::String(tokens[next_index].clone())
} else {
InputValue::Bool(true)
}
}
OptionValueKind::RequiredString => {
if let Some(value) = inline_value {
InputValue::String(value)
} else if index + 1 < tokens.len() && !tokens[index + 1].starts_with('-') {
next_index += 1;
InputValue::String(tokens[next_index].clone())
} else {
return Err(CommandParseError::MissingRequiredOptionValue {
command: spec.name,
option: option.long,
});
}
}
OptionValueKind::StringList => {
let value = if let Some(value) = inline_value {
value
} else if index + 1 < tokens.len() && !tokens[index + 1].starts_with('-') {
next_index += 1;
tokens[next_index].clone()
} else {
String::from("true")
};
let mut values = match parsed.options.remove(option.input_name) {
Some(InputValue::StringList(values)) => values,
_ => Vec::new(),
};
values.push(value);
InputValue::StringList(values)
}
};
parsed.options.insert(option.input_name, value);
parsed.explicit_options.insert(option.input_name);
Ok(next_index)
}
fn split_long_option(raw: &str) -> (&str, Option<String>) {
raw.split_once('=')
.map(|(name, value)| (name, Some(value.to_owned())))
.unwrap_or((raw, None))
}
fn split_short_option(raw: &str) -> (char, Option<String>) {
let short = raw.chars().nth(1).expect("short option marker");
let rest = &raw[2..];
let value = if rest.is_empty() {
None
} else {
Some(rest.trim_start_matches('=').to_owned())
};
(short, value)
}
fn validate_positionals(
spec: &CommandSpec,
positionals: &[String],
) -> Result<(), CommandParseError> {
let required_count = spec
.args
.iter()
.filter(|argument| argument.arity == ArgumentArity::Required)
.count();
if positionals.len() < required_count {
let argument = spec.args[positionals.len()].name;
return Err(CommandParseError::MissingRequiredArgument {
command: spec.name,
argument,
});
}
let has_variadic = spec
.args
.iter()
.any(|argument| argument.arity == ArgumentArity::OptionalVariadic);
if !has_variadic && positionals.len() > spec.args.len() {
return Err(CommandParseError::TooManyArguments {
command: spec.name,
extra: positionals[spec.args.len()..].to_vec(),
});
}
Ok(())
}
fn build_add_invocation(parsed: ParsedCommand) -> Result<ActionInvocation, CommandParseError> {
let mut invocation = ActionInvocation::for_command(CommandName::Add);
invocation.inputs.push(Input::new(
"library",
string_positional(&parsed, 0).map(InputValue::String),
));
invocation.options.push(Input::new(
"dry-run",
Some(InputValue::Bool(bool_option(&parsed, "dry-run"))),
));
invocation.options.push(Input::new(
"skip-install",
Some(InputValue::Bool(bool_option(&parsed, "skip-install"))),
));
invocation
.options
.push(Input::new("project", option_value(&parsed, "project")));
invocation.extra_flags = parsed.extra_flags;
Ok(invocation)
}
fn build_build_invocation(parsed: ParsedCommand) -> Result<ActionInvocation, CommandParseError> {
let mut invocation = ActionInvocation::for_command(CommandName::Build);
if parsed.positionals.is_empty() {
invocation.inputs.push(Input::new("app", None));
} else {
invocation.inputs.extend(
parsed
.positionals
.iter()
.cloned()
.map(|app| Input::new("app", Some(InputValue::String(app)))),
);
}
let is_webpack_enabled = !bool_option(&parsed, "tsc") && bool_option(&parsed, "webpack");
invocation
.options
.push(Input::new("config", option_value(&parsed, "config")));
invocation.options.push(Input::new(
"webpack",
Some(InputValue::Bool(is_webpack_enabled)),
));
invocation.options.push(Input::new(
"watch",
Some(InputValue::Bool(bool_option(&parsed, "watch"))),
));
invocation.options.push(Input::new(
"watchAssets",
Some(InputValue::Bool(bool_option(&parsed, "watchAssets"))),
));
invocation
.options
.push(Input::new("path", option_value(&parsed, "path")));
invocation.options.push(Input::new(
"webpackPath",
option_value(&parsed, "webpackPath"),
));
push_validated_builder(&mut invocation, &parsed)?;
invocation
.options
.push(Input::new("typeCheck", option_value(&parsed, "typeCheck")));
invocation.options.push(Input::new(
"preserveWatchOutput",
Some(InputValue::Bool(
bool_option(&parsed, "preserveWatchOutput")
&& bool_option(&parsed, "watch")
&& !is_webpack_enabled,
)),
));
invocation.options.push(Input::new(
"all",
Some(InputValue::Bool(bool_option(&parsed, "all"))),
));
Ok(invocation)
}
fn build_generate_invocation(parsed: ParsedCommand) -> Result<ActionInvocation, CommandParseError> {
let mut invocation = ActionInvocation::for_command(CommandName::Generate);
invocation.inputs.push(Input::new(
"schematic",
string_positional(&parsed, 0).map(InputValue::String),
));
invocation.inputs.push(Input::new(
"name",
string_positional(&parsed, 1).map(InputValue::String),
));
invocation.inputs.push(Input::new(
"path",
string_positional(&parsed, 2).map(InputValue::String),
));
invocation.options.push(Input::new(
"dry-run",
Some(InputValue::Bool(bool_option(&parsed, "dry-run"))),
));
if parsed.explicit_options.contains("flat") {
invocation
.options
.push(Input::new("flat", option_value(&parsed, "flat")));
}
invocation.options.push(Input::with_options(
"spec",
Some(InputValue::Bool(bool_option_default(&parsed, "spec", true))),
InputOptions {
passed_as_input: parsed.explicit_options.contains("spec"),
},
));
invocation.options.push(Input::new(
"specFileSuffix",
option_value(&parsed, "specFileSuffix"),
));
invocation.options.push(Input::new(
"collection",
option_value(&parsed, "collection"),
));
invocation
.options
.push(Input::new("project", option_value(&parsed, "project")));
invocation.options.push(Input::new(
"skipImport",
Some(InputValue::Bool(bool_option(&parsed, "skipImport"))),
));
invocation
.options
.push(Input::new("type", option_value(&parsed, "type")));
if let Some(value) = option_value(&parsed, "crud") {
let crud_enabled = match &value {
InputValue::Bool(value) => *value,
InputValue::String(value) => value == "true",
InputValue::StringList(values) => !values.is_empty(),
};
invocation
.options
.push(Input::new("crud", Some(InputValue::Bool(crud_enabled))));
}
Ok(invocation)
}
fn build_info_invocation(parsed: ParsedCommand) -> Result<ActionInvocation, CommandParseError> {
let mut invocation = ActionInvocation::for_command(CommandName::Info);
invocation.extra_flags = parsed.extra_flags;
Ok(invocation)
}
fn build_new_invocation(parsed: ParsedCommand) -> Result<ActionInvocation, CommandParseError> {
let mut invocation = ActionInvocation::for_command(CommandName::New);
invocation.inputs.push(Input::new(
"name",
string_positional(&parsed, 0).map(InputValue::String),
));
invocation
.options
.push(Input::new("directory", option_value(&parsed, "directory")));
invocation.options.push(Input::new(
"dry-run",
Some(InputValue::Bool(bool_option(&parsed, "dry-run"))),
));
invocation.options.push(Input::new(
"skip-git",
Some(InputValue::Bool(bool_option(&parsed, "skip-git"))),
));
invocation.options.push(Input::new(
"skip-install",
Some(InputValue::Bool(bool_option(&parsed, "skip-install"))),
));
invocation.options.push(Input::new(
"strict",
Some(InputValue::Bool(bool_option(&parsed, "strict"))),
));
invocation.options.push(Input::new(
"packageManager",
option_value(&parsed, "packageManager"),
));
invocation.options.push(Input::new(
"collection",
Some(
option_value(&parsed, "collection")
.unwrap_or_else(|| InputValue::String(String::from("@nestrs/schematics"))),
),
));
invocation.options.push(Input::new(
"language",
Some(InputValue::String(normalize_language(option_value(
&parsed, "language",
))?)),
));
Ok(invocation)
}
fn build_start_invocation(parsed: ParsedCommand) -> Result<ActionInvocation, CommandParseError> {
let mut invocation = ActionInvocation::for_command(CommandName::Start);
invocation.inputs.push(Input::new(
"app",
string_positional(&parsed, 0).map(InputValue::String),
));
let is_webpack_enabled = !bool_option(&parsed, "tsc") && bool_option(&parsed, "webpack");
invocation
.options
.push(Input::new("config", option_value(&parsed, "config")));
invocation.options.push(Input::new(
"webpack",
Some(InputValue::Bool(is_webpack_enabled)),
));
invocation
.options
.push(Input::new("debug", option_value(&parsed, "debug")));
invocation.options.push(Input::new(
"watch",
Some(InputValue::Bool(bool_option(&parsed, "watch"))),
));
invocation.options.push(Input::new(
"watchAssets",
Some(InputValue::Bool(bool_option(&parsed, "watchAssets"))),
));
invocation
.options
.push(Input::new("path", option_value(&parsed, "path")));
invocation.options.push(Input::new(
"webpackPath",
option_value(&parsed, "webpackPath"),
));
invocation
.options
.push(Input::new("exec", option_value(&parsed, "exec")));
invocation.options.push(Input::new(
"sourceRoot",
option_value(&parsed, "sourceRoot"),
));
invocation
.options
.push(Input::new("entryFile", option_value(&parsed, "entryFile")));
invocation.options.push(Input::new(
"preserveWatchOutput",
Some(InputValue::Bool(
bool_option(&parsed, "preserveWatchOutput")
&& bool_option(&parsed, "watch")
&& !is_webpack_enabled,
)),
));
invocation.options.push(Input::new(
"shell",
Some(InputValue::Bool(bool_option_default(
&parsed, "shell", true,
))),
));
invocation.options.push(Input::new(
"envFile",
Some(
option_value(&parsed, "envFile").unwrap_or_else(|| InputValue::StringList(Vec::new())),
),
));
push_validated_builder(&mut invocation, &parsed)?;
invocation
.options
.push(Input::new("typeCheck", option_value(&parsed, "typeCheck")));
invocation.extra_flags = parsed.extra_flags;
Ok(invocation)
}
fn push_validated_builder(
invocation: &mut ActionInvocation,
parsed: &ParsedCommand,
) -> Result<(), CommandParseError> {
if let Some(builder) = option_value(parsed, "builder") {
match &builder {
InputValue::String(value) if value == "cargo" => {
invocation
.options
.push(Input::new("builder", Some(builder)));
Ok(())
}
value => Err(CommandParseError::InvalidBuilder(value_to_string(value))),
}
} else {
invocation.options.push(Input::new("builder", None));
Ok(())
}
}
fn normalize_language(value: Option<InputValue>) -> Result<String, CommandParseError> {
let language = match value {
Some(InputValue::String(value)) => value,
Some(value) => value_to_string(&value),
None => String::from("Rust"),
};
match language.to_lowercase().as_str() {
"rust" | "rs" => Ok(String::from("rs")),
_ => Err(CommandParseError::InvalidLanguage(language)),
}
}
fn option_value(parsed: &ParsedCommand, name: &'static str) -> Option<InputValue> {
parsed.options.get(name).cloned()
}
fn bool_option(parsed: &ParsedCommand, name: &'static str) -> bool {
bool_option_default(parsed, name, false)
}
fn bool_option_default(parsed: &ParsedCommand, name: &'static str, default: bool) -> bool {
match parsed.options.get(name) {
Some(InputValue::Bool(value)) => *value,
Some(InputValue::String(value)) => value == "true",
Some(InputValue::StringList(values)) => !values.is_empty(),
None => default,
}
}
fn string_positional(parsed: &ParsedCommand, index: usize) -> Option<String> {
parsed.positionals.get(index).cloned()
}
fn value_to_string(value: &InputValue) -> String {
match value {
InputValue::Bool(value) => value.to_string(),
InputValue::String(value) => value.clone(),
InputValue::StringList(values) => values.join(","),
}
}
const fn option(
short: char,
long: &'static str,
input_name: &'static str,
value_kind: OptionValueKind,
) -> CommandOptionSpec {
CommandOptionSpec {
short: Some(short),
long,
input_name,
value_kind,
default: OptionDefault::None,
negates: None,
}
}
const fn option_no_short(
long: &'static str,
input_name: &'static str,
value_kind: OptionValueKind,
) -> CommandOptionSpec {
CommandOptionSpec {
short: None,
long,
input_name,
value_kind,
default: OptionDefault::None,
negates: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn captures_upstream_command_names_in_loader_order() {
let names: Vec<&str> = command_specs()
.iter()
.map(|command| command.name.as_str())
.collect();
assert_eq!(names, ["new", "build", "start", "info", "add", "generate"]);
}
#[test]
fn captures_generate_negated_options() {
let generate = command_spec(CommandName::Generate).expect("generate command");
assert_eq!(generate.aliases, ["g"]);
assert_eq!(
generate.option("no-spec").map(|option| option.negates),
Some(Some("spec"))
);
assert_eq!(
generate.option("no-flat").map(|option| option.negates),
Some(Some("flat"))
);
}
#[test]
fn captures_start_common_runtime_options() {
let start = command_spec(CommandName::Start).expect("start command");
assert!(start.allow_unknown_options);
assert_eq!(
start.option("env-file").map(|option| option.value_kind),
Some(OptionValueKind::StringList)
);
assert_eq!(
start.option("shell").map(|option| option.default),
Some(OptionDefault::Bool(true))
);
}
}