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 \