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:
7 files changed
+468
-9
No files matched your search
@@ -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
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
Reference in new issue
Block a user