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).map(TypedConfigValue::Bool).map_err(|_| {
73            Error::Config(ConfigError::Other(format!(
74                "bad boolean config value '{value}' for '{key}'"
75            )))
76        }),
77        ConfigValueType::Int => parse_git_config_int_strict(value)
78            .map(TypedConfigValue::Int)
79            .map_err(|e| int_parse_error(key, value, e)),
80        ConfigValueType::Path => Ok(TypedConfigValue::Path(parse_path(env, value))),
81    }
82}
83
84fn int_parse_error(key: &str, value: &str, err: GitConfigIntStrictError) -> Error {
85    let reason = match err {
86        GitConfigIntStrictError::InvalidUnit => BadNumericSource::InvalidUnit,
87        GitConfigIntStrictError::OutOfRange => BadNumericSource::OutOfRange,
88    };
89    Error::Config(ConfigError::BadNumericValue {
90        key: key.to_owned(),
91        value: value.to_owned(),
92        reason,
93    })
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
97enum ListPrefixMatch {
98    /// Match keys that start with this canonical prefix (section or section.subsection).
99    KeyPrefix(String),
100    /// Match one canonical key exactly (full `--list user.email` style prefix).
101    ExactKey(String),
102}
103
104impl ConfigSet {
105    /// Read the winning value for `key` as a Git integer (with optional `k`/`m`/`g` suffix).
106    ///
107    /// Missing keys return `Ok(None)`.
108    ///
109    /// # Errors
110    ///
111    /// Returns [`ConfigError::BadNumericValue`] (as [`Error::Config`]) when the value is
112    /// not an integer or does not fit in an `i64`.
113    pub(crate) fn get_i64_strict(&self, key: &str) -> Result<Option<i64>> {
114        let Some(raw) = self.get(key) else {
115            return Ok(None);
116        };
117        parse_git_config_int_strict(&raw)
118            .map(Some)
119            .map_err(|e| int_parse_error(key, &raw, e))
120    }
121
122    /// Entries whose canonical key starts with `prefix`.
123    ///
124    /// `prefix` may be a full key, a section (`remote.origin`), or a section prefix ending in `.`.
125    ///
126    /// # Errors
127    ///
128    /// Returns [`Error::Config`] when `prefix` is empty or invalid.
129    pub fn entries_with_prefix(&self, prefix: &str) -> Result<Vec<super::ConfigEntry>> {
130        let matcher = list_prefix_match(prefix)?;
131        Ok(self
132            .entries()
133            .iter()
134            .filter(|e| match &matcher {
135                ListPrefixMatch::ExactKey(key) => &e.key == key,
136                ListPrefixMatch::KeyPrefix(needle) => e.key.starts_with(needle.as_str()),
137            })
138            .cloned()
139            .collect())
140    }
141}
142
143fn list_prefix_match(prefix: &str) -> Result<ListPrefixMatch> {
144    let prefix = prefix.trim();
145    if prefix.is_empty() {
146        return Err(Error::Config(ConfigError::Other(
147            "list prefix must not be empty".into(),
148        )));
149    }
150    if let Ok(canon) = canonical_key(prefix) {
151        return Ok(ListPrefixMatch::ExactKey(canon));
152    }
153    let section_part = prefix.trim_end_matches('.');
154    if section_part.is_empty() {
155        return Err(Error::Config(ConfigError::Other(
156            "list prefix must not be empty".into(),
157        )));
158    }
159    let (sec, sub) = match section_part.find('.') {
160        Some(i) => (&section_part[..i], Some(&section_part[i + 1..])),
161        None => (section_part, None),
162    };
163    if sec.is_empty()
164        || !sec
165            .chars()
166            .all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
167    {
168        return Err(Error::Config(ConfigError::Other(format!(
169            "invalid list prefix '{prefix}'"
170        ))));
171    }
172    let sec_lower = sec.to_ascii_lowercase();
173    let needle = match sub {
174        None => format!("{sec_lower}."),
175        Some(sub) if !sub.is_empty() => format!("{sec_lower}.{sub}."),
176        Some(_) => {
177            return Err(Error::Config(ConfigError::Other(format!(
178                "invalid list prefix '{prefix}'"
179            ))));
180        }
181    };
182    Ok(ListPrefixMatch::KeyPrefix(needle))
183}