gtoml 0.1.2

Get TOML values quickly
Documentation
use regex::Regex;
use toml::Value;

/// Parses a TOML string and returns a `Value` type.
///
/// # Arguments
/// * `toml_str` - A reference to a string containing TOML data.
///
/// # Returns
/// * `Result<Value, toml::de::Error>` - Returns a `Value` if parsing is successful,
///   otherwise returns a `toml::de::Error`.
pub fn parse(toml_str: &str) -> Result<Value, toml::de::Error> {
    toml::from_str(toml_str)
}

/// Retrieves a value from a TOML `Value` at a specified path.
///
/// # Arguments
/// * `value` - A reference to the TOML `Value` to search in.
/// * `path` - A reference to a string representing the path to the desired value.
///
/// # Returns
/// * `Result<Option<Value>, String>` - Returns `Ok(Some(Value))` if the value is found,
///   `Ok(None)` if the path does not exist, or an `Err` with an error message if there is an issue.
pub fn get<'a>(value: &'a Value, path: &str) -> Result<Option<Value>, String> {
    let mut current = value;
    for part in path.split('.') {
        current = match current {
            // If the current value is a table
            Value::Table(map) => {
                if part.contains('*') || part.contains('?') {
                    // Replace wildcard characters with regex patterns
                    let pattern = part.replace('*', ".*").replace('?', ".");
                    let regex = Regex::new(&pattern).map_err(|e| e.to_string())?;
                    // Filter and collect values that match the regex pattern
                    let results: Vec<_> = map
                       .iter()
                       .filter(|(key, _)| regex.is_match(key))
                       .map(|(_, value)| value.clone())
                       .collect();
                    return Ok(Some(Value::Array(results)));
                } else {
                    // Get the value at the specified key
                    map.get(part).ok_or_else(|| format!("Key not found: {}", part))?
                }
            }
            // If the current value is an array
            Value::Array(arr) => {
                if part == "#" {
                    // Return the length of the array
                    return Ok(Some(Value::Integer(arr.len() as i64)));
                } else if part.contains('*') || part.contains('?') {
                    // Replace wildcard characters with regex patterns
                    let pattern = part.replace('*', ".*").replace('?', ".");
                    let regex = Regex::new(&pattern).map_err(|e| e.to_string())?;
                    // Filter and collect values that match the regex pattern
                    let results: Vec<_> = arr
                       .iter()
                       .filter(|item| regex.is_match(&item.to_string()))
                       .cloned()
                       .collect();
                    return Ok(Some(Value::Array(results)));
                } else {
                    if let Ok(index) = part.parse::<usize>() {
                        // Get the value at the specified index
                        arr.get(index).ok_or_else(|| format!("Index out of bounds: {}", index))?
                    } else {
                        return Ok(None);
                    }
                }
            }
            // If the current value is neither a table nor an array
            _ => return Ok(None),
        };
    }
    Ok(Some(current.clone()))
}

