From 41929875f7494382e10c4bd3fa8fccfd0c7e4234 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Tue, 13 Oct 2026 21:24:04 +0700 Subject: [PATCH] feat(uci): support repeated sections and skipped fields Allow structured lists to share the existing UCI configuration adapter while keeping collection fields out of scalar section mappings. --- crates/uci-derive/src/lib.rs | 63 +++++++++++++++++++++--------- src/settings/uci.rs | 74 +++++++++++++++++++++++++++++++++--- src/settings/uci_schema.rs | 12 ++++++ 3 files changed, 124 insertions(+), 25 deletions(-) diff --git a/crates/uci-derive/src/lib.rs b/crates/uci-derive/src/lib.rs index 2329174..526f058 100644 --- a/crates/uci-derive/src/lib.rs +++ b/crates/uci-derive/src/lib.rs @@ -9,13 +9,13 @@ use syn::{Data, DeriveInput, Field, Fields, FieldsNamed, LitStr, Token, parse_ma /// Generates `UciSectionSchema` from a settings struct. /// -/// Every named field is mapped one-to-one: +/// Every named field without `#[uci(skip)]` is mapped one-to-one: /// /// ```text /// Rust field name -> UCI option name /// Rust field name -> config-rs field name /// ``` -#[proc_macro_derive(UciSection)] +#[proc_macro_derive(UciSection, attributes(uci))] pub fn derive_uci_section(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -56,26 +56,28 @@ fn expand_uci_section(input: &DeriveInput) -> syn::Result>(); + }); + } let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl(); @@ -96,6 +98,29 @@ fn expand_uci_section(input: &DeriveInput) -> syn::Result syn::Result { + let mut skip = false; + + for attribute in &field.attrs { + if !attribute.path().is_ident("uci") { + continue; + } + + attribute.parse_nested_meta(|meta| { + if !meta.path.is_ident("skip") { + return Err(meta.error("expected `skip`")); + } + if skip { + return Err(meta.error("duplicate `skip` attribute")); + } + skip = true; + Ok(()) + })?; + } + + Ok(skip) +} + fn expand_uci_config(input: &DeriveInput) -> syn::Result { let struct_name = &input.ident; let fields = require_named_fields(input)?; diff --git a/src/settings/uci.rs b/src/settings/uci.rs index 1646b67..e35d8e6 100644 --- a/src/settings/uci.rs +++ b/src/settings/uci.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -use crate::settings::uci_schema::{UciConfigSchema, UciSectionMapping}; +use crate::settings::uci_schema::{UciConfigSchema, UciRepeatedSectionMapping, UciSectionMapping}; use config::{ConfigError as SourceError, Map, Source, Value, ValueKind}; use rust_uci::config::{ @@ -18,6 +18,7 @@ pub(crate) struct UciSource { packages: PackageSelection, required: bool, schema: &'static [UciSectionMapping], + repeated_schema: &'static [UciRepeatedSectionMapping], config_dir: Option, save_dir: Option, } @@ -28,6 +29,7 @@ impl UciSource { packages, required: true, schema: &[], + repeated_schema: &[], config_dir: None, save_dir: None, } @@ -65,6 +67,17 @@ impl UciSource { self } + /// Registers section types collected as arrays of tables at their config paths. + /// Absent section types leave the destination unset for Serde/source defaults. + #[allow(dead_code)] + pub(crate) fn with_repeated_schema( + mut self, + schema: &'static [UciRepeatedSectionMapping], + ) -> Self { + self.repeated_schema = schema; + self + } + #[allow(dead_code)] pub(crate) fn with_directories( mut self, @@ -145,6 +158,7 @@ impl UciSource { package_name: &str, package: &UciPackage, values: &mut Map, + repeated_values: &mut Map>, ) -> Result<(), SourceError> { let sections = package.sections().map_err(|e| { SourceError::Message(format!( @@ -154,6 +168,12 @@ impl UciSource { })?; for section in sections { + let repeated_mapping = self + .repeated_schema + .iter() + .find(|mapping| mapping.uci_section == section.type_()); + let mut section_values = Map::new(); + let selector = section.selector().ok_or_else(|| { SourceError::Message(format!( "loaded UCI section of type {:?} in package \ @@ -181,9 +201,21 @@ impl UciSource { let source_key = format!("{package_name}.{config_relative_key}"); - let destination_key = self - .mapped_config_key(&selector, option_name) - .unwrap_or(source_key); + let repeated_option = repeated_mapping.and_then(|mapping| { + mapping + .options + .iter() + .find(|option| option.uci_option == option_name) + }); + + let destination_key = if let Some(mapping) = repeated_option { + mapping.config_field.to_owned() + } else if repeated_mapping.is_some() { + option_name.to_owned() + } else { + self.mapped_config_key(&selector, option_name) + .unwrap_or(source_key) + }; let Some(value) = option.get().map_err(|e| { SourceError::Message(format!( @@ -210,13 +242,27 @@ impl UciSource { } }; - if values.insert(destination_key.clone(), value).is_some() { + let destination = if repeated_mapping.is_some() { + &mut section_values + } else { + &mut *values + }; + + if destination.insert(destination_key.clone(), value).is_some() { return Err(SourceError::Message(format!( "duplicate UCI destination key \ {destination_key:?} while mapping {uci_key:?}" ))); } } + + if let Some(mapping) = repeated_mapping { + let origin = format!("UCI section {package_name}.{selector}"); + repeated_values + .entry(mapping.config_path.to_owned()) + .or_default() + .push(Value::new(Some(&origin), ValueKind::Table(section_values))); + } } Ok(()) @@ -272,6 +318,7 @@ impl Source for UciSource { }; let mut values = Map::new(); + let mut repeated_values = Map::new(); for package in packages { let package_name = package @@ -279,7 +326,22 @@ impl Source for UciSource { .map_err(|e| SourceError::Message(format!("failed to read UCI package name: {e}")))? .to_owned(); - self.collect_package(&package_name, &package, &mut values)?; + self.collect_package(&package_name, &package, &mut values, &mut repeated_values)?; + } + + for (config_path, sections) in repeated_values { + let origin = "UCI repeated sections".to_owned(); + if values + .insert( + config_path.clone(), + Value::new(Some(&origin), ValueKind::Array(sections)), + ) + .is_some() + { + return Err(SourceError::Message(format!( + "duplicate UCI destination key {config_path:?} while mapping repeated sections" + ))); + } } Ok(values) diff --git a/src/settings/uci_schema.rs b/src/settings/uci_schema.rs index aedd51d..2cfd737 100644 --- a/src/settings/uci_schema.rs +++ b/src/settings/uci_schema.rs @@ -19,6 +19,18 @@ pub(crate) struct UciSectionMapping { pub(crate) options: &'static [UciOptionMapping], } +#[cfg(all(feature = "uci", unix))] +#[derive(Clone, Copy, Debug)] +pub(crate) struct UciRepeatedSectionMapping { + /// UCI section type, for both named and anonymous sections. + pub(crate) uci_section: &'static str, + + /// Config-rs / Serde path receiving an array of section tables. + pub(crate) config_path: &'static str, + + pub(crate) options: &'static [UciOptionMapping], +} + pub(crate) trait UciSectionSchema { const UCI_OPTIONS: &'static [UciOptionMapping]; }