mise 2026.4.11

The front-end to your dev env
use crate::Result;
use std::collections::BTreeMap;
use std::fmt::Formatter;
use std::str::FromStr;

use serde::{Deserialize, de};

use crate::config::config_file::mise_toml::EnvList;

pub struct TomlParser<'a> {
    table: &'a toml::Value,
}

impl<'a> TomlParser<'a> {
    pub fn new(table: &'a toml::Value) -> Self {
        Self { table }
    }

    pub fn parse_str<T>(&self, key: &str) -> Option<T>
    where
        T: From<String>,
    {
        self.table
            .get(key)
            .and_then(|value| value.as_str())
            .map(|value| value.to_string().into())
    }
    pub fn parse_bool(&self, key: &str) -> Option<bool> {
        self.table.get(key).and_then(|value| value.as_bool())
    }
    pub fn parse_array<T>(&self, key: &str) -> Option<Vec<T>>
    where
        T: From<String>,
    {
        self.table
            .get(key)
            .and_then(|value| value.as_array())
            .map(|array| {
                array
                    .iter()
                    .filter_map(|value| value.as_str().map(|v| v.to_string().into()))
                    .collect::<Vec<T>>()
            })
    }
    pub fn parse_table(&self, key: &str) -> Option<BTreeMap<String, toml::Value>> {
        self.table
            .get(key)
            .and_then(|value| value.as_table())
            .map(|table| {
                table
                    .iter()
                    .map(|(key, value)| (key.clone(), value.clone()))
                    .collect::<BTreeMap<String, toml::Value>>()
            })
    }

    pub fn parse_env(&self, key: &str) -> Result<Option<EnvList>> {
        self.table
            .get(key)
            .map(|value| {
                EnvList::deserialize(value.clone())
                    .map_err(|e| eyre::eyre!("failed to parse env: {}", e))
            })
            .transpose()
    }
}

pub struct TrackingTomlParser<'a> {
    inner: TomlParser<'a>,
    table: &'a toml::Value,
    parsed_keys: std::collections::BTreeSet<String>,
}

impl<'a> TrackingTomlParser<'a> {
    pub fn new(table: &'a toml::Value) -> Self {
        Self {
            inner: TomlParser::new(table),
            table,
            parsed_keys: std::collections::BTreeSet::new(),
        }
    }

    fn record(&mut self, key: &str) {
        self.parsed_keys.insert(key.to_string());
    }

    #[cfg(test)]
    pub fn parsed_keys(&self) -> impl Iterator<Item = &str> {
        self.parsed_keys.iter().map(|s| s.as_str())
    }

    pub fn unparsed_keys(&self) -> Vec<String> {
        if let Some(table) = self.table.as_table() {
            table
                .keys()
                .filter(|k| !self.parsed_keys.contains(k.as_str()))
                .cloned()
                .collect()
        } else {
            vec![]
        }
    }

    pub fn parse_str<T>(&mut self, key: &str) -> Option<T>
    where
        T: From<String>,
    {
        self.record(key);
        self.inner.parse_str::<T>(key)
    }

    pub fn parse_bool(&mut self, key: &str) -> Option<bool> {
        self.record(key);
        self.inner.parse_bool(key)
    }

    pub fn parse_array<T>(&mut self, key: &str) -> Option<Vec<T>>
    where
        T: From<String>,
    {
        self.record(key);
        self.inner.parse_array::<T>(key)
    }

    pub fn parse_table(&mut self, key: &str) -> Option<BTreeMap<String, toml::Value>> {
        self.record(key);
        self.inner.parse_table(key)
    }

    pub fn parse_env(&mut self, key: &str) -> Result<Option<EnvList>> {
        self.record(key);
        self.inner.parse_env(key)
    }

    pub fn get_raw(&mut self, key: &str) -> Option<&'a toml::Value> {
        self.record(key);
        self.table.get(key)
    }
}

