use crate::cli::Cli;
use crate::config::{
Config, ConfigLoadError, ConfigLoadProvenance, ConfigStatus, config_file_path, load_config,
load_config_from_path, load_config_from_path_with_provenance, mask_home_path,
};
#[derive(Debug)]
pub(crate) struct StartupDiagnostic {
message: String,
log_message: Option<String>,
}
impl StartupDiagnostic {
pub(crate) fn user_warning(message: String) -> Self {
Self {
message,
log_message: None,
}
}
fn emit(self) {
eprintln!("{}", self.message);
if let Some(message) = self.log_message {
log::warn!("{message}");
}
}
}
#[derive(Debug)]
pub(crate) struct ConfigLoadReport {
pub(crate) config: Config,
pub(crate) config_path: Option<std::path::PathBuf>,
pub(crate) status: ConfigStatus,
pub(crate) diagnostics: Vec<StartupDiagnostic>,
}
pub(crate) fn report_startup_diagnostics(diagnostics: Vec<StartupDiagnostic>) {
for diagnostic in diagnostics {
diagnostic.emit();
}
}
pub(crate) fn report_diagnostics_on_setup_error<T, E>(
result: Result<T, E>,
diagnostics: Vec<StartupDiagnostic>,
report: impl FnOnce(Vec<StartupDiagnostic>),
) -> Result<(T, Vec<StartupDiagnostic>), E> {
match result {
Ok(value) => Ok((value, diagnostics)),
Err(error) => {
report(diagnostics);
Err(error)
}
}
}
#[derive(Debug)]
pub(crate) struct ConfigLoadWarning {
pub(crate) code: &'static str,
pub(crate) message: String,
pub(crate) path: std::path::PathBuf,
}
pub(crate) fn load_config_with_fallback(cli: &Cli) -> ConfigLoadReport {
let (result, config_path) = if let Some(path) = &cli.r_source.config {
(load_config_from_path(path), Some(path.clone()))
} else {
let default_path = config_file_path();
(load_config(), default_path)
};
match result {
Ok(mut config) => {
let diagnostics = config
.history_migration_warning
.take()
.map(|warning| StartupDiagnostic {
message: format!("Warning: {warning}"),
log_message: None,
})
.into_iter()
.collect();
ConfigLoadReport {
config,
config_path,
status: ConfigStatus::Ok,
diagnostics,
}
}
Err(e) => {
let (raw_path, masked_path, source_msg, status) = match &e {
ConfigLoadError::Read { path, source } => (
path.display().to_string(),
mask_home_path(path),
source.to_string(),
ConfigStatus::ReadError,
),
ConfigLoadError::Parse { path, source } => (
path.display().to_string(),
mask_home_path(path),
source.to_string(),
ConfigStatus::ParseError,
),
ConfigLoadError::Validation { path, message } => (
path.display().to_string(),
mask_home_path(path),
message.clone(),
ConfigStatus::ParseError,
),
};
ConfigLoadReport {
config: Config::default(),
config_path,
status,
diagnostics: vec![StartupDiagnostic {
message: format!(
"Warning: Failed to load config from {}: {}\n Using default configuration. Run `arf config check` to see details.",
masked_path, source_msg
),
log_message: Some(format!(
"Config load error for {}: {}",
raw_path, source_msg
)),
}],
}
}
}
}
pub(crate) fn load_config_for_startup(
config_path: Option<&std::path::PathBuf>,
) -> (Config, Vec<StartupDiagnostic>) {
let result = if let Some(path) = config_path {
load_config_from_path(path)
} else {
load_config()
};
match result {
Ok(mut config) => {
let diagnostics = config
.history_migration_warning
.take()
.map(|warning| StartupDiagnostic {
message: format!("Warning: {warning}"),
log_message: None,
})
.into_iter()
.collect();
(config, diagnostics)
}
Err(e) => {
let (path_display, source_msg) = match &e {
ConfigLoadError::Read { path, source } => {
(mask_home_path(path), source.to_string())
}
ConfigLoadError::Parse { path, source } => {
(mask_home_path(path), source.to_string())
}
ConfigLoadError::Validation { path, message } => {
(mask_home_path(path), message.clone())
}
};
(
Config::default(),
vec![StartupDiagnostic {
message: format!(
"Warning: Failed to load config from {}: {}\n Using default configuration.",
path_display, source_msg
),
log_message: None,
}],
)
}
}
}
pub(crate) fn load_config_or_warn(config_path: Option<&std::path::PathBuf>) -> Config {
let (config, diagnostics) = load_config_for_startup(config_path);
report_startup_diagnostics(diagnostics);
config
}
pub(crate) fn load_config_collecting_warnings(
config_path: Option<&std::path::PathBuf>,
warnings: &mut Vec<String>,
) -> Config {
let result = if let Some(path) = config_path {
load_config_from_path(path)
} else {
load_config()
};
match result {
Ok(mut config) => {
if let Some(warning) = config.history_migration_warning.take() {
warnings.push(warning);
}
config
}
Err(e) => {
let (path_display, source_msg) = match &e {
ConfigLoadError::Read { path, source } => {
(mask_home_path(path), source.to_string())
}
ConfigLoadError::Parse { path, source } => {
(mask_home_path(path), source.to_string())
}
ConfigLoadError::Validation { path, message } => {
(mask_home_path(path), message.clone())
}
};
warnings.push(format!(
"Failed to load config from {path_display}: {source_msg}. Using default configuration."
));
Config::default()
}
}
}
pub(crate) fn load_config_collecting_diagnostics(
config_path: Option<&std::path::PathBuf>,
warnings: &mut Vec<ConfigLoadWarning>,
) -> (Config, Option<ConfigLoadProvenance>) {
let path = config_path.cloned().or_else(config_file_path);
let result = if let Some(path) = path.as_deref() {
load_config_from_path_with_provenance(path)
} else {
Ok((Config::default(), None))
};
match result {
Ok((mut config, provenance)) => {
let warning = config.history_migration_warning.take();
if let (Some(warning), Some(provenance)) = (&warning, provenance.as_ref()) {
log::warn!("{warning}");
warnings.push(ConfigLoadWarning {
code: "config.history_disabled_deprecated",
message: warning.clone(),
path: provenance.path.clone(),
});
}
let provenance = provenance.map(|mut provenance| {
provenance.history_migration_warning = warning;
provenance
});
(config, provenance)
}
Err(error) => {
let (code, path, message) = match error {
ConfigLoadError::Read { path, source } => {
("config.read_failed", path, source.to_string())
}
ConfigLoadError::Parse { path, source } => {
("config.parse_failed", path, source.to_string())
}
ConfigLoadError::Validation { path, message } => {
("config.parse_failed", path, message)
}
};
warnings.push(ConfigLoadWarning {
code,
message: format!(
"Failed to load config from {}: {}. Using default configuration.",
path.display(),
message
),
path,
});
(Config::default(), None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn setup_error_reports_pending_diagnostics_once() {
let mut reported = None;
let result = report_diagnostics_on_setup_error(
Err::<(), _>("R setup failed"),
vec![StartupDiagnostic::user_warning("Warning: pending".into())],
|diagnostics| reported = Some(diagnostics),
);
assert!(matches!(result, Err("R setup failed")));
let diagnostics = reported.expect("diagnostics should be reported on setup error");
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].message, "Warning: pending");
}
#[test]
fn successful_r_setup_keeps_diagnostics_pending() {
let mut reported = false;
let (value, diagnostics) = report_diagnostics_on_setup_error(
Ok::<_, &str>(42),
vec![StartupDiagnostic::user_warning("Warning: pending".into())],
|_| reported = true,
)
.unwrap();
assert_eq!(value, 42);
assert_eq!(diagnostics.len(), 1);
assert!(!reported);
}
#[test]
fn startup_config_load_holds_deprecation_warning_for_the_caller() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("arf.toml");
std::fs::write(&path, "[history]\ndisabled = true\n").unwrap();
let (config, diagnostics) = load_config_for_startup(Some(&path));
assert!(matches!(
config.history.mode,
crate::config::HistoryMode::Volatile
));
assert_eq!(diagnostics.len(), 1);
assert!(
diagnostics[0]
.message
.starts_with("Warning: Config key history.disabled")
);
assert!(diagnostics[0].log_message.is_none());
}
#[test]
fn interactive_config_load_holds_deprecation_warning_for_the_caller() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("arf.toml");
std::fs::write(&path, "[history]\ndisabled = true\n").unwrap();
let cli = Cli::try_parse_from(["arf", "--config", path.to_str().unwrap()]).unwrap();
let report = load_config_with_fallback(&cli);
assert_eq!(report.status, ConfigStatus::Ok);
assert_eq!(report.diagnostics.len(), 1);
assert!(
report.diagnostics[0]
.message
.starts_with("Warning: Config key history.disabled")
);
assert!(report.diagnostics[0].log_message.is_none());
}
#[test]
fn deprecated_history_disabled_is_collected_for_headless_output() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("arf.toml");
std::fs::write(&path, "[history]\ndisabled = true\n").unwrap();
let mut warnings = Vec::new();
let config = load_config_collecting_warnings(Some(&path), &mut warnings);
assert!(matches!(
config.history.mode,
crate::config::HistoryMode::Volatile
));
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains(r#"history.mode = "volatile""#));
}
#[test]
fn deprecated_history_disabled_has_a_stable_diagnostic_code() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("arf.toml");
std::fs::write(&path, "[history]\ndisabled = false\n").unwrap();
let mut warnings = Vec::new();
let (_, provenance) = load_config_collecting_diagnostics(Some(&path), &mut warnings);
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "config.history_disabled_deprecated");
assert!(
provenance
.unwrap()
.history_migration_warning
.unwrap()
.contains("persistent")
);
}
}