youtube-legend-cli 0.4.0

Non-interactive Rust CLI that downloads YouTube subtitles through third-party providers, using a native Unix stdin/stdout interface.
//! Parsing one config-file value into one typed argument.
//!
//! Split out of `cli.rs` on 2026-09-04 under GAP-2026-074. Every impl
//! here answers the same narrow question — how does this type spell
//! itself in `config.toml`? — and they were only mixed into the clap
//! derive because both grew in one file.
//!
//! Keeping them together is also what makes the divergence visible: a
//! type accepted on the command line and refused in the file is a bug
//! this crate has already shipped once, recorded in GAP-2026-219.

use super::*;

/// A configuration value that can be parsed from its canonical spelling
/// in `config.toml`.
///
/// The trait exists so that the `config_schema!` macro can treat every
/// enum-like
/// key uniformly instead of repeating one `match` per key in
/// `load_config`.
pub trait ConfigValue: Sized {
    /// Parse the canonical lowercase spelling used in the config file.
    ///
    /// # Errors
    ///
    /// Returns [`AppError::Config`] when `raw` is not a legal spelling
    /// for this type.
    fn from_config_str(raw: &str) -> AppResult<Self>;
}

impl ConfigValue for FormatArg {
    fn from_config_str(raw: &str) -> AppResult<Self> {
        match raw {
            "txt" => Ok(Self::Txt),
            "srt" => Ok(Self::Srt),
            "vtt" => Ok(Self::Vtt),
            other => Err(AppError::Config(format!(
                "{} `{other}`",
                t(Message::ConfigInvalidFormat)
            ))),
        }
    }
}

impl ConfigValue for LogLevelArg {
    fn from_config_str(raw: &str) -> AppResult<Self> {
        match raw {
            "error" => Ok(Self::Error),
            "warn" => Ok(Self::Warn),
            "info" => Ok(Self::Info),
            "debug" => Ok(Self::Debug),
            "trace" => Ok(Self::Trace),
            other => Err(AppError::Config(format!(
                "{} `{other}`",
                t(Message::ConfigInvalidLogLevel)
            ))),
        }
    }
}

impl ConfigValue for LogFormatArg {
    fn from_config_str(raw: &str) -> AppResult<Self> {
        match raw {
            "text" => Ok(Self::Text),
            "json" => Ok(Self::Json),
            other => Err(AppError::Config(format!(
                "{} `{other}`",
                t(Message::ConfigInvalidLogFormat)
            ))),
        }
    }
}

impl ConfigValue for ColorArg {
    fn from_config_str(raw: &str) -> AppResult<Self> {
        match raw {
            "auto" => Ok(Self::Auto),
            "always" => Ok(Self::Always),
            "never" => Ok(Self::Never),
            other => Err(AppError::Config(format!(
                "{} `{other}`",
                t(Message::ConfigInvalidColor)
            ))),
        }
    }
}

impl ConfigValue for ProviderChoice {
    fn from_config_str(raw: &str) -> AppResult<Self> {
        // MEASURED on 2026-09-04, while removing the browser providers:
        // this match listed `auto` and ONE provider, so pinning any of
        // the others through the configuration file was refused while
        // the identical value on the command line was accepted. The
        // defect predates the removal and is fixed here rather than
        // carried forward, because the surviving set is small enough
        // that the omission would be invisible again.
        match raw {
            "auto" => Ok(Self::Auto),
            "provider-decopy" => Ok(Self::ProviderDecopy),
            "provider-noiz" => Ok(Self::ProviderNoiz),
            other => Err(AppError::Config(format!(
                "{} `{other}`",
                t(Message::ConfigInvalidProvider)
            ))),
        }
    }
}

impl ConfigValue for LanguageArg {
    fn from_config_str(raw: &str) -> AppResult<Self> {
        Self::parse(raw).map_err(|e| AppError::Config(e.to_string()))
    }
}

impl ConfigValue for Language {
    fn from_config_str(raw: &str) -> AppResult<Self> {
        Self::from_tag(raw).ok_or_else(|| {
            AppError::Config(format!(
                "{} `{raw}` ({})",
                t(Message::ConfigInvalidUiLang),
                Self::compiled_tags()
            ))
        })
    }
}