Making two struct fields optional

In the following Keymap struct, the prompt field is optional:

pub struct Keymap {
    pub key: char,
    pub description: String,
    pub command: String,
    pub prompt: Option<String>,
}

And I've created two methods. One that creates a Keymap without prompt, and another that creates a Keymap with a prompt:

impl Keymap {
    pub fn new(key: char, description: &str, command: &str) -> Self {
        Self {
            key,
            description: description.to_string(),
            command: command.to_string(),
            prompt: None,
        }
    }

    pub fn with_prompt(key: char, description: &str, command: &str, prompt: &str) -> Self {
        Self {
            key,
            description: description.to_string(),
            command: command.to_string(),
            prompt: Some(prompt.to_string()),
        }
    }
}

Now, I want to make the description field optional too (it not present, it will default to the same value as command). To do that, I would have to create a with_description method and a with_prompt_and_description method.

I feel like I have to use another design pattern here?

Have a look at the Builder pattern. Can't explain it rn but just google "builder pattern rust"

3 Likes

Yes, I'd use the builder pattern for this as well. Something like this, probably:

#[derive(Debug, Default)]
pub struct Keymap {
    pub key: char,
    pub command: String,
    pub description: Option<String>,
    pub prompt: Option<String>,
}

impl Keymap {
    pub fn new<S: AsRef<str>>(key: char, command: S) -> Self {
        Self {
            key,
            command: command.as_ref().to_owned(),
            ..Default::default()
        }
    }

    pub fn with_prompt<S: AsRef<str>>(mut self, prompt: S) -> Self {
        self.prompt = Some(prompt.as_ref().to_owned());
        self
    }
    
    pub fn with_description<S: AsRef<str>>(mut self, description: S) -> Self {
        self.description = Some(description.as_ref().to_owned());
        self
    }
}
2 Likes

Thanks for the example!

One question: why did you use AsRef as opposed to just this?

pub struct KeymapBuilder {
    key: char,
    description: Option<String>,
    command: String,
    prompt: Option<String>,
}

impl KeymapBuilder {
    pub fn new(key: char, command: &str) -> Self {
        Self {
            key,
            description: None,
            command: command.to_string(),
            prompt: None,
        }
    }

    pub fn with_description(mut self, description: &str) -> Self {
        self.description = Some(description.to_string());
        self
    }

    pub fn with_prompt(mut self, prompt: &str) -> Self {
        self.prompt = Some(prompt.to_string());
        self
    }

    pub fn build(self) -> Keymap {
        Keymap {
            key: self.key,
            description: self.description.unwrap_or_else(|| self.command.clone()),
            command: self.command,
            prompt: self.prompt,
        }
    }
}

It's a convenience that lets you also pass in a String without needing to put a & in front of it.

Often if the field is an owned String (i.e. prompt: Option<String>), I'll make the with_prompt() method accept a prompt: impl Into<String>. That way if the caller already has an owned String they can pass that in without making any copies, and for things like testing where you use hard-coded string literals, it'll let you pass in that &str without cluttering your code with loads of to_string() calls.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.