use std::path::PathBuf;
use clap::{Args, ValueEnum};
use crate::tiered::{ConfigTier, TieredConfig};
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum TierArg {
Bare,
Discovered,
Default,
Custom,
Env,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
Yaml,
Json,
}
#[derive(Debug, Clone, Args)]
pub struct ConfigShowCommand {
#[arg(value_enum, default_value_t = TierArg::Env)]
pub tier: TierArg,
#[arg(long)]
pub path: Option<PathBuf>,
#[arg(value_enum, long, default_value_t = OutputFormat::Yaml)]
pub format: OutputFormat,
#[arg(long, value_enum)]
pub diff: Option<TierArg>,
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigShowError {
#[error("`tier custom` requires --path <FILE>")]
CustomTierWithoutPath,
#[error("YAML serialization failed: {0}")]
Yaml(#[from] serde_yaml::Error),
#[error("JSON serialization failed: {0}")]
Json(#[from] serde_json::Error),
}
impl ConfigShowCommand {
pub fn run<C: TieredConfig>(&self, env_var: &str) -> Result<(), ConfigShowError> {
let tier = self.resolve(env_var)?;
let cfg = C::resolve_tier(tier);
if let Some(diff_arg) = self.diff {
let baseline_tier = Self::tier_arg_to_tier(diff_arg, env_var, &self.path)?;
let baseline = C::resolve_tier(baseline_tier);
print!("{}", cfg.diff_against(&baseline).render_unified());
return Ok(());
}
let s = match self.format {
OutputFormat::Yaml => serde_yaml::to_string(&cfg)?,
OutputFormat::Json => serde_json::to_string_pretty(&cfg)?,
};
print!("{s}");
Ok(())
}
fn resolve(&self, env_var: &str) -> Result<ConfigTier, ConfigShowError> {
Self::tier_arg_to_tier(self.tier, env_var, &self.path)
}
fn tier_arg_to_tier(
arg: TierArg,
env_var: &str,
path: &Option<PathBuf>,
) -> Result<ConfigTier, ConfigShowError> {
Ok(match arg {
TierArg::Bare => ConfigTier::Bare,
TierArg::Discovered => ConfigTier::Discovered,
TierArg::Default => ConfigTier::Default,
TierArg::Custom => match path {
Some(p) => ConfigTier::Custom(p.clone()),
None => return Err(ConfigShowError::CustomTierWithoutPath),
},
TierArg::Env => ConfigTier::from_env(env_var),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
struct FixtureConfig {
port: u16,
log_level: String,
}
impl TieredConfig for FixtureConfig {
fn bare() -> Self {
Self {
port: 0,
log_level: String::new(),
}
}
fn prescribed_default() -> Self {
Self {
port: 8080,
log_level: "info".into(),
}
}
}
#[test]
fn run_default_tier_emits_prescribed_yaml() {
let cmd = ConfigShowCommand {
tier: TierArg::Default,
path: None,
format: OutputFormat::Yaml,
diff: None,
};
cmd.run::<FixtureConfig>("FIXTURE_TIER").unwrap();
}
#[test]
fn run_bare_tier_dispatches_to_bare_via_resolve() {
let cmd = ConfigShowCommand {
tier: TierArg::Bare,
path: None,
format: OutputFormat::Json,
diff: None,
};
cmd.run::<FixtureConfig>("FIXTURE_TIER").unwrap();
}
#[test]
fn custom_tier_without_path_errors() {
let cmd = ConfigShowCommand {
tier: TierArg::Custom,
path: None,
format: OutputFormat::Yaml,
diff: None,
};
let err = cmd.run::<FixtureConfig>("FIXTURE_TIER").unwrap_err();
assert!(matches!(err, ConfigShowError::CustomTierWithoutPath));
}
#[test]
fn diff_renders_unified_diff_without_panic() {
let cmd = ConfigShowCommand {
tier: TierArg::Default,
path: None,
format: OutputFormat::Yaml,
diff: Some(TierArg::Bare),
};
cmd.run::<FixtureConfig>("FIXTURE_TIER").unwrap();
}
#[test]
fn env_tier_resolves_via_env_var() {
unsafe { std::env::set_var("FIXTURE_TIER_TEST_BARE", "bare") };
let cmd = ConfigShowCommand {
tier: TierArg::Env,
path: None,
format: OutputFormat::Yaml,
diff: None,
};
cmd.run::<FixtureConfig>("FIXTURE_TIER_TEST_BARE").unwrap();
unsafe { std::env::remove_var("FIXTURE_TIER_TEST_BARE") };
}
}