use clap::ValueEnum;
use ortho_config::{OrthoConfig, OrthoResult, PostMergeContext, PostMergeHook};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
use super::validation_error;
use crate::host_pattern::HostPattern;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct NoInput(bool);
impl NoInput {
#[must_use]
pub const fn is_enabled(self) -> bool {
self.0
}
}
impl Default for NoInput {
fn default() -> Self {
Self(true)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum, Default)]
#[serde(rename_all = "kebab-case")]
pub enum ColourPolicy {
#[default]
Auto,
Always,
Never,
}
impl fmt::Display for ColourPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Auto => write!(f, "auto"),
Self::Always => write!(f, "always"),
Self::Never => write!(f, "never"),
}
}
}
impl FromStr for ColourPolicy {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
<Self as ValueEnum>::from_str(s, true).map_err(|_| format!("invalid color policy '{s}'"))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum, Default)]
#[serde(rename_all = "kebab-case")]
pub enum ProgressPolicy {
#[default]
Auto,
Always,
Never,
}
impl fmt::Display for ProgressPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Auto => write!(f, "auto"),
Self::Always => write!(f, "always"),
Self::Never => write!(f, "never"),
}
}
}
impl FromStr for ProgressPolicy {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
<Self as ValueEnum>::from_str(s, true).map_err(|_| format!("invalid progress policy '{s}'"))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum, Default)]
#[serde(rename_all = "kebab-case")]
pub enum EmojiPolicy {
#[default]
Auto,
Always,
Never,
}
impl fmt::Display for EmojiPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Auto => write!(f, "auto"),
Self::Always => write!(f, "always"),
Self::Never => write!(f, "never"),
}
}
}
impl FromStr for EmojiPolicy {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
<Self as ValueEnum>::from_str(s, true).map_err(|_| format!("invalid emoji policy '{s}'"))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum, Default)]
#[serde(rename_all = "kebab-case")]
pub enum AccessibilityPolicy {
#[default]
Auto,
On,
Off,
}
impl fmt::Display for AccessibilityPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Auto => write!(f, "auto"),
Self::On => write!(f, "on"),
Self::Off => write!(f, "off"),
}
}
}
impl FromStr for AccessibilityPolicy {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
<Self as ValueEnum>::from_str(s, true)
.map_err(|_| format!("invalid accessibility policy '{s}'"))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct BuildConfig {
#[serde(default)]
pub targets: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct CommandConfigs {
#[serde(default)]
pub build: BuildConfig,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, OrthoConfig)]
#[ortho_config(prefix = "NETSUKE", post_merge_hook)]
pub struct CliConfig {
#[ortho_config(default = default_manifest_path())]
pub file: PathBuf,
pub jobs: Option<usize>,
#[ortho_config(default = false)]
pub verbose: bool,
pub locale: Option<String>,
#[ortho_config(merge_strategy = "append")]
#[serde(default)]
pub fetch_allow_scheme: Vec<String>,
#[ortho_config(merge_strategy = "append")]
#[serde(default)]
pub fetch_allow_host: Vec<HostPattern>,
#[ortho_config(merge_strategy = "append")]
#[serde(default)]
pub fetch_block_host: Vec<HostPattern>,
#[ortho_config(default = false)]
pub fetch_default_deny: bool,
#[ortho_config(default = false)]
pub json: bool,
#[ortho_config(skip_cli)]
pub no_input: NoInput,
#[ortho_config(skip_cli)]
pub color: ColourPolicy,
#[ortho_config(skip_cli)]
pub emoji: EmojiPolicy,
#[ortho_config(skip_cli)]
pub progress: ProgressPolicy,
#[ortho_config(skip_cli)]
pub accessibility: AccessibilityPolicy,
#[ortho_config(merge_strategy = "append")]
#[serde(default)]
pub default_targets: Vec<String>,
#[ortho_config(skip_cli)]
#[serde(default)]
pub cmds: CommandConfigs,
}
impl Default for CliConfig {
fn default() -> Self {
Self {
file: Self::default_manifest_path(),
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,
no_input: NoInput::default(),
color: ColourPolicy::Auto,
emoji: EmojiPolicy::Auto,
progress: ProgressPolicy::Auto,
accessibility: AccessibilityPolicy::Auto,
default_targets: Vec::new(),
cmds: CommandConfigs::default(),
}
}
}
impl CliConfig {
pub(super) fn default_manifest_path() -> PathBuf {
default_manifest_path()
}
}
const MAX_JOBS: usize = super::MAX_JOBS;
const fn jobs_out_of_bounds(jobs: usize) -> bool {
jobs == 0 || jobs > MAX_JOBS
}
impl PostMergeHook for CliConfig {
fn post_merge(&mut self, _ctx: &PostMergeContext) -> OrthoResult<()> {
validate_non_interactive(self)?;
validate_jobs(self)?;
Ok(())
}
}
fn default_manifest_path() -> PathBuf {
PathBuf::from("Netsukefile")
}
fn validate_non_interactive(config: &CliConfig) -> OrthoResult<()> {
if config.no_input.is_enabled() {
Ok(())
} else {
Err(validation_error(
"no_input",
"no_input = false is unsupported because Netsuke has no interactive mode",
))
}
}
fn validate_jobs(config: &CliConfig) -> OrthoResult<()> {
let Some(jobs) = config.jobs else {
return Ok(());
};
if jobs_out_of_bounds(jobs) {
return Err(validation_error(
"jobs",
&format!("jobs = {jobs} is out of range; must be between 1 and {MAX_JOBS}"),
));
}
Ok(())
}