prns-config 0.3.4

Config discovery, parsing, and reconciliation for the Personal Reticulum daemon
Documentation
use std::collections::BTreeSet;
use std::fmt;

use crate::configobj::{ConfigDocument, ConfigError};
use crate::{parse_and_plan_named, ConfigDiagnostic, ConfigFix};

use super::{ConfigEdit, InterfaceConfigKey, InterfaceName};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigRepairReport {
    diagnostics: Vec<ConfigDiagnostic>,
}

impl ConfigRepairReport {
    pub fn analyze(source: &str) -> Result<Self, ConfigRepairError> {
        Self::analyze_named("<config>", source)
    }

    pub fn analyze_named(
        source_name: impl Into<String>,
        source: &str,
    ) -> Result<Self, ConfigRepairError> {
        ConfigDocument::parse(source).map_err(ConfigRepairError::Syntax)?;
        let diagnostics = match parse_and_plan_named(source_name, source) {
            Ok(report) => report.warnings,
            Err(errors) => errors.diagnostics().to_vec(),
        };
        Ok(Self { diagnostics })
    }

    pub fn diagnostics(&self) -> &[ConfigDiagnostic] {
        &self.diagnostics
    }

    pub fn safe_edit(&self) -> Option<ConfigEdit> {
        let mut disabled = BTreeSet::new();
        let mut removed = BTreeSet::new();
        for fix in self
            .diagnostics
            .iter()
            .flat_map(ConfigDiagnostic::fixes)
            .filter(|fix| fix.is_safe())
        {
            match fix {
                ConfigFix::DisableInterface { name } => {
                    if let Ok(name) = InterfaceName::new(name.clone()) {
                        disabled.insert(name);
                    }
                }
                ConfigFix::RemoveValue { path, .. } => {
                    if let Some(target) = interface_value(path) {
                        removed.insert(target);
                    }
                }
                ConfigFix::InsertValue { .. }
                | ConfigFix::ReplaceValue { .. }
                | ConfigFix::ResolveAliases { .. }
                | ConfigFix::ChooseInterfaceType { .. } => {}
            }
        }
        if disabled.is_empty() && removed.is_empty() {
            return None;
        }
        let mut edits = disabled
            .into_iter()
            .map(|name| ConfigEdit::SetEnabled {
                name,
                enabled: false,
            })
            .collect::<Vec<_>>();
        edits.extend(
            removed
                .into_iter()
                .map(|(name, key)| ConfigEdit::RemoveInterfaceValue { name, key }),
        );
        Some(ConfigEdit::Batch(edits))
    }
}

fn interface_value(path: &str) -> Option<(InterfaceName, InterfaceConfigKey)> {
    let start = path.find("[[")? + 2;
    let rest = &path[start..];
    let end = rest.find("]]")?;
    if rest[end + 2..].contains("[[[") {
        return None;
    }
    let name = InterfaceName::new(rest[..end].trim()).ok()?;
    let key = InterfaceConfigKey::new(path.rsplit(" > ").next()?.trim()).ok()?;
    Some((name, key))
}

#[derive(Debug)]
pub enum ConfigRepairError {
    Syntax(ConfigError),
}

impl fmt::Display for ConfigRepairError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Syntax(error) => write!(
                formatter,
                "{error}; malformed ConfigObj syntax is preserved for manual correction"
            ),
        }
    }
}

impl std::error::Error for ConfigRepairError {}