pub fn deserialize_arr<'de, D, C, T>(deserializer: D) -> std::result::Result<C, D::Error>
where
    D: de::Deserializer<'de>,
    C: FromIterator<T> + Deserialize<'de>,
    T: FromStr + Deserialize<'de>,
    <T as FromStr>::Err: std::fmt::Display,
{
    struct ArrVisitor<C, T>(std::marker::PhantomData<(C, T)>);

    impl<'de, C, T> de::Visitor<'de> for ArrVisitor<C, T>
    where
        C: FromIterator<T> + Deserialize<'de>,
        T: FromStr + Deserialize<'de>,
        <T as FromStr>::Err: std::fmt::Display,
    {
        type Value = C;
        fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
            formatter.write_str("a string, a map, or a list of strings/maps")
        }

        fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
        where
            E: de::Error,
        {
            let v = v.parse().map_err(de::Error::custom)?;
            Ok(std::iter::once(v).collect())
        }

        fn visit_map<M>(self, map: M) -> std::result::Result<Self::Value, M::Error>
        where
            M: de::MapAccess<'de>,
        {
            let item = T::deserialize(de::value::MapAccessDeserializer::new(map))?;
            Ok(std::iter::once(item).collect())
        }

        fn visit_seq<S>(self, seq: S) -> std::result::Result<Self::Value, S::Error>
        where
            S: de::SeqAccess<'de>,
        {
            #[derive(Deserialize)]
            #[serde(untagged)]
            enum StringOrValue<T> {
                String(String),
                Value(T),
            }
            let mut seq = seq;
            std::iter::from_fn(|| seq.next_element::<StringOrValue<T>>().transpose())
                .map(|element| match element {
                    Ok(StringOrValue::String(s)) => s.parse().map_err(de::Error::custom),
                    Ok(StringOrValue::Value(v)) => Ok(v),
                    Err(e) => Err(e),
                })
                .collect()
        }
    }

    deserializer.deserialize_any(ArrVisitor(std::marker::PhantomData))
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde::Deserialize;
    use std::str::FromStr;

    #[test]
    fn test_parse_arr() {
        let toml = r#"arr = ["1", "2", "3"]"#;
        let table = toml::from_str(toml).unwrap();
        let parser = TomlParser::new(&table);
        let arr = parser.parse_array::<String>("arr");
        assert_eq!(arr.unwrap().join(":"), "1:2:3");
    }

    #[test]
    fn test_parse_table() {
        let toml = r#"table = {foo = "bar", baz = "qux", num = 123}"#;
        let table = toml::from_str(toml).unwrap();
        let parser = TomlParser::new(&table);
        let table = parser.parse_table("table").unwrap();
        assert_eq!(table.len(), 3);
        assert_eq!(table.get("foo").unwrap().as_str().unwrap(), "bar");
        assert_eq!(table.get("baz").unwrap().as_str().unwrap(), "qux");
        assert_eq!(table.get("num").unwrap().as_integer().unwrap(), 123);
    }

    #[derive(Deserialize, Debug, PartialEq, Eq)]
    #[serde(untagged)]
    enum TestItem {
        String(String),
        Object { a: String, b: i64 },
    }

    impl FromStr for TestItem {
        type Err = String;

        fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
            Ok(TestItem::String(s.to_string()))
        }
    }

    #[derive(Deserialize, Debug, PartialEq)]
    struct TestStruct {
        #[serde(default, deserialize_with = "deserialize_arr")]
        arr: Vec<TestItem>,
    }

    #[test]
    fn test_deserialize_arr_string() {
        let toml_str = r#"arr = "hello""#;
        let expected = TestStruct {
            arr: vec![TestItem::String("hello".to_string())],
        };
        let actual: TestStruct = toml::from_str(toml_str).unwrap();
        assert_eq!(actual, expected);
    }

    #[test]
    fn test_deserialize_arr_string_list() {
        let toml_str = r#"arr = ["hello", "world"]"#;
        let expected = TestStruct {
            arr: vec![
                TestItem::String("hello".to_string()),
                TestItem::String("world".to_string()),
            ],
        };
        let actual: TestStruct = toml::from_str(toml_str).unwrap();
        assert_eq!(actual, expected);
    }

    #[test]
    fn test_deserialize_arr_map() {
        let toml_str = r#"arr = { a = "foo", b = 123 }"#;
        let expected = TestStruct {
            arr: vec![TestItem::Object {
                a: "foo".to_string(),
                b: 123,
            }],
        };
        let actual: TestStruct = toml::from_str(toml_str).unwrap();
        assert_eq!(actual, expected);
    }

    #[test]
    fn test_deserialize_arr_map_list() {
        let toml_str = r#"
        arr = [
            { a = "foo", b = 123 },
            { a = "bar", b = 456 },
        ]
        "#;
        let expected = TestStruct {
            arr: vec![
                TestItem::Object {
                    a: "foo".to_string(),
                    b: 123,
                },
                TestItem::Object {
                    a: "bar".to_string(),
                    b: 456,
                },
            ],
        };
        let actual: TestStruct = toml::from_str(toml_str).unwrap();
        assert_eq!(actual, expected);
    }

    #[test]
    fn test_deserialize_arr_mixed_list() {
        let toml_str = r#"
        arr = [
            "hello",
            { a = "foo", b = 123 },
        ]
        "#;
        let expected = TestStruct {
            arr: vec![
                TestItem::String("hello".to_string()),
                TestItem::Object {
                    a: "foo".to_string(),
                    b: 123,
                },
            ],
        };
        let actual: TestStruct = toml::from_str(toml_str).unwrap();
        assert_eq!(actual, expected);
    }
}