use crate::environment::Environment;
use crate::error::{BadNumericSource, ConfigError, Error, Result};
use super::{
canonical_key, parse_bool, parse_git_config_int_strict, parse_path, ConfigSet,
GitConfigIntStrictError,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigValueType {
Bool,
Int,
Path,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypedConfigValue {
Bool(bool),
Int(i64),
Path(String),
}
impl TypedConfigValue {
#[must_use]
pub fn to_canonical_string(&self) -> String {
match self {
Self::Bool(b) => if *b { "true" } else { "false" }.to_owned(),
Self::Int(n) => n.to_string(),
Self::Path(p) => p.clone(),
}
}
}
pub fn canonicalize_config_write_value(
key: &str,
ty: ConfigValueType,
value: &str,
env: &Environment,
) -> Result<String> {
let typed = parse_typed_value(key, ty, value, env)?;
Ok(typed.to_canonical_string())
}
pub fn parse_typed_value(
key: &str,
ty: ConfigValueType,
value: &str,
env: &Environment,
) -> Result<TypedConfigValue> {
match ty {
ConfigValueType::Bool => parse_bool(value).map(TypedConfigValue::Bool).map_err(|_| {
Error::Config(ConfigError::Other(format!(
"bad boolean config value '{value}' for '{key}'"
)))
}),
ConfigValueType::Int => parse_git_config_int_strict(value)
.map(TypedConfigValue::Int)
.map_err(|e| int_parse_error(key, value, e)),
ConfigValueType::Path => Ok(TypedConfigValue::Path(parse_path(env, value))),
}
}
fn int_parse_error(key: &str, value: &str, err: GitConfigIntStrictError) -> Error {
let reason = match err {
GitConfigIntStrictError::InvalidUnit => BadNumericSource::InvalidUnit,
GitConfigIntStrictError::OutOfRange => BadNumericSource::OutOfRange,
};
Error::Config(ConfigError::BadNumericValue {
key: key.to_owned(),
value: value.to_owned(),
reason,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ListPrefixMatch {
KeyPrefix(String),
ExactKey(String),
}
impl ConfigSet {
pub fn get_typed(
&self,
key: &str,
ty: ConfigValueType,
env: &Environment,
) -> Result<Option<TypedConfigValue>> {
let _canon = canonical_key(key)?;
let Some(entry) = self.get_last_entry(key) else {
return Ok(None);
};
let raw = entry.value.as_deref().unwrap_or("true");
parse_typed_value(key, ty, raw, env).map(Some)
}
pub fn entries_with_prefix(&self, prefix: &str) -> Result<Vec<super::ConfigEntry>> {
let matcher = list_prefix_match(prefix)?;
Ok(self
.entries()
.iter()
.filter(|e| match &matcher {
ListPrefixMatch::ExactKey(key) => &e.key == key,
ListPrefixMatch::KeyPrefix(needle) => e.key.starts_with(needle.as_str()),
})
.cloned()
.collect())
}
}
fn list_prefix_match(prefix: &str) -> Result<ListPrefixMatch> {
let prefix = prefix.trim();
if prefix.is_empty() {
return Err(Error::Config(ConfigError::Other(
"list prefix must not be empty".into(),
)));
}
if let Ok(canon) = canonical_key(prefix) {
return Ok(ListPrefixMatch::ExactKey(canon));
}
let section_part = prefix.trim_end_matches('.');
if section_part.is_empty() {
return Err(Error::Config(ConfigError::Other(
"list prefix must not be empty".into(),
)));
}
let (sec, sub) = match section_part.find('.') {
Some(i) => (§ion_part[..i], Some(§ion_part[i + 1..])),
None => (section_part, None),
};
if sec.is_empty()
|| !sec
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
{
return Err(Error::Config(ConfigError::Other(format!(
"invalid list prefix '{prefix}'"
))));
}
let sec_lower = sec.to_ascii_lowercase();
let needle = match sub {
None => format!("{sec_lower}."),
Some(sub) if !sub.is_empty() => format!("{sec_lower}.{sub}."),
Some(_) => {
return Err(Error::Config(ConfigError::Other(format!(
"invalid list prefix '{prefix}'"
))));
}
};
Ok(ListPrefixMatch::KeyPrefix(needle))
}