use serde::Serialize;
use crate::output::CliReport;
use crate::output::human::Human;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ConfigActivation {
SavedOnly,
}
#[derive(Serialize)]
pub struct UnsetVar {
pub unset_env: String,
}
#[derive(Serialize)]
pub struct ValidateReport {
pub valid: bool,
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub providers: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub models: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presets: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub routers: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub migration_hint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variants: Option<usize>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<UnsetVar>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub ignored_config: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<String>,
}
impl ValidateReport {
pub fn valid(
path: String,
providers: usize,
models: usize,
presets: usize,
variants: usize,
warnings: Vec<UnsetVar>,
) -> Self {
Self {
valid: true,
path,
providers: Some(providers),
models: Some(models),
presets: Some(presets),
routers: None,
migration_hint: (presets > 0).then(|| "Legacy presets remain supported; preview conversion with `bro config migrate-routers`.".to_string()),
variants: Some(variants),
warnings,
ignored_config: Vec::new(),
errors: Vec::new(),
}
}
pub fn with_ignored_config(mut self, ignored_config: Vec<String>) -> Self {
self.ignored_config = ignored_config;
self
}
pub fn with_routers(mut self, count: usize) -> Self {
self.routers = Some(count);
self
}
pub fn invalid(path: String, error: String) -> Self {
Self {
valid: false,
path,
providers: None,
models: None,
presets: None,
routers: None,
migration_hint: None,
variants: None,
warnings: Vec::new(),
ignored_config: Vec::new(),
errors: vec![error],
}
}
}
impl CliReport for ValidateReport {
fn render(&self, h: &mut Human<'_>) -> std::io::Result<()> {
if self.valid {
h.line(&format!("✓ {} is valid", self.path))?;
h.line(&format!(
" providers: {} models: {} presets: {} variants: {}",
self.providers.unwrap_or(0),
self.models.unwrap_or(0),
self.presets.unwrap_or(0),
self.variants.unwrap_or(0),
))?;
if let Some(routers) = self.routers {
h.line(&format!(" routers: {routers}"))?;
}
if let Some(hint) = &self.migration_hint {
h.line(hint)?;
}
if !self.warnings.is_empty() {
h.blank()?;
h.line(&format!(
" note: {} unset environment variable(s) substituted with a placeholder \
for validation (re-validate at runtime):",
self.warnings.len()
))?;
for w in &self.warnings {
h.line(&format!(" - ${{{}}}", w.unset_env))?;
}
}
if !self.ignored_config.is_empty() {
h.blank()?;
h.line(&format!(
" note: {} setting(s) this binary does not act on — ignored, \
which is silent at runtime:",
self.ignored_config.len()
))?;
for line in &self.ignored_config {
h.line(&format!(" - {line}"))?;
}
}
Ok(())
} else {
h.line(&format!("✗ {} is invalid", self.path))?;
for e in &self.errors {
h.line(&format!(" {e}"))?;
}
Ok(())
}
}
fn exit_code(&self) -> i32 {
if self.valid { 0 } else { 1 }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::CliReport;
#[test]
fn ignored_config_is_reported_without_failing_validation() {
let report = ValidateReport::valid("p".into(), 1, 0, 0, 0, vec![])
.with_ignored_config(vec!["plugins.bitrouter-guardrail is ignored".into()]);
assert_eq!(report.exit_code(), 0);
let v = serde_json::to_value(&report).unwrap();
assert_eq!(
v["ignored_config"][0],
"plugins.bitrouter-guardrail is ignored"
);
let clean = ValidateReport::valid("p".into(), 1, 0, 0, 0, vec![]);
assert!(
serde_json::to_value(&clean)
.unwrap()
.get("ignored_config")
.is_none()
);
}
#[test]
fn validate_exit_code_and_shape() {
let ok = ValidateReport::valid("p".into(), 1, 2, 0, 0, vec![]);
assert_eq!(ok.exit_code(), 0);
let bad = ValidateReport::invalid("p".into(), "boom".into());
assert_eq!(bad.exit_code(), 1);
let v = serde_json::to_value(&bad).unwrap();
assert_eq!(v["valid"], false);
assert_eq!(v["errors"][0], "boom");
assert!(serde_json::to_value(&ok).unwrap().get("errors").is_none());
}
}