use std::collections::HashMap;
use std::io;
use crate::model::ArgKind;
use crate::model::ArgSpec;
use crate::model::CliSpec;
use crate::model::CommandSpec;
use crate::model::OutputEncoding;
use crate::model::OutputMode;
use crate::model::OutputSchema;
pub(crate) fn inherited_globals<'a>(
inherited: &[&'a ArgSpec],
command: &'a CommandSpec,
) -> Vec<&'a ArgSpec> {
let mut globals = inherited.to_vec();
for arg in command.args.iter().filter(|arg| arg.global) {
if !globals.iter().any(|global| global.id == arg.id) {
globals.push(arg);
}
}
globals
}
pub(crate) fn combined_args<'a>(
inherited: &[&'a ArgSpec],
command: &'a CommandSpec,
) -> Vec<&'a ArgSpec> {
let mut args = inherited.to_vec();
for arg in &command.args {
if !args.iter().any(|existing| existing.id == arg.id) {
args.push(arg);
}
}
args
}
pub(crate) fn is_required(arg: &ArgSpec) -> bool {
arg.required
&& !matches!(
arg.kind,
ArgKind::FlagTrue | ArgKind::FlagFalse | ArgKind::Counter
)
}
pub(crate) fn is_grouped_repeated(arg: &ArgSpec) -> bool {
matches!(arg.kind, ArgKind::Option) && arg.value.repeated && arg.value.arity.allows_multiple()
}
pub(crate) fn option_token(arg: &ArgSpec) -> Option<String> {
arg.long
.as_ref()
.map(|long| format!("--{long}"))
.or_else(|| arg.short.map(|short| format!("-{short}")))
}
pub(crate) fn output_schema(schema: &OutputSchema) -> &str {
match schema {
OutputSchema::JsonSchema(schema) => schema,
}
}
pub(crate) fn output_encoding(encoding: OutputEncoding) -> &'static str {
match encoding {
OutputEncoding::Json => "json",
OutputEncoding::JsonLines => "json-lines",
OutputEncoding::Text => "text",
}
}
pub(crate) fn output_mode(mode: OutputMode) -> &'static str {
match mode {
OutputMode::Buffered => "buffered",
OutputMode::Streaming => "streaming",
OutputMode::Interactive => "interactive",
}
}
pub(crate) fn capitalize(input: &str) -> String {
let mut chars = input.chars();
match chars.next() {
Some(first) => format!(
"{}{}",
first.to_ascii_uppercase(),
chars.as_str().to_ascii_lowercase()
),
None => String::new(),
}
}
pub(crate) fn pascal_case(input: &str) -> String {
let output = words(input)
.iter()
.map(|word| capitalize(word))
.collect::<String>();
if output.is_empty() {
"Command".to_owned()
} else {
output
}
}
pub(crate) fn words(input: &str) -> Vec<String> {
let mut words = Vec::<String>::new();
let mut current = String::new();
let mut previous_was_lower_or_digit = false;
for character in input.chars() {
if !character.is_ascii_alphanumeric() {
if !current.is_empty() {
words.push(std::mem::take(&mut current));
}
previous_was_lower_or_digit = false;
continue;
}
if character.is_ascii_uppercase() && previous_was_lower_or_digit && !current.is_empty() {
words.push(std::mem::take(&mut current));
}
previous_was_lower_or_digit = character.is_ascii_lowercase() || character.is_ascii_digit();
current.push(character);
}
if !current.is_empty() {
words.push(current);
}
words
}
pub(crate) fn lower_join(input: &str, separator: &str) -> String {
words(input)
.iter()
.map(|word| word.to_ascii_lowercase())
.collect::<Vec<_>>()
.join(separator)
}
pub(crate) fn collapse_lines(input: &str) -> String {
input
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join(" ")
}
pub(crate) fn quote_double(input: &str) -> String {
let mut output = String::with_capacity(input.len() + 2);
output.push('"');
for character in input.chars() {
match character {
'\\' => output.push_str("\\\\"),
'"' => output.push_str("\\\""),
'\n' => output.push_str("\\n"),
'\r' => output.push_str("\\r"),
'\t' => output.push_str("\\t"),
_ => output.push(character),
}
}
output.push('"');
output
}
pub(crate) fn safe_identifier(input: &str, is_reserved: impl Fn(&str) -> bool) -> String {
let mut identifier = if input
.chars()
.next()
.is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
{
input.to_owned()
} else {
format!("_{input}")
};
if is_reserved(&identifier) {
identifier.push('_');
}
identifier
}
pub(crate) fn command_pieces<'a>(command: &'a CommandSpec, path: &'a [String]) -> Vec<&'a str> {
let mut pieces = if path.is_empty() {
vec![command.name.as_str()]
} else {
path.iter().map(String::as_str).collect::<Vec<_>>()
};
if pieces.is_empty() {
pieces.push("command");
}
pieces
}
pub(crate) fn command_type_prefix(command: &CommandSpec, path: &[String]) -> String {
command_pieces(command, path)
.iter()
.map(|piece| pascal_case(piece))
.collect::<String>()
}
pub(crate) fn ensure_unique_command_prefixes(spec: &CliSpec) -> io::Result<()> {
let mut by_prefix: HashMap<String, Vec<Vec<String>>> = HashMap::new();
let mut path = Vec::<String>::new();
collect_prefixes(&spec.root, &mut path, &mut by_prefix);
if let Some((prefix, paths)) = by_prefix.into_iter().find(|(_, paths)| paths.len() > 1) {
let descriptions = paths
.iter()
.map(|path| format!("[{}]", path.join(", ")))
.collect::<Vec<_>>()
.join(" and ");
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"command name collision: paths {descriptions} both render to `{prefix}`. \
Rename one of the subcommands so the generated type/function names are unique."
),
));
}
Ok(())
}
fn collect_prefixes(
command: &CommandSpec,
path: &mut Vec<String>,
out: &mut HashMap<String, Vec<Vec<String>>>,
) {
let prefix = command_type_prefix(command, path);
out.entry(prefix).or_default().push(path.clone());
for sub in &command.subcommands {
path.push(sub.name.clone());
collect_prefixes(sub, path, out);
path.pop();
}
}