hbox 0.7.1

CLI tool that leverages container technology to manage packages.
Documentation
use log::LevelFilter;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::error::Error;
use std::str::FromStr;

use crate::configs::app::AppConfig;
use crate::serialization::{parse_json, save_json};

pub struct UserConfig {}

impl UserConfig {
    pub fn load() -> Result<Root, Box<dyn Error>> {
        let config = AppConfig::load();
        if config.config_file_path().exists() {
            parse_json(&config.config_file_path())
        } else {
            let root = Root::default();
            save_json(&root, &config.config_file_path())?;
            Ok(root)
        }
    }

    pub fn save(root: Root) -> Result<(), Box<dyn Error>> {
        let config = AppConfig::load();
        save_json(&root, &config.config_file_path())?;
        Ok(())
    }

    pub fn write_config_value(path: &str, value: &str) -> Result<(), Box<dyn Error>> {
        let mut root: Value = serde_json::to_value(Self::load()?)?;
        let (current, last_part) = Self::traverse_path(&mut root, path)?;

        let parsed_value =
            if value.eq_ignore_ascii_case("true") || value.eq_ignore_ascii_case("false") {
                Value::Bool(value.parse::<bool>()?)
            } else if let Ok(int_value) = value.parse::<i64>() {
                Value::Number(int_value.into())
            } else {
                Value::String(value.to_string())
            };

        match current {
            Value::Object(map) => {
                if map.contains_key(&last_part) {
                    map.insert(last_part, parsed_value);
                } else {
                    return Err(format!("Invalid configuration key: {}", path).into());
                }
            }
            _ => return Err("Invalid configuration path".into()),
        }

        let updated_root: Root = serde_json::from_value(root)?;
        Self::save(updated_root)?;

        Ok(())
    }

    pub fn read_config_value(path: &str) -> Result<(), Box<dyn Error>> {
        let mut root: Value = serde_json::to_value(Self::load()?)?;
        let (current, last_part) = Self::traverse_path(&mut root, path)?;

        match current.get(&last_part) {
            Some(value) => {
                if value.is_object() {
                    println!("{}", serde_json::to_string_pretty(&value)?);
                } else {
                    println!("{}", value);
                }
            }
            None => return Err(format!("Invalid configuration key: {}", path).into()),
        }

        Ok(())
    }

    fn traverse_path<'a>(
        root: &'a mut Value,
        path: &str,
    ) -> Result<(&'a mut Value, String), Box<dyn Error>> {
        let parts: Vec<&str> = path.split('.').collect();
        if parts.is_empty() {
            return Err("Invalid configuration path".into());
        }

        let mut current = root;
        for part in parts.iter().take(parts.len() - 1) {
            current = current.get_mut(*part).ok_or("Invalid configuration path")?;
        }

        let last_part = parts
            .last()
            .ok_or("Invalid configuration path")?
            .to_string();
        Ok((current, last_part))
    }
}

#[derive(Serialize, Deserialize, Debug, Default)]
pub struct Root {
    pub engine: Engine,
    pub logs: Logs,
    pub experimental: Experimental,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum Engine {
    Docker,
    Podman,
}

impl Default for Engine {
    fn default() -> Self {
        Self::Docker
    }
}

impl Engine {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Docker => "docker",
            Self::Podman => "podman",
        }
    }
}

impl FromStr for Engine {
    type Err = ();

    fn from_str(input: &str) -> Result<Engine, Self::Err> {
        match input.to_lowercase().as_str() {
            "docker" => Ok(Engine::Docker),
            "podman" => Ok(Engine::Podman),
            _ => Err(()),
        }
    }
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Logs {
    #[serde(default)]
    pub enabled: bool,
    pub level: Level,
    pub strategy: Strategy,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum Level {
    Trace,
    Debug,
    Info,
    Warn,
    Error,
}

#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Strategy {
    Truncate,
    Append,
}

#[derive(Serialize, Deserialize, Debug, Default)]
pub struct Experimental {
    #[serde(default)]
    pub capture_stdout: bool,
    #[serde(default)]
    pub capture_stderr: bool,
}

impl Default for Logs {
    fn default() -> Self {
        Self {
            enabled: false,
            level: Level::Info,
            strategy: Strategy::Append,
        }
    }
}

impl Level {
    pub fn to_level_filter(&self) -> LevelFilter {
        match self {
            Level::Info => LevelFilter::Info,
            Level::Warn => LevelFilter::Warn,
            Level::Debug => LevelFilter::Debug,
            Level::Trace => LevelFilter::Trace,
            _ => LevelFilter::Info,
        }
    }
}

impl FromStr for Level {
    type Err = ();

    fn from_str(input: &str) -> Result<Level, Self::Err> {
        match input {
            "trace" => Ok(Level::Trace),
            "debug" => Ok(Level::Debug),
            "info" => Ok(Level::Info),
            "warn" => Ok(Level::Warn),
            "error" => Ok(Level::Error),
            _ => Err(()),
        }
    }
}

impl FromStr for Strategy {
    type Err = ();

    fn from_str(input: &str) -> Result<Strategy, Self::Err> {
        match input {
            "truncate" => Ok(Strategy::Truncate),
            "append" => Ok(Strategy::Append),
            _ => Err(()),
        }
    }
}