cup 1.0.0

A config file lib. You can use it, maybe?
Documentation
use std::fs;
use std::path::Path;
use std::collections::HashSet;

pub mod lexer;
pub mod parser;

pub use lexer::{Lexer, Symbol, LexerError, LexerOptions};
pub use parser::{Parser, Type, Expression, ParserError};

#[derive(Debug, Clone, Default)]
pub struct Config(HashSet<Expression>);

impl Config {
    pub fn parse(path: impl AsRef<Path>, options: LexerOptions) -> Result<Self, Box<dyn std::error::Error>> {
        let contents = fs::read(path)?;

        let mut lexer = lexer::Lexer::new(contents, options);
        let symbols = lexer.lex()?;

        let mut parser = parser::Parser::new(symbols);
        let config = parser.parse()?;

        Ok(config)
    }

    pub fn set(&mut self, expression: Expression) {
        if let Type::Null = expression.value {
            self.0.remove(&expression);
        } else {
            self.0.insert(expression);
        }
    }

    pub fn get(&self, key: impl AsRef<str>) -> Option<&Type> {
        let namespaces = key.as_ref().split('.');

        let mut value: Option<&Type> = None;
        for key in namespaces {
            if let Some(inner) = value {
                if let Type::Dict(dict) = inner {
                    value = dict.0.get(&Expression::new(key, Type::Null)).map(|expr| &expr.value);
                } else {
                    return None;
                }
            } else {
                value = self.0.get(&Expression::new(key, Type::Null)).map(|expr| &expr.value);
            }
        } 

        value
    }

    pub fn get_integer(&self, key: impl AsRef<str>) -> Option<&i64> {
        if let Type::Integer(inner) = self.get(key)? {
            Some(inner)
        } else {
            None
        }
    }

    pub fn get_float(&self, key: impl AsRef<str>) -> Option<&f64> {
        if let Type::Float(inner) = self.get(key)? {
            Some(inner)
        } else {
            None
        }
    }

    pub fn get_boolean(&self, key: impl AsRef<str>) -> Option<&bool> {
        if let Type::Boolean(inner) = self.get(key)? {
            Some(inner)
        } else {
            None
        }
    }

    pub fn get_string(&self, key: impl AsRef<str>) -> Option<&String> {
        if let Type::String(inner) = self.get(key)? {
            Some(inner)
        } else {
            None
        }
    }

    pub fn get_array(&self, key: impl AsRef<str>) -> Option<&Vec<Type>> {
        if let Type::Array(inner) = self.get(key)? {
            Some(inner)
        } else {
            None
        }
    }

    pub fn get_dict(&self, key: impl AsRef<str>) -> Option<&Config> {
        if let Type::Dict(inner) = self.get(key)? {
            Some(inner)
        } else {
            None
        }
    }
}

impl IntoIterator for Config {
    type Item = Expression;
    type IntoIter = <HashSet<Expression> as IntoIterator>::IntoIter;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl std::fmt::Display for Type {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        use Type::*;
        match self {
            Null => write!(f, "\x1b[34mnull\x1b[m")?,
            Integer(number) => write!(f, "\x1b[33m{}\x1b[m", number)?,
            Float(number) => write!(f, "\x1b[33m{}\x1b[m", number)?,
            String(string) => write!(f, "\x1b[32m{:?}\x1b[m", string)?,
            Boolean(bool) => write!(f, "\x1b[31m{}\x1b[m", bool)?,
            Array(array) => {
                write!(f, "[")?;

                for (i, entry) in array.iter().enumerate() {
                    write!(f, "{entry}")?;

                    if i != array.len() - 1 {
                        write!(f, ", ")?;
                    }
                }

                write!(f, "]")?;
            }
            Dict(dict) => {
                writeln!(f, "{{")?;

                for entry in &dict.0 {
                    writeln!(f, "  {}", format!("{entry}").replace('\n', "\n  "))?;
                }

                write!(f, "}}")?;
            }
        };

        Ok(())
    }
}

impl std::fmt::Display for Expression {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} = {}", self.key, self.value)
    }
}

impl std::fmt::Display for Config {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "Config {{")?;
        
        for entry in &self.0 {
            writeln!(f, "  {}", format!("{entry}").replace('\n', "\n  "))?;
        }

        write!(f, "}}")?;

        Ok(())
    }
}

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

    #[test]
    fn config_get() {
        let mut config = Config::default();

        let mut employees = Config::default();
        employees.set(Expression::new("employee-1", Type::String("John Doe".into())));

        let mut clusters = Config::default();
        clusters.set(Expression::new("server-1", Type::String("127.0.0.1".into())));

        let mut instances = Config::default();
        instances.set(Expression::new("heaven-1", Type::Dict(clusters)));

        config.set(Expression::new("cities", Type::Array(Vec::new())));
        config.set(Expression::new("employees", Type::Dict(employees)));
        config.set(Expression::new("clusters", Type::Dict(instances)));

        assert!(config.get("products").is_none());

        assert!(config.get("cities").is_some());

        assert!(config.get("employees").is_some());
        assert!(config.get("employees.employee-1").is_some());
        assert!(config.get("employees.employee-2").is_none());

        assert!(config.get("clusters").is_some());
        assert!(config.get("clusters.heaven-1").is_some());
        assert!(config.get("clusters.heaven-1.server-1").is_some());
        assert!(config.get("clusters.heaven-1.server-2").is_none());
        assert!(config.get("clusters.hell-1").is_none());
        assert!(config.get("clusters.hell-1.devin").is_none());
    }
}