use std::path::{Path, PathBuf};
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Clone)]
pub struct UnknownKeyInfo {
pub key: String,
pub path: PathBuf,
pub line: usize,
pub source: Option<Arc<str>>,
}
impl UnknownKeyInfo {
pub fn leaf(&self) -> &str {
self.key.rsplit('.').next().unwrap_or(&self.key)
}
}
#[derive(Debug, Error)]
pub enum ClapfigError {
#[error("{}", format_unknown_keys(.0))]
UnknownKeys(Vec<UnknownKeyInfo>),
#[error("Failed to parse {}: {source}", path.display())]
ParseError {
path: PathBuf,
source: Box<toml::de::Error>,
source_text: Option<Arc<str>>,
},
#[error("Failed to read {}: {source}", path.display())]
IoError {
path: PathBuf,
source: std::io::Error,
},
#[error("Configuration error: {0}")]
ConfigError(#[from] confique::Error),
#[error("Key not found: {0}")]
KeyNotFound(String),
#[error("Invalid value for '{key}': {reason}")]
InvalidValue { key: String, reason: String },
#[error("No persist scopes configured — call .persist_scope() on the builder")]
NoPersistPath,
#[error("Ancestors is not valid as a persist scope path (it resolves to multiple directories)")]
AncestorsNotAllowedAsPersistPath,
#[error("Unknown scope '{scope}' — available scopes: {}", available.join(", "))]
UnknownScope {
scope: String,
available: Vec<String>,
},
#[error("Unknown config subcommand: '{0}'")]
UnknownSubcommand(String),
#[error("App name is required — call .app_name() on the builder")]
AppNameRequired,
#[error("Configuration validation failed: {0}")]
PostValidationFailed(String),
}
impl ClapfigError {
pub fn unknown_keys(&self) -> Option<&[UnknownKeyInfo]> {
match self {
ClapfigError::UnknownKeys(infos) => Some(infos),
_ => None,
}
}
pub fn parse_error(&self) -> Option<(&Path, &toml::de::Error, Option<&str>)> {
match self {
ClapfigError::ParseError {
path,
source,
source_text,
} => Some((path.as_path(), source.as_ref(), source_text.as_deref())),
_ => None,
}
}
pub fn is_strict_violation(&self) -> bool {
matches!(self, ClapfigError::UnknownKeys(_))
}
}
fn format_unknown_keys(infos: &[UnknownKeyInfo]) -> String {
use std::fmt::Write;
let mut out = String::from("Unknown keys in config file:");
for info in infos {
let _ = write!(
out,
"\n - '{}' in {} (line {})",
info.key,
info.path.display(),
info.line
);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn info(key: &str, line: usize) -> UnknownKeyInfo {
UnknownKeyInfo {
key: key.into(),
path: "/home/user/.config/myapp/config.toml".into(),
line,
source: None,
}
}
#[test]
fn unknown_keys_formats_correctly() {
let err = ClapfigError::UnknownKeys(vec![info("typo_key", 42)]);
let msg = err.to_string();
assert!(msg.contains("typo_key"));
assert!(msg.contains("config.toml"));
assert!(msg.contains("42"));
}
#[test]
fn unknown_keys_accessor_returns_data() {
let err = ClapfigError::UnknownKeys(vec![info("a", 1), info("b.c", 2)]);
let keys = err.unknown_keys().expect("should be unknown keys");
assert_eq!(keys.len(), 2);
assert_eq!(keys[0].key, "a");
assert_eq!(keys[1].key, "b.c");
assert_eq!(keys[1].leaf(), "c");
}
#[test]
fn unknown_keys_accessor_none_for_other_variants() {
assert!(
ClapfigError::KeyNotFound("x".into())
.unknown_keys()
.is_none()
);
}
#[test]
fn is_strict_violation_matches_only_unknown_keys() {
assert!(ClapfigError::UnknownKeys(vec![info("x", 1)]).is_strict_violation());
assert!(!ClapfigError::KeyNotFound("x".into()).is_strict_violation());
assert!(!ClapfigError::AppNameRequired.is_strict_violation());
}
#[test]
fn key_not_found_formats() {
let err = ClapfigError::KeyNotFound("database.url".into());
assert!(err.to_string().contains("database.url"));
}
#[test]
fn app_name_required_formats() {
let err = ClapfigError::AppNameRequired;
assert!(err.to_string().contains("app_name"));
}
#[test]
fn leaf_returns_last_segment() {
assert_eq!(info("a.b.c", 0).leaf(), "c");
assert_eq!(info("toplevel", 0).leaf(), "toplevel");
}
}