Repository navigation
Proposal: Higher-level API allowing easier config traversal #25
Copy link
Copy link
Open
Description
When using this library i've found it very cumbersome to construct the config keys using string formatting to pass them to methods like get. I'd imagine it should be possible to have an abstraction that allows you to traverse into config options in a path-like manner.
Roughly I'd imagine an API like this:
API Outline
use crate::{error::Result, UciPtr};
/// represents the root of the config tree
/// It's the parent structure to [Package]s
pub struct Config {
_ptr: UciPtr,
}
impl Config {
/// return a single [Package] by its name
/// also works if the package is not defined yet
pub fn package(&mut self, _name: impl AsRef<str>) -> Result<Package> {
todo!()
}
/// list all [Package]s in the config
pub fn packages(&mut self) -> Result<impl Iterator<Item = Package>> {
Ok(vec![].into_iter())
}
}
/// represents a single package in the config tree
/// parent to different [Section]s
pub struct Package {
_ptr: UciPtr,
}
impl Package {
/// name of this package
pub fn name(&mut self) -> Result<String> {
todo!()
}
/// return a single [Section] by its name
/// also works if the section is not defined yet
pub fn section(&mut self, _name: impl AsRef<str>) -> Result<Section> {
todo!()
}
/// list all [Section]s in this package
pub fn sections(&mut self) -> Result<impl Iterator<Item = Section>> {
Ok(vec![].into_iter())
}
}
/// represents a single section
/// parent to different [Option]s
pub struct Section {
_ptr: UciPtr,
}
impl Section {
/// returns the name of named sections, otherwise None
pub fn name(&mut self) -> Result<std::option::Option<String>> {
todo!()
}
/// returns the type of the section
pub fn r#type(&mut self) -> Result<String> {
todo!()
}
/// returns a specific [Option] by name
/// also works if the option is not defined yet
pub fn option(&mut self, _name: impl AsRef<str>) -> Result<Option> {
todo!()
}
/// lists all options in this section
pub fn options(&mut self) -> Result<impl Iterator<Item = Option>> {
Ok(vec![].into_iter())
}
}
/// represents the value of an [Option]
pub enum Value {
String(String),
Boolean(bool),
Integer(i64),
List(Vec<Value>),
}
/// represents an option within a [Section]
pub struct Option {
_ptr: UciPtr,
}
impl Option {
/// name of the option
pub fn name(&mut self) -> Result<String> {
todo!()
}
/// returns the current value of the option, None if not set
pub fn get(&mut self) -> Result<std::option::Option<Value>> {
todo!()
}
/// sets the value of the option, overriding the previous value
/// will create the [Package] or [Section] along the way if they do
/// not exist
pub fn set(&mut self, _value: Value) -> Result<()> {
todo!()
}
/// adds a value to the existing value
/// behaves like `uci add_list` which will:
/// - create the option if it doesn't exist (not as a list)
/// - turn a single-value option into a list
///
/// returns the resulting value
pub fn add_list(&mut self, _value: Value) -> Result<Value> {
todo!()
}
}Does this sound reasonable? Any feedback on the API design?
I'd be happy to also implement this, just want to see if there is interest and feedback
cc @MarkusPettersson98 since you seem to have involved yourself with this lib recently too
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
mraerino commentedon May 3, 2026
I've dome some experiments in #26
Take-aways:
CStrings and giving them to uci only when actually doing an action, e.g. get/set etcArc<Mutex<Uci>>in every struct for now, which becomes a little verbose, but the API is a lot nicer without lifetimes. If we wanted to have lifetimes to also enforce some safety, i'd still pass&Mutex<Uci>around, just because making everything require a mutable ref makes for a bad APIResult, which seems excessive and is only needed sinceCString::newis fallible - one could argue we can storeStringin the intermediate structs and only convert them toCStringwhen they are actually needed by Uci. This would make for a better API, but introduces an allocation overhead when intermediate structs are long-lived. That overhead might be neglible though.I've tried the API out in practice and it seems to hold up somewhat well: https://github.com/ffddorf/node-tooling/blob/1125c139ce6276e045086321d0855be8f0b6d8bc/dorfconf/src/main.rs#L61
pulsastrix commentedon May 5, 2026
In general, I think this is a good idea. I've also been playing around with improving the API (and adding support for non-string value types) a few years ago, but unfortunately didn't have the time to finish working on it, so I'll gladly accept any contributions that do so 😄
My two cents regarding your takeaways and the API design:
I think that your current approach with using
CStrings seems to be the most sensible, given thatUciPtrinstances are very susceptible to being invalidated by operations that modify the configuration. Any other idea I have (e.g., making the lookup methods ofConfig,PackageandSectiontake ownership of theUciinstance or making the API read-only) would be a usability nightmare compared to it.For the user of the library, using lifetimes and making the structs contain a reference to
Ucimight make it more obvious that theuci_contextstill exists as long as one of these structs exists.However, I do see the risk of making the API more difficult to use by adding lifetimes, and I'm fine with keeping it as is if you prefer to implement it that way.
To avoid passing around a mutex, you could make
Config,PackageandSectiongeneric overBorrow<Uci>and then have twoimplBlocks for each struct:impl <T: Borrow<Uci>> Section<T>for read-only methods andimpl <T: BorrowMut<Uci> Section<T>for anything that requires a mutableUciinstance.This way, the user could choose in advance (when creating the
Configinstance) whether to have a mutable or an immutable instance of the config.However, I'd also be fine with keeping it as a
Mutexif that idea is too much work to implement or if there is any usability issue with that approach that I overlooked.