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
Generated
+99
@@ -133,6 +133,12 @@ version = "1.16.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -187,6 +193,12 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icmp-socket2"
|
name = "icmp-socket2"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -196,6 +208,16 @@ dependencies = [
|
|||||||
"socket2",
|
"socket2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@@ -471,6 +493,19 @@ dependencies = [
|
|||||||
"syn 3.0.2",
|
"syn 3.0.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.151"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -529,6 +564,7 @@ dependencies = [
|
|||||||
"rustix",
|
"rustix",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"uci-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -553,6 +589,21 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "target-triple"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termcolor"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.19"
|
version = "2.0.19"
|
||||||
@@ -579,10 +630,12 @@ version = "1.1.3+spec-1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c"
|
checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -604,6 +657,37 @@ dependencies = [
|
|||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trybuild"
|
||||||
|
version = "1.0.120"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"target-triple",
|
||||||
|
"termcolor",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uci-derive"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
"trybuild",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
@@ -655,6 +739,15 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -678,3 +771,9 @@ checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
@@ -29,6 +29,7 @@ flume = { version = "0.12.0", default-features = false, features = ["eventual-f
|
|||||||
config = { version = "0.15.25", default-features = false }
|
config = { version = "0.15.25", default-features = false }
|
||||||
csv = "1.4.0"
|
csv = "1.4.0"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
uci-derive = { path = "crates/uci-derive" }
|
||||||
|
|
||||||
[dependencies.log]
|
[dependencies.log]
|
||||||
features = ["std"]
|
features = ["std"]
|
||||||
@@ -46,3 +47,7 @@ default = ["toml", "uci-vendored"]
|
|||||||
toml = ["config/toml"]
|
toml = ["config/toml"]
|
||||||
uci = ["dep:rust-uci"]
|
uci = ["dep:rust-uci"]
|
||||||
uci-vendored = ["uci", "rust-uci/vendored"]
|
uci-vendored = ["uci", "rust-uci/vendored"]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["crates/uci-derive"]
|
||||||
|
resolver = "3"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "uci-derive"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = "1"
|
||||||
|
quote = "1"
|
||||||
|
syn = { version = "2", features = ["full"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
trybuild = "1"
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::ext::IdentExt;
|
||||||
|
use syn::{Data, DeriveInput, Field, Fields, FieldsNamed, LitStr, Token, parse_macro_input};
|
||||||
|
|
||||||
|
/// Generates `UciSectionSchema` from a settings struct.
|
||||||
|
///
|
||||||
|
/// Every named field is mapped one-to-one:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// Rust field name -> UCI option name
|
||||||
|
/// Rust field name -> config-rs field name
|
||||||
|
/// ```
|
||||||
|
#[proc_macro_derive(UciSection)]
|
||||||
|
pub fn derive_uci_section(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
|
||||||
|
match expand_uci_section(&input) {
|
||||||
|
Ok(tokens) => tokens.into(),
|
||||||
|
Err(error) => error.to_compile_error().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates `UciConfigSchema` from the root settings struct.
|
||||||
|
///
|
||||||
|
/// Required container attribute:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// #[uci(package = "sqm-autorate-rust")]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Every field must specify its UCI section:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// #[uci(section)]
|
||||||
|
/// network: NetworkSettings,
|
||||||
|
///
|
||||||
|
/// #[uci(section = "advanced_settings")]
|
||||||
|
/// advanced: AdvancedSettings,
|
||||||
|
/// ```
|
||||||
|
#[proc_macro_derive(UciConfig, attributes(uci))]
|
||||||
|
pub fn derive_uci_config(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
|
||||||
|
match expand_uci_config(&input) {
|
||||||
|
Ok(tokens) => tokens.into(),
|
||||||
|
Err(error) => error.to_compile_error().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_uci_section(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
||||||
|
let struct_name = &input.ident;
|
||||||
|
let fields = require_named_fields(input)?;
|
||||||
|
|
||||||
|
let mappings = fields
|
||||||
|
.named
|
||||||
|
.iter()
|
||||||
|
.map(|field| {
|
||||||
|
let field_ident = field
|
||||||
|
.ident
|
||||||
|
.as_ref()
|
||||||
|
.expect("named fields always have identifiers");
|
||||||
|
|
||||||
|
let field_name = field_ident.unraw().to_string();
|
||||||
|
let field_name = LitStr::new(&field_name, field_ident.span());
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
crate::settings::uci_schema::UciOptionMapping {
|
||||||
|
uci_option: #field_name,
|
||||||
|
config_field: #field_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#[automatically_derived]
|
||||||
|
impl #impl_generics
|
||||||
|
crate::settings::uci_schema::UciSectionSchema
|
||||||
|
for #struct_name #type_generics
|
||||||
|
#where_clause
|
||||||
|
{
|
||||||
|
const UCI_OPTIONS: &'static [
|
||||||
|
crate::settings::uci_schema::UciOptionMapping
|
||||||
|
] = &[
|
||||||
|
#(#mappings),*
|
||||||
|
];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_uci_config(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
||||||
|
let struct_name = &input.ident;
|
||||||
|
let fields = require_named_fields(input)?;
|
||||||
|
let package = parse_package_name(input)?;
|
||||||
|
|
||||||
|
let mut sections = Vec::with_capacity(fields.named.len());
|
||||||
|
|
||||||
|
for field in &fields.named {
|
||||||
|
let field_ident = field
|
||||||
|
.ident
|
||||||
|
.as_ref()
|
||||||
|
.expect("named fields always have identifiers");
|
||||||
|
|
||||||
|
let field_type = &field.ty;
|
||||||
|
|
||||||
|
let config_section_name = field_ident.unraw().to_string();
|
||||||
|
let config_section_name = LitStr::new(&config_section_name, field_ident.span());
|
||||||
|
|
||||||
|
let uci_section_name = parse_section_name(field)?;
|
||||||
|
|
||||||
|
sections.push(quote! {
|
||||||
|
crate::settings::uci_schema::UciSectionMapping {
|
||||||
|
uci_section: #uci_section_name,
|
||||||
|
config_section: #config_section_name,
|
||||||
|
options: <
|
||||||
|
#field_type as
|
||||||
|
crate::settings::uci_schema::UciSectionSchema
|
||||||
|
>::UCI_OPTIONS,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#[automatically_derived]
|
||||||
|
impl #impl_generics
|
||||||
|
crate::settings::uci_schema::UciConfigSchema
|
||||||
|
for #struct_name #type_generics
|
||||||
|
#where_clause
|
||||||
|
{
|
||||||
|
const UCI_PACKAGE: &'static str = #package;
|
||||||
|
|
||||||
|
const UCI_SECTIONS: &'static [
|
||||||
|
crate::settings::uci_schema::UciSectionMapping
|
||||||
|
] = &[
|
||||||
|
#(#sections),*
|
||||||
|
];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_named_fields(input: &DeriveInput) -> syn::Result<&FieldsNamed> {
|
||||||
|
match &input.data {
|
||||||
|
Data::Struct(data) => match &data.fields {
|
||||||
|
Fields::Named(fields) => Ok(fields),
|
||||||
|
|
||||||
|
Fields::Unnamed(_) | Fields::Unit => Err(syn::Error::new_spanned(
|
||||||
|
input,
|
||||||
|
"this derive only supports structs with named fields",
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
|
||||||
|
Data::Enum(_) | Data::Union(_) => Err(syn::Error::new_spanned(
|
||||||
|
input,
|
||||||
|
"this derive only supports structs",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_package_name(input: &DeriveInput) -> syn::Result<LitStr> {
|
||||||
|
let mut package = None;
|
||||||
|
|
||||||
|
for attribute in &input.attrs {
|
||||||
|
if !attribute.path().is_ident("uci") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute.parse_nested_meta(|meta| {
|
||||||
|
if meta.path.is_ident("package") {
|
||||||
|
if package.is_some() {
|
||||||
|
return Err(meta.error("duplicate `package` attribute"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: LitStr = meta.value()?.parse()?;
|
||||||
|
|
||||||
|
if value.value().is_empty() {
|
||||||
|
return Err(syn::Error::new(value.span(), "`package` cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
package = Some(value);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(meta.error("expected `package = \"...\"`"))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
package.ok_or_else(|| {
|
||||||
|
syn::Error::new_spanned(
|
||||||
|
&input.ident,
|
||||||
|
"UciConfig requires `#[uci(package = \"...\")]`",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_section_name(field: &Field) -> syn::Result<LitStr> {
|
||||||
|
let field_ident = field
|
||||||
|
.ident
|
||||||
|
.as_ref()
|
||||||
|
.expect("named fields always have identifiers");
|
||||||
|
|
||||||
|
let default_name = field_ident.unraw().to_string();
|
||||||
|
let default_name = LitStr::new(&default_name, field_ident.span());
|
||||||
|
|
||||||
|
let mut section = None;
|
||||||
|
|
||||||
|
for attribute in &field.attrs {
|
||||||
|
if !attribute.path().is_ident("uci") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute.parse_nested_meta(|meta| {
|
||||||
|
if meta.path.is_ident("section") {
|
||||||
|
if section.is_some() {
|
||||||
|
return Err(meta.error("duplicate `section` attribute"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if meta.input.peek(Token![=]) {
|
||||||
|
let value: LitStr = meta.value()?.parse()?;
|
||||||
|
|
||||||
|
if value.value().is_empty() {
|
||||||
|
return Err(syn::Error::new(value.span(), "`section` cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
section = Some(value);
|
||||||
|
} else {
|
||||||
|
section = Some(default_name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(meta.error("expected `section` or `section = \"...\"`"))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.ok_or_else(|| {
|
||||||
|
syn::Error::new_spanned(
|
||||||
|
field,
|
||||||
|
"UciConfig fields require `#[uci(section)]` \
|
||||||
|
or `#[uci(section = \"...\")]`",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::settings::uci_schema::{UciConfigSchema, UciSectionMapping};
|
||||||
use config::{ConfigError as SourceError, Map, Source, Value, ValueKind};
|
use config::{ConfigError as SourceError, Map, Source, Value, ValueKind};
|
||||||
|
|
||||||
use rust_uci::config::{
|
use rust_uci::config::{
|
||||||
@@ -16,6 +17,7 @@ enum PackageSelection {
|
|||||||
pub(crate) struct UciSource {
|
pub(crate) struct UciSource {
|
||||||
packages: PackageSelection,
|
packages: PackageSelection,
|
||||||
required: bool,
|
required: bool,
|
||||||
|
schema: &'static [UciSectionMapping],
|
||||||
config_dir: Option<PathBuf>,
|
config_dir: Option<PathBuf>,
|
||||||
save_dir: Option<PathBuf>,
|
save_dir: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
@@ -25,6 +27,7 @@ impl UciSource {
|
|||||||
Self {
|
Self {
|
||||||
packages,
|
packages,
|
||||||
required: true,
|
required: true,
|
||||||
|
schema: &[],
|
||||||
config_dir: None,
|
config_dir: None,
|
||||||
save_dir: None,
|
save_dir: None,
|
||||||
}
|
}
|
||||||
@@ -50,6 +53,18 @@ impl UciSource {
|
|||||||
self
|
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)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn with_directories(
|
pub(crate) fn with_directories(
|
||||||
mut self,
|
mut self,
|
||||||
@@ -84,6 +99,10 @@ impl UciSource {
|
|||||||
Ok(uci.into())
|
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 {
|
fn config_selector(selector: &SectionSelector<'_>) -> String {
|
||||||
match selector {
|
match selector {
|
||||||
SectionSelector::Named(name) => (*name).to_owned(),
|
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(
|
fn collect_package(
|
||||||
&self,
|
&self,
|
||||||
package_name: &str,
|
package_name: &str,
|
||||||
@@ -127,14 +172,19 @@ impl UciSource {
|
|||||||
for option in options {
|
for option in options {
|
||||||
let option_name = option.name();
|
let option_name = option.name();
|
||||||
|
|
||||||
|
let uci_relative_key = format!("{selector}.{option_name}");
|
||||||
|
|
||||||
let config_relative_key =
|
let config_relative_key =
|
||||||
format!("{}.{option_name}", Self::config_selector(&selector),);
|
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 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| {
|
let Some(value) = option.get().map_err(|e| {
|
||||||
SourceError::Message(format!(
|
SourceError::Message(format!(
|
||||||
"failed to retrieve UCI option \
|
"failed to retrieve UCI option \
|
||||||
|
|||||||
+14
-6
@@ -9,6 +9,9 @@ use serde::{Deserialize, Deserializer, de};
|
|||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use uci_derive::{UciConfig, UciSection};
|
||||||
|
|
||||||
|
pub(crate) mod uci_schema;
|
||||||
|
|
||||||
#[cfg(all(feature = "uci", unix))]
|
#[cfg(all(feature = "uci", unix))]
|
||||||
use crate::platform::unix::uci::UciSource;
|
use crate::platform::unix::uci::UciSource;
|
||||||
@@ -70,17 +73,22 @@ struct ReflectorRecord {
|
|||||||
description: String,
|
description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize, UciConfig)]
|
||||||
|
#[uci(package = "sqm-autorate-rust")]
|
||||||
pub(crate) struct Settings {
|
pub(crate) struct Settings {
|
||||||
|
#[uci(section)]
|
||||||
pub(crate) network: NetworkSettings,
|
pub(crate) network: NetworkSettings,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[uci(section)]
|
||||||
pub(crate) output: OutputSettings,
|
pub(crate) output: OutputSettings,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[uci(section)]
|
||||||
pub(crate) observability: ObservabilitySettings,
|
pub(crate) observability: ObservabilitySettings,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[uci(section)]
|
||||||
pub(crate) advanced_settings: AdvancedSettings,
|
pub(crate) advanced_settings: AdvancedSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +97,7 @@ impl Settings {
|
|||||||
let builder = Config::builder();
|
let builder = Config::builder();
|
||||||
|
|
||||||
#[cfg(all(feature = "uci", unix))]
|
#[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")]
|
#[cfg(feature = "toml")]
|
||||||
let builder = builder.add_source(
|
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) struct NetworkSettings {
|
||||||
pub(crate) download_interface: String,
|
pub(crate) download_interface: String,
|
||||||
pub(crate) upload_interface: String,
|
pub(crate) upload_interface: String,
|
||||||
@@ -160,7 +168,7 @@ impl NetworkSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize, UciSection)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) struct OutputSettings {
|
pub(crate) struct OutputSettings {
|
||||||
#[serde(deserialize_with = "deserialize_from_str")]
|
#[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)]
|
#[serde(default)]
|
||||||
pub(crate) struct ObservabilitySettings {
|
pub(crate) struct ObservabilitySettings {
|
||||||
pub(crate) enabled: bool,
|
pub(crate) enabled: bool,
|
||||||
@@ -216,7 +224,7 @@ impl Default for ObservabilitySettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize, UciSection)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) struct AdvancedSettings {
|
pub(crate) struct AdvancedSettings {
|
||||||
pub(crate) download_delay_ms: f64,
|
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