use clap::builder::{TypedValueParser, ValueParser};
use clap::error::ErrorKind;
use clap::{ArgMatches, Args, CommandFactory, FromArgMatches, Parser, Subcommand};
use ortho_config::localize_clap_error_with_command;
use ortho_config::{LocalizationArgs, Localizer};
use serde::{Deserialize, Serialize};
use std::ffi::OsString;
use std::path::PathBuf;
use std::sync::Arc;
use super::config::CliConfig;
use super::parsing::{
parse_accessibility_policy, parse_color_policy, parse_emoji_policy, parse_host_pattern,
parse_jobs, parse_locale, parse_progress_policy, parse_scheme,
};
use super::{AccessibilityPolicy, ColourPolicy, EmojiPolicy, ProgressPolicy};
use crate::cli_l10n::localize_command;
pub use crate::cli_l10n::{json_hint_from_args, locale_hint_from_args};
use crate::host_pattern::HostPattern;
use crate::theme::ThemePreference;
#[derive(Clone)]
struct LocalizedValueParser<F> {
localizer: Arc<dyn Localizer>,
parser: F,
}
impl<F> LocalizedValueParser<F> {
fn new(localizer: Arc<dyn Localizer>, parser: F) -> Self {
Self { localizer, parser }
}
}
impl<F, T> TypedValueParser for LocalizedValueParser<F>
where
F: Fn(&dyn Localizer, &str) -> Result<T, String> + Clone + Send + Sync + 'static,
T: Send + Sync + Clone + 'static,
{
type Value = T;
fn parse_ref(
&self,
cmd: &clap::Command,
_arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let mut command = cmd.clone();
let Some(raw_value) = value.to_str() else {
return Err(command.error(ErrorKind::InvalidUtf8, "invalid UTF-8"));
};
(self.parser)(self.localizer.as_ref(), raw_value)
.map_err(|err| command.error(ErrorKind::ValueValidation, err))
}
}
pub(super) fn validation_message(
localizer: &dyn Localizer,
key: &'static str,
args: Option<&LocalizationArgs<'_>>,
fallback: &str,
) -> String {
localizer.message(key, args, fallback)
}
#[derive(Debug, Parser, Serialize, Deserialize)]
#[command(name = "netsuke", author, version, about, long_about = None)]
pub struct Cli {
#[arg(
short,
long,
value_name = "FILE",
default_value_os_t = CliConfig::default_manifest_path()
)]
pub file: PathBuf,
#[arg(short = 'C', long, value_name = "DIR")]
pub directory: Option<PathBuf>,
#[arg(long, value_name = "FILE")]
#[serde(skip)]
pub config: Option<PathBuf>,
#[arg(short, long, value_name = "N")]
pub jobs: Option<usize>,
#[arg(short, long)]
pub verbose: bool,
#[arg(long, value_name = "LOCALE")]
pub locale: Option<String>,
#[arg(long = "fetch-allow-scheme", value_name = "SCHEME")]
pub fetch_allow_scheme: Vec<String>,
#[arg(long = "fetch-allow-host", value_name = "HOST")]
pub fetch_allow_host: Vec<HostPattern>,
#[arg(long = "fetch-block-host", value_name = "HOST")]
pub fetch_block_host: Vec<HostPattern>,
#[arg(long = "fetch-default-deny")]
pub fetch_default_deny: bool,
#[arg(long)]
pub json: bool,
#[command(flatten)]
pub interaction: InteractionArgs,
#[arg(long, value_name = "POLICY", default_value_t)]
pub color: ColourPolicy,
#[arg(long, value_name = "POLICY", default_value_t)]
pub emoji: EmojiPolicy,
#[arg(long, value_name = "POLICY", default_value_t)]
pub progress: ProgressPolicy,
#[arg(long, value_name = "POLICY", default_value_t)]
pub accessibility: AccessibilityPolicy,
#[arg(long = "default-target", value_name = "TARGET")]
pub default_targets: Vec<String>,
#[serde(skip)]
#[command(subcommand)]
pub command: Option<Commands>,
}
impl Cli {
#[must_use]
pub fn with_default_command(mut self) -> Self {
if self.command.is_none() {
self.command = Some(Commands::Build(BuildArgs::default()));
}
self
}
#[must_use]
pub const fn theme_preference(&self) -> Option<ThemePreference> {
match self.emoji {
EmojiPolicy::Auto => None,
EmojiPolicy::Always => Some(ThemePreference::Unicode),
EmojiPolicy::Never => Some(ThemePreference::Ascii),
}
}
#[must_use]
pub const fn accessibility_override(&self) -> Option<bool> {
match self.accessibility {
AccessibilityPolicy::Auto => None,
AccessibilityPolicy::On => Some(true),
AccessibilityPolicy::Off => Some(false),
}
}
#[must_use]
pub const fn no_input(&self) -> bool {
self.interaction.no_input
}
#[must_use]
pub const fn progress_enabled(&self) -> bool {
!matches!(self.progress, ProgressPolicy::Never)
}
}
impl Default for Cli {
fn default() -> Self {
Self {
file: CliConfig::default_manifest_path(),
directory: None,
config: None,
jobs: None,
verbose: false,
locale: None,
fetch_allow_scheme: Vec::new(),
fetch_allow_host: Vec::new(),
fetch_block_host: Vec::new(),
fetch_default_deny: false,
json: false,
interaction: InteractionArgs::default(),
color: ColourPolicy::Auto,
emoji: EmojiPolicy::Auto,
progress: ProgressPolicy::Auto,
accessibility: AccessibilityPolicy::Auto,
default_targets: Vec::new(),
command: None,
}
.with_default_command()
}
}
#[derive(Debug, Args, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct InteractionArgs {
#[arg(long, default_value_t = true)]
pub no_input: bool,
}
impl Default for InteractionArgs {
fn default() -> Self {
Self { no_input: true }
}
}
#[derive(Debug, Args, PartialEq, Eq, Clone, Serialize, Deserialize, Default)]
pub struct BuildArgs {
#[serde(default)]
pub targets: Vec<String>,
}
#[derive(Debug, Args, PartialEq, Eq, Clone, Serialize, Deserialize, Default)]
pub struct GraphArgs {
#[arg(long)]
#[serde(skip)]
pub html: bool,
#[arg(long, value_name = "FILE")]
#[serde(skip)]
pub output: Option<PathBuf>,
}
#[derive(Debug, Subcommand, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Commands {
Build(BuildArgs),
Clean,
Graph(GraphArgs),
Generate {
#[arg(long, value_name = "FILE")]
output: Option<PathBuf>,
},
}
pub fn parse_with_localizer_from<I, T>(
iter: I,
localizer: &Arc<dyn Localizer>,
) -> Result<(Cli, ArgMatches), clap::Error>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let mut command = localize_command(Cli::command(), localizer.as_ref());
command = configure_validation_parsers(command, localizer);
let matches = command
.try_get_matches_from_mut(iter)
.map_err(|err| localize_clap_error_with_command(err, localizer.as_ref(), Some(&command)))?;
let matches_for_merge = matches.clone();
let mut matches_for_parse = matches;
let cli = Cli::from_arg_matches_mut(&mut matches_for_parse).map_err(|clap_err| {
let with_cmd = clap_err.with_cmd(&command);
localize_clap_error_with_command(with_cmd, localizer.as_ref(), Some(&command))
})?;
Ok((cli, matches_for_merge))
}
fn configure_validation_parsers(
mut command: clap::Command,
localizer: &Arc<dyn Localizer>,
) -> clap::Command {
let jobs_parser = LocalizedValueParser::new(Arc::clone(localizer), parse_jobs);
let locale_parser = LocalizedValueParser::new(Arc::clone(localizer), parse_locale);
let scheme_parser = LocalizedValueParser::new(Arc::clone(localizer), parse_scheme);
let host_parser = LocalizedValueParser::new(Arc::clone(localizer), parse_host_pattern);
let color_policy_parser = LocalizedValueParser::new(Arc::clone(localizer), parse_color_policy);
let emoji_policy_parser = LocalizedValueParser::new(Arc::clone(localizer), parse_emoji_policy);
let progress_policy_parser =
LocalizedValueParser::new(Arc::clone(localizer), parse_progress_policy);
let accessibility_policy_parser =
LocalizedValueParser::new(Arc::clone(localizer), parse_accessibility_policy);
command = command.mut_arg("jobs", |arg| {
arg.value_parser(ValueParser::new(jobs_parser))
});
command = command.mut_arg("locale", |arg| {
arg.value_parser(ValueParser::new(locale_parser))
});
command = command.mut_arg("fetch_allow_scheme", |arg| {
arg.value_parser(ValueParser::new(scheme_parser.clone()))
});
command = command.mut_arg("fetch_allow_host", |arg| {
arg.value_parser(ValueParser::new(host_parser.clone()))
});
command = command.mut_arg("fetch_block_host", |arg| {
arg.value_parser(ValueParser::new(host_parser))
});
command = command.mut_arg("color", |arg| {
arg.value_parser(ValueParser::new(color_policy_parser))
});
command = command.mut_arg("emoji", |arg| {
arg.value_parser(ValueParser::new(emoji_policy_parser))
});
command = command.mut_arg("progress", |arg| {
arg.value_parser(ValueParser::new(progress_policy_parser))
});
command = command.mut_arg("accessibility", |arg| {
arg.value_parser(ValueParser::new(accessibility_policy_parser))
});
command
}
#[cfg(test)]
#[path = "parser_tests.rs"]
mod tests;