samedi 12 février 2022

Using Traits to configure an application

I've been trying to figure out a good idiomatic configuration pattern in Rust. I read a few articles and the impression I'm getting is that Traits are a way to go for my use-case. However the examples were either too simplistic or too complex. But I went ahead an refactored my application but encountered some stuff I've never seen/understood. I marked the two locations in the code.

(a) - It took a minute to get this running as I kept dealing with compiler errors but finally tried &* assuming I was de-referencing the Box and then passing a reference to it. I'm not entirely sure this is correct and would like some insight into why it works and if it'll bite me later.

(b) - None of the articles/examples I read explained how to pass the configuration to other internal methods. Again I just kept adding symbols until the compiler was happy so I'm not too confident this is the right way.

On the pattern:

My example code's structure is very similar the actual application. I take the user's input then pass it to the App which delegates the creation of the data to Words::add() who delegates the creation of a Word to Word::new() and so on. In the actual application there's an additional layer or so where metadata is generated but in the end a each of these need access to the configuration. I started with a global static config which led to testing headaches, then moved to a concrete Config type which resulted in a massive struct dealing with all the different contexts.

My goal is to have an immutable configuration that can change based on context e.g. dev, testing, production etc. I'm assuming this is a big topic that might not have a simple answer but curious if there any insights into this. I was unable to find one that fits my use-case so I'm not 100% confident I'm going in the right direction.

Link to the playground.

// Trait

pub trait Configuration: std::fmt::Debug {
    fn prefix(&self) -> String;
    fn suffix(&self) -> String;
}

// App

#[derive(Debug)]
pub struct App {
    config: Box<dyn Configuration>,
    data: Words,
}

impl App {
    pub fn new(config: Box<dyn Configuration>) -> Self {
        Self {
            config,
            data: Words::default(),
        }
    }

    pub fn add(&mut self, word: &str) {
        self.data.insert(&*self.config, word);
        // (a) This -----^^
    }
}

#[derive(Debug, Default)]
struct AppConfig;

impl Configuration for AppConfig {
    fn prefix(&self) -> String {
        "re".to_string()
    }
    fn suffix(&self) -> String {
        "able".to_string()
    }
}

// Words

#[derive(Debug, Default)]
struct Words(Vec<Word>);

impl Words {
    fn insert(&mut self, config: &dyn Configuration, word: &str) {
        // (b) This -------------^^^^

        let word = Word::new(config, word);

        println!("Adding {:?}", word);

        self.0.push(word);
    }
}

#[derive(Debug, PartialEq)]
struct Word(String);

impl Word {
    pub fn new(config: &dyn Configuration, word: &str) -> Self {
        Self(format!("{}{}{}", config.prefix(), word, config.suffix()))
    }
}

fn main() {
    let config = AppConfig::default();
    let mut app = App::new(Box::new(config));
    app.add("use");
    app.add("mark");
    app.add("seal");
}

#[cfg(test)]
mod tests {

    use super::*;

    #[derive(Debug, Default)]
    struct TestConfig;

    impl Configuration for TestConfig {
        fn prefix(&self) -> String {
            "test".to_string()
        }
        fn suffix(&self) -> String {
            "able".to_string()
        }
    }

    #[test]
    fn test() {
        let config = TestConfig::default();
        let mut app = App::new(Box::new(config));
        app.add("");

        assert_eq!(app.data.0[0], Word("testable".to_string()));
    }
}

Aucun commentaire:

Enregistrer un commentaire