Skip to content

Proposal: Higher-level API allowing easier config traversal #25

Open

Description

@mraerino

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

Activity

  1. mraerino commented on May 3, 2026

    @mraerino
    ContributorAuthor

    I've dome some experiments in #26

    Take-aways:

    • It's hard to provide this API when dealing with pointers gotten from the uci lib, since they can be freed in moments when you don't expect it - hence i've kept to only keeping around CStrings and giving them to uci only when actually doing an action, e.g. get/set etc
    • To avoid lifetime hassle i've gone with an Arc<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 API
    • Almost all methods return Result, which seems excessive and is only needed since CString::new is fallible - one could argue we can store String in the intermediate structs and only convert them to CString when 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

  2. pulsastrix commented on May 5, 2026

    @pulsastrix
    Member

    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 that UciPtr instances are very susceptible to being invalidated by operations that modify the configuration. Any other idea I have (e.g., making the lookup methods of Config, Package and Section take ownership of the Uci instance 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 Uci might make it more obvious that the uci_context still 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, Package and Section generic over Borrow<Uci> and then have two impl Blocks for each struct: impl <T: Borrow<Uci>> Section<T> for read-only methods and impl <T: BorrowMut<Uci> Section<T> for anything that requires a mutable Uci instance.

      This way, the user could choose in advance (when creating the Config instance) whether to have a mutable or an immutable instance of the config.
      However, I'd also be fine with keeping it as a Mutex if that idea is too much work to implement or if there is any usability issue with that approach that I overlooked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions