prns-config 0.3.4

Config discovery, parsing, and reconciliation for the Personal Reticulum daemon
Documentation
use std::str::FromStr;

use crate::configobj::{Section, SourceLocations, Value};
use prns_core::interfaces::rnode::multi::{
    HIGH_FREQUENCY_MAX_HZ, HIGH_FREQUENCY_MIN_HZ, LOW_FREQUENCY_MAX_HZ, LOW_FREQUENCY_MIN_HZ,
    MAX_SUBINTERFACES, TX_POWER_MIN_DBM,
};
use prns_core::interfaces::rnode::protocol::TXPOWER_DBM_MAX;

use super::diagnostics::{ErrorCode, ErrorDiagnostic, WarningCode, WarningDiagnostic};
use super::interpret::cleaned_number;
use super::keys::{interface as interface_key, section as section_key};
use super::schema::{rnode_multi_subinterface_key_rule, ValueKind};
use super::validation::{
    location, setting_location, unknown_key, validate_alias_group, validate_value,
    ValidationErrorCollector, ValidationWarnings,
};

const REQUIRED_RADIO_SETTINGS: &[RequiredRadioSetting] = &[
    RequiredRadioSetting {
        key: interface_key::VPORT,
        accepted: "a virtual port from 0 through 10",
        example: "0",
    },
    RequiredRadioSetting {
        key: interface_key::FREQUENCY,
        accepted: "a frequency supported by an RNodeMulti radio",
        example: "868000000",
    },
    RequiredRadioSetting {
        key: interface_key::BANDWIDTH,
        accepted: "a radio bandwidth",
        example: "125000",
    },
    RequiredRadioSetting {
        key: interface_key::TXPOWER,
        accepted: "a transmit power",
        example: "7",
    },
    RequiredRadioSetting {
        key: interface_key::SPREADINGFACTOR,
        accepted: "a LoRa spreading factor",
        example: "8",
    },
    RequiredRadioSetting {
        key: interface_key::CODINGRATE,
        accepted: "a LoRa coding rate",
        example: "5",
    },
];

struct RequiredRadioSetting {
    key: &'static str,
    accepted: &'static str,
    example: &'static str,
}

struct SubinterfaceContext<'a> {
    source: &'a str,
    parent: &'a str,
    name: &'a str,
    section: &'a Section,
    locations: &'a SourceLocations,
}

impl<'a> SubinterfaceContext<'a> {
    fn into_enabled(
        self,
        warnings: &mut ValidationWarnings,
        errors: &mut ValidationErrorCollector,
    ) -> Option<EnabledSubinterface<'a>> {
        let source_path = self.source_path();
        let display_path = self.display_path();
        let enabled = validate_alias_group(
            self.source,
            &source_path,
            &display_path,
            self.section,
            self.locations,
            interface_key::INTERFACE_ENABLED,
            interface_key::ENABLED_ALIASES,
            ValueKind::Bool,
            warnings,
            errors,
        );
        (enabled.as_deref() == Some("true")).then_some(EnabledSubinterface {
            context: self,
            source_path,
            display_path,
        })
    }

    fn source_path(&self) -> [&'a str; 3] {
        [section_key::INTERFACES, self.parent, self.name]
    }

    fn display_path(&self) -> String {
        format!(
            "[interfaces] > [[{parent}]] > [[[{name}]]]",
            parent = self.parent,
            name = self.name
        )
    }
}

struct EnabledSubinterface<'a> {
    context: SubinterfaceContext<'a>,
    source_path: [&'a str; 3],
    display_path: String,
}

impl EnabledSubinterface<'_> {
    fn validate(
        &self,
        assigned_vports: &mut AssignedVports,
        warnings: &mut ValidationWarnings,
        errors: &mut ValidationErrorCollector,
    ) {
        self.validate_values(warnings, errors);
        self.require_radio_settings(errors);
        self.register_vport(assigned_vports, errors);
        self.warn_about_nested_sections(warnings);
    }

    fn validate_values(
        &self,
        warnings: &mut ValidationWarnings,
        errors: &mut ValidationErrorCollector,
    ) {
        for (key, value) in &self.context.section.scalars {
            if interface_key::ENABLED_ALIASES.contains(&key.as_str()) {
                continue;
            }
            match rnode_multi_subinterface_key_rule(key) {
                Some(rule) => validate_value(
                    self.context.source,
                    setting_location(self.context.locations, &self.source_path, key),
                    format!("{} > {key}", self.display_path),
                    key,
                    value,
                    rule.value_kind(),
                    errors,
                ),
                None => warnings.push(unknown_key(
                    self.context.source,
                    setting_location(self.context.locations, &self.source_path, key),
                    format!("{} > {key}", self.display_path),
                    key,
                    value,
                    interface_key::RNODE_MULTI_SUBINTERFACE,
                )),
            }
        }
    }

    fn require_radio_settings(&self, errors: &mut ValidationErrorCollector) {
        for required in REQUIRED_RADIO_SETTINGS {
            if self.context.section.get(required.key).is_some() {
                continue;
            }
            errors.push(ErrorDiagnostic::new(
                ErrorCode::MissingRequiredKey,
                self.context.source,
                location(self.context.locations, &self.source_path),
                format!("{} > {}", self.display_path, required.key),
                None,
                format!(
                    "enabled RNodeMulti subinterface is missing {}",
                    required.accepted
                ),
                Some(required.accepted.to_string()),
                format!(
                    "add `{} = {}` under {}",
                    required.key, required.example, self.display_path
                ),
            ));
        }
    }

    fn register_vport(
        &self,
        assigned_vports: &mut AssignedVports,
        errors: &mut ValidationErrorCollector,
    ) {
        let Some(vport) = self
            .context
            .section
            .get(interface_key::VPORT)
            .and_then(Value::as_scalar)
            .and_then(VirtualPort::parse)
        else {
            return;
        };
        let Err(duplicate) = assigned_vports.register(vport, self.context.name) else {
            return;
        };
        errors.push(ErrorDiagnostic::new(
            ErrorCode::InvalidValue,
            self.context.source,
            setting_location(
                self.context.locations,
                &self.source_path,
                interface_key::VPORT,
            ),
            format!("{} > {}", self.display_path, interface_key::VPORT),
            Some(vport.value().to_string()),
            format!(
                "vport {} is already assigned to subinterface {:?}",
                vport.value(),
                duplicate.first
            ),
            Some("a unique integer from 0 through 10".to_string()),
            format!(
                "set `{} = {}`",
                interface_key::VPORT,
                duplicate.replacement.value()
            ),
        ));
    }

    fn warn_about_nested_sections(&self, warnings: &mut ValidationWarnings) {
        for (nested, _) in &self.context.section.sections {
            warnings.push(WarningDiagnostic::new(
                WarningCode::UnknownSection,
                self.context.source,
                location(
                    self.context.locations,
                    &[
                        section_key::INTERFACES,
                        self.context.parent,
                        self.context.name,
                        nested,
                    ],
                ),
                format!("{} > [[[[{nested}]]]]", self.display_path),
                Some(nested.clone()),
                "sections cannot be nested beneath an RNodeMulti subinterface",
                None,
                format!("remove [[[[{nested}]]]] from [[[{}]]]", self.context.name),
            ));
        }
    }
}