/// Retrieves a value from a TOML string at a specified path.
///
/// # Arguments
/// * `toml_str` - A reference to a string containing TOML data.
/// * `path` - A reference to a string representing the path to the desired value.
///
/// # Returns
/// * `Result<Option<Value>, String>` - Returns `Ok(Some(Value))` if the value is found,
///   `Ok(None)` if the path does not exist, or an `Err` with an error message if there is an issue.
pub fn get_from_str(toml_str: &str, path: &str) -> Result<Option<Value>, String> {
    let value = parse(toml_str).map_err(|e| e.to_string())?;
    get(&value, path)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Test the `parse` and `get` functions with a simple TOML string.
    #[test]
    fn test_parse_and_get() {
        let toml_str = r#"
            [owner]
            name = "Tom Preston-Werner"
            dob = 1979-05-27T07:32:00Z
        "#;

        let value = parse(toml_str).unwrap();
        assert_eq!(
            get(&value, "owner.name").unwrap(),
            Some(Value::String("Tom Preston-Werner".to_string()))
        );
        assert_eq!(
            get(&value, "owner.dob").unwrap().unwrap().to_string(),
            "1979-05-27T07:32:00Z"
        );
    }

    /// Test the `get_from_str` function with a simple TOML string.
    #[test]
    fn test_get_from_str() {
        let toml_str = r#"
            [owner]
            name = "Tom Preston-Werner"
            dob = 1979-05-27T07:32:00Z
        "#;

        assert_eq!(
            get_from_str(toml_str, "owner.name").unwrap(),
            Some(Value::String("Tom Preston-Werner".to_string()))
        );
        assert_eq!(
            get_from_str(toml_str, "owner.dob")
               .unwrap()
               .unwrap()
               .to_string(),
            "1979-05-27T07:32:00Z"
        );
    }

    /// Test the `get` function with an array in a TOML string.
    #[test]
    fn test_get_array() {
        let toml_str = r#"
            [[people]]
            name = "Alice"
            age = 30

            [[people]]
            name = "Bob"
            age = 25
        "#;

        let value = parse(toml_str).unwrap();
        assert_eq!(
            get(&value, "people.0.name").unwrap(),
            Some(Value::String("Alice".to_string()))
        );
        assert_eq!(
            get(&value, "people.1.age").unwrap(),
            Some(Value::Integer(25))
        );
    }

    /// Test the `get` function with a nested array in a TOML string.
    #[test]
    fn test_nested_array() {
        let toml_str = r#"
            [[matrix]]
            values = [1, 2, 3]

            [[matrix]]
            values = [4, 5, 6]
        "#;

        let value = parse(toml_str).unwrap();
        assert_eq!(
            get(&value, "matrix.0.values.1").unwrap(),
            Some(Value::Integer(2))
        );
        assert_eq!(
            get(&value, "matrix.1.values.2").unwrap(),
            Some(Value::Integer(6))
        );
    }

    /// Test the `get` function with a complex path in a TOML string.
    #[test]
    fn test_complex_path() {
        let toml_str = r#"
            [group]
            [[group.people]]
            name = "Alice"
            age = 30

            [[group.people]]
            name = "Bob"
            age = 25
        "#;

        let value = parse(toml_str).unwrap();
        assert_eq!(
            get(&value, "group.people.0.name").unwrap(),
            Some(Value::String("Alice".to_string()))
        );
        assert_eq!(
            get(&value, "group.people.1.age").unwrap(),
            Some(Value::Integer(25))
        );
    }

    /// Test the `get` function to get the length of an array in a TOML string.
    #[test]
    fn test_get_array_length() {
        let toml_str = r#"
            children = ["Sara", "Alex", "Jack"]
        "#;

        let value = parse(toml_str).unwrap();
        assert_eq!(get(&value, "children.#").unwrap(), Some(Value::Integer(3)));
    }

    /// Test the `get` function with a wildcard in a TOML string.
    #[test]
    fn test_get_array_wildcard() {
        let toml_str = r#"
            children = ["Sara", "Alex", "Jack"]
        "#;

        let value = parse(toml_str).unwrap();
        assert_eq!(
            get(&value, "children.2").unwrap(),
            Some(Value::String("Jack".to_string()))
        );
    }

    /// Test the `get` function with a question mark in a TOML string.
    #[test]
    fn test_get_array_question_mark() {
        let toml_str = r#"
            children = ["Sara", "Alex", "Jack"]
        "#;

        let value = parse(toml_str).unwrap();
        assert_eq!(
            get(&value, "children.0").unwrap(),
            Some(Value::String("Sara".to_string()))
        );
    }

    /// Test the `get` function with a nested array to get specific values in a TOML string.
    #[test]
    fn test_get_nested_array() {
        let toml_str = r#"
            friends = [
                { first = "James", last = "Murphy" },
                { first = "Roger", last = "Craig" }
            ]
        "#;

        let value = parse(toml_str).unwrap();
        let result = get(&value, "friends").unwrap().unwrap();
        if let Value::Array(arr) = result {
            let first_names: Vec<Value> = arr
               .iter()
               .filter_map(|item| {
                    if let Value::Table(map) = item {
                        map.get("first").cloned()
                    } else {
                        None
                    }
                })
               .collect();
            assert_eq!(
                first_names,
                vec![
                    Value::String("James".to_string()),
                    Value::String("Roger".to_string())
                ]
            );
        } else {
            panic!("Expected an array");
        }
    }
}