feat(uci): derive UCI schema from settings

Add uci-derive proc-macro crate with UciConfig and UciSection derive
macros that generate UCI schema metadata from Settings structs. The
schema maps UCI section types and option names to config-rs field
paths. UciSource::from_schema() creates a source from the derived
schema, mapping known UCI options to their Settings fields while
preserving unknown options under raw UCI-derived paths.
This commit is contained in:
Nils Andreas Svee committed 2026-08-07 16:04:59 +07:00
1 parent f51d90bff6
commit a6478c9bd4
7 files changed
+468 -9

No files matched your search

+53 -3
View File
@@ -1,5 +1,6 @@
use std::path::{Path, PathBuf};
use crate::settings::uci_schema::{UciConfigSchema, UciSectionMapping};
use config::{ConfigError as SourceError, Map, Source, Value, ValueKind};
use rust_uci::config::{
@@ -16,6 +17,7 @@ enum PackageSelection {
pub(crate) struct UciSource {
packages: PackageSelection,
required: bool,
schema: &'static [UciSectionMapping],
config_dir: Option<PathBuf>,
save_dir: Option<PathBuf>,
}
@@ -25,6 +27,7 @@ impl UciSource {
Self {
packages,
required: true,
schema: &[],
config_dir: None,
save_dir: None,
}
@@ -50,6 +53,18 @@ impl UciSource {
self
}
pub(crate) fn from_schema<T>() -> Self
where
T: UciConfigSchema,
{
Self::new([T::UCI_PACKAGE]).with_schema(T::UCI_SECTIONS)
}
pub(crate) fn with_schema(mut self, schema: &'static [UciSectionMapping]) -> Self {
self.schema = schema;
self
}
#[allow(dead_code)]
pub(crate) fn with_directories(
mut self,
@@ -84,6 +99,10 @@ impl UciSource {
Ok(uci.into())
}
///
/// Converts a [`SectionSelector`] into a string representation.
/// Anonymous sections are converted to `section_type[index]` format,
/// because config-rs does not allow @-prefixed section names.
fn config_selector(selector: &SectionSelector<'_>) -> String {
match selector {
SectionSelector::Named(name) => (*name).to_owned(),
@@ -95,6 +114,32 @@ impl UciSource {
}
}
fn mapped_config_key(
&self,
selector: &SectionSelector<'_>,
uci_option: &str,
) -> Option<String> {
let section_type = match selector {
SectionSelector::Anonymous {
section_type,
index,
} if *index == 0 => *section_type,
SectionSelector::Anonymous { .. } | SectionSelector::Named(_) => return None,
};
self.schema
.iter()
.filter(|section| section.uci_section == section_type)
.find_map(|section| {
section
.options
.iter()
.find(|option| option.uci_option == uci_option)
.map(|option| format!("{}.{}", section.config_section, option.config_field,))
})
}
fn collect_package(
&self,
package_name: &str,
@@ -127,14 +172,19 @@ impl UciSource {
for option in options {
let option_name = option.name();
let uci_relative_key = format!("{selector}.{option_name}");
let config_relative_key =
format!("{}.{option_name}", Self::config_selector(&selector),);
let destination_key = format!("{package_name}.{config_relative_key}");
let uci_relative_key = format!("{selector}.{option_name}");
let uci_key = format!("{package_name}.{uci_relative_key}");
let source_key = format!("{package_name}.{config_relative_key}");
let destination_key = self
.mapped_config_key(&selector, option_name)
.unwrap_or(source_key);
let Some(value) = option.get().map_err(|e| {
SourceError::Message(format!(
"failed to retrieve UCI option \
+14 -6
View File
@@ -9,6 +9,9 @@ use serde::{Deserialize, Deserializer, de};
use std::fmt::{self, Display};
use std::net::IpAddr;
use std::str::FromStr;
use uci_derive::{UciConfig, UciSection};
pub(crate) mod uci_schema;
#[cfg(all(feature = "uci", unix))]
use crate::platform::unix::uci::UciSource;
@@ -70,17 +73,22 @@ struct ReflectorRecord {
description: String,
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, UciConfig)]
#[uci(package = "sqm-autorate-rust")]
pub(crate) struct Settings {
#[uci(section)]
pub(crate) network: NetworkSettings,
#[serde(default)]
#[uci(section)]
pub(crate) output: OutputSettings,
#[serde(default)]
#[uci(section)]
pub(crate) observability: ObservabilitySettings,
#[serde(default)]
#[uci(section)]
pub(crate) advanced_settings: AdvancedSettings,
}
@@ -89,7 +97,7 @@ impl Settings {
let builder = Config::builder();
#[cfg(all(feature = "uci", unix))]
let builder = builder.add_source(UciSource::new(["sqm-autorate-rust"]).required(false));
let builder = builder.add_source(UciSource::from_schema::<Self>().required(false));
#[cfg(feature = "toml")]
let builder = builder.add_source(
@@ -138,7 +146,7 @@ impl Settings {
}
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, UciSection)]
pub(crate) struct NetworkSettings {
pub(crate) download_interface: String,
pub(crate) upload_interface: String,
@@ -160,7 +168,7 @@ impl NetworkSettings {
}
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, UciSection)]
#[serde(default)]
pub(crate) struct OutputSettings {
#[serde(deserialize_with = "deserialize_from_str")]
@@ -181,7 +189,7 @@ impl Default for OutputSettings {
}
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, UciSection)]
#[serde(default)]
pub(crate) struct ObservabilitySettings {
pub(crate) enabled: bool,
@@ -216,7 +224,7 @@ impl Default for ObservabilitySettings {
}
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, UciSection)]
#[serde(default)]
pub(crate) struct AdvancedSettings {
pub(crate) download_delay_ms: f64,
+29
View File
@@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
//
// SPDX-License-Identifier: MPL-2.0
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct UciOptionMapping {
pub(crate) uci_option: &'static str,
pub(crate) config_field: &'static str,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct UciSectionMapping {
/// UCI section type, corresponding to `@section_type[0]`.
pub(crate) uci_section: &'static str,
/// Top-level config-rs / Serde settings key.
pub(crate) config_section: &'static str,
pub(crate) options: &'static [UciOptionMapping],
}
pub(crate) trait UciSectionSchema {
const UCI_OPTIONS: &'static [UciOptionMapping];
}
pub(crate) trait UciConfigSchema {
const UCI_PACKAGE: &'static str;
const UCI_SECTIONS: &'static [UciSectionMapping];
}