Skip to main content

grit_lib/config/
typed.rs

1//! Typed config reads and canonical write values (`--type bool|int|path`).
2
3use crate::environment::Environment;
4use crate::error::{BadNumericSource, ConfigError, Error, Result};
5
6use super::{
7    canonical_key, parse_bool, parse_git_config_int_strict, parse_path, ConfigSet,
8    GitConfigIntStrictError,
9};
10
11/// Value type for typed config operations (Git `--type` subset).
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ConfigValueType {
14    /// Boolean (`true` / `false`).
15    Bool,
16    /// Signed integer with optional `k`/`m`/`g` suffix on input.
17    Int,
18    /// Path (tilde and environment expansion).
19    Path,
20}
21
22/// A config value after typed parsing.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum TypedConfigValue {
25    /// Parsed boolean.
26    Bool(bool),
27    /// Parsed integer.
28    Int(i64),
29    /// Expanded path string.
30    Path(String),
31}
32
33impl TypedConfigValue {
34    /// Canonical string form for writing to a config file (matches Git `--type` output).
35    #[must_use]
36    pub fn to_canonical_string(&self) -> String {
37        match self {
38            Self::Bool(b) => if *b { "true" } else { "false" }.to_owned(),
39            Self::Int(n) => n.to_string(),
40            Self::Path(p) => p.clone(),
41        }
42    }
43}
44
45/// Parse and canonicalize a value for writing with `--type`.
46///
47/// # Errors
48///
49/// Returns [`Error::Config`] when the value is not valid for the requested type.
50pub fn canonicalize_config_write_value(
51    key: &str,
52    ty: ConfigValueType,
53    value: &str,
54    env: &Environment,
55) -> Result<String> {
56    let typed = parse_typed_value(key, ty, value, env)?;
57    Ok(typed.to_canonical_string())
58}
59
60/// Parse a raw config string according to `ty` (for reads and writes).
61///
62/// # Errors
63///
64/// Returns [`Error::Config`] when the value is not valid for the requested type.
65pub fn parse_typed_value(
66    key: &str,
67    ty: ConfigValueType,
68    value: &str,
69    env: &Environment,
70) -> Result<TypedConfigValue> {
71    match ty {
72        ConfigValueType::Bool => parse_bool(value)
73            .map(TypedConfigValue::Bool)
74            .map_err(|e| e.for_key(key).into()),
75        ConfigValueType::Int => parse_git_config_int_strict(value)
76            .map(TypedConfigValue::Int)
77            .map_err(|e| int_parse_error(key, value, e)),
78        ConfigValueType::Path => Ok(TypedConfigValue::Path(parse_path(env, value))),
79    }
80}
81
82fn int_parse_error(key: &str, value: &str, err: GitConfigIntStrictError) -> Error {
83    let reason = match err {
84        GitConfigIntStrictError::InvalidUnit => BadNumericSource::InvalidUnit,
85        GitConfigIntStrictError::OutOfRange => BadNumericSource::OutOfRange,
86    };
87    Error::Config(ConfigError::BadNumericValue {
88        key: key.to_owned(),
89        value: value.to_owned(),
90        reason,
91    })
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95enum ListPrefixMatch {
96    /// Match keys that start with this canonical prefix (section or section.subsection).
97    KeyPrefix(String),
98    /// Match one canonical key exactly (full `--list user.email` style prefix).
99    ExactKey(String),
100}
101
102impl ConfigSet {
103    /// Read the winning value for `key` as a Git integer (with optional `k`/`m`/`g` suffix).
104    ///
105    /// Missing keys return `Ok(None)`.
106    ///
107    /// # Errors
108    ///
109    /// Returns [`ConfigError::BadNumericValue`] (as [`Error::Config`]) when the value is
110    /// not an integer or does not fit in an `i64`.
111    pub(crate) fn get_i64_strict(&self, key: &str) -> Result<Option<i64>> {
112        let Some(raw) = self.get(key) else {
113            return Ok(None);
114        };
115        parse_git_config_int_strict(&raw)
116            .map(Some)
117            .map_err(|e| int_parse_error(key, &raw, e))
118    }
119
120    /// Entries whose canonical key starts with `prefix`.
121    ///
122    /// `prefix` may be a full key, a section (`remote.origin`), or a section prefix ending in `.`.
123    ///
124    /// # Errors
125    ///
126    /// Returns [`Error::Config`] when `prefix` is empty or invalid.
127    pub fn entries_with_prefix(&self, prefix: &str) -> Result<Vec<super::ConfigEntry>> {
128        let matcher = list_prefix_match(prefix)?;
129        Ok(self
130            .entries()
131            .iter()
132            .filter(|e| match &matcher {
133                ListPrefixMatch::ExactKey(key) => &e.key == key,
134                ListPrefixMatch::KeyPrefix(needle) => e.key.starts_with(needle.as_str()),
135            })
136            .cloned()
137            .collect())
138    }
139}
140
141fn list_prefix_match(prefix: &str) -> Result<ListPrefixMatch> {
142    let prefix = prefix.trim();
143    if prefix.is_empty() {
144        return Err(Error::Config(ConfigError::Other(
145            "list prefix must not be empty".into(),
146        )));
147    }
148    if let Ok(canon) = canonical_key(prefix) {
149        return Ok(ListPrefixMatch::ExactKey(canon));
150    }
151    let section_part = prefix.trim_end_matches('.');
152    if section_part.is_empty() {
153        return Err(Error::Config(ConfigError::Other(
154            "list prefix must not be empty".into(),
155        )));
156    }
157    let (sec, sub) = match section_part.find('.') {
158        Some(i) => (&section_part[..i], Some(&section_part[i + 1..])),
159        None => (section_part, None),
160    };
161    if sec.is_empty()
162        || !sec
163            .chars()
164            .all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
165    {
166        return Err(Error::Config(ConfigError::Other(format!(
167            "invalid list prefix '{prefix}'"
168        ))));
169    }
170    let sec_lower = sec.to_ascii_lowercase();
171    let needle = match sub {
172        None => format!("{sec_lower}."),
173        Some(sub) if !sub.is_empty() => format!("{sec_lower}.{sub}."),
174        Some(_) => {
175            return Err(Error::Config(ConfigError::Other(format!(
176                "invalid list prefix '{prefix}'"
177            ))));
178        }
179    };
180    Ok(ListPrefixMatch::KeyPrefix(needle))
181}