#[derive(Default)]
struct AssignedVports([Option<String>; MAX_SUBINTERFACES]);

impl AssignedVports {
    fn register(&mut self, vport: VirtualPort, subinterface: &str) -> Result<(), DuplicateVport> {
        let first = match &self.0[vport.index()] {
            Some(first) => first.clone(),
            None => {
                self.0[vport.index()] = Some(subinterface.to_string());
                return Ok(());
            }
        };
        let replacement = self
            .0
            .iter()
            .position(Option::is_none)
            .and_then(|index| u8::try_from(index).ok())
            .map(VirtualPort)
            .unwrap_or(vport);
        Err(DuplicateVport { first, replacement })
    }
}

struct DuplicateVport {
    first: String,
    replacement: VirtualPort,
}

#[derive(Clone, Copy)]
struct VirtualPort(u8);

impl VirtualPort {
    fn parse(value: &str) -> Option<Self> {
        parse_number(value)
            .filter(|value| usize::from(*value) < MAX_SUBINTERFACES)
            .map(Self)
    }

    fn index(self) -> usize {
        usize::from(self.0)
    }

    fn value(self) -> u8 {
        self.0
    }
}

pub(super) fn validate_subinterfaces(
    source: &str,
    parent: &str,
    section: &Section,
    locations: &SourceLocations,
    warnings: &mut ValidationWarnings,
    errors: &mut ValidationErrorCollector,
) {
    let mut assigned_vports = AssignedVports::default();
    let mut has_enabled_subinterface = false;
    for (name, child) in &section.sections {
        let context = SubinterfaceContext {
            source,
            parent,
            name,
            section: child,
            locations,
        };
        let Some(subinterface) = context.into_enabled(warnings, errors) else {
            continue;
        };
        has_enabled_subinterface = true;
        subinterface.validate(&mut assigned_vports, warnings, errors);
    }
    if !has_enabled_subinterface {
        require_enabled_subinterface(source, parent, section, locations, errors);
    }
}

fn require_enabled_subinterface(
    source: &str,
    parent: &str,
    section: &Section,
    locations: &SourceLocations,
    errors: &mut ValidationErrorCollector,
) {
    let reason = if section.sections.is_empty() {
        "enabled RNodeMulti interface has no configured subinterfaces"
    } else {
        "enabled RNodeMulti interface has no enabled subinterfaces"
    };
    errors.push(ErrorDiagnostic::new(
        ErrorCode::MissingRequiredKey,
        source,
        location(locations, &[section_key::INTERFACES, parent]),
        format!("[interfaces] > [[{parent}]] > [[[subinterface]]]"),
        None,
        reason,
        Some("at least one enabled [[[subinterface]]] section".to_string()),
        format!(
            "add `[[[Radio 0]]]` under [[{parent}]] with `interface_enabled = Yes`, `vport = 0`, and its radio settings"
        ),
    ));
}

pub(super) fn semantic_value_is_valid(kind: ValueKind, text: &str) -> Option<bool> {
    match kind {
        ValueKind::RnodeMultiVport => Some(VirtualPort::parse(text).is_some()),
        ValueKind::RnodeMultiFrequency => Some(parse_number::<u64>(text).is_some_and(|value| {
            (u64::from(LOW_FREQUENCY_MIN_HZ)..=u64::from(LOW_FREQUENCY_MAX_HZ)).contains(&value)
                || (u64::from(HIGH_FREQUENCY_MIN_HZ)..=u64::from(HIGH_FREQUENCY_MAX_HZ))
                    .contains(&value)
        })),
        ValueKind::RnodeMultiTxPower => Some(
            parse_number::<i16>(text)
                .is_some_and(|value| (TX_POWER_MIN_DBM..=TXPOWER_DBM_MAX).contains(&value)),
        ),
        _ => None,
    }
}

fn parse_number<T>(text: &str) -> Option<T>
where
    T: FromStr,
{
    cleaned_number(text.trim())?.parse().ok()
}