rust

Idiomatic way to compute a default for an Option


I have a function that reads a configuration file. It receives as a parameter a Option<String> that contains the path to the configuration file if the user optionally sets it as a command line argument. If not defined the function computes a default path using the user home directory (home crate) concatenated with a default file name. This code works but the match looks a bit like there must be some more idiomatic way in Rust to resolve this. I can not use unwrap_or() because it doesn't allow multiple statements needed to compute the default path.

fn get_config(config_file: Option<String>) -> Result<Config, Box<dyn std::error::Error>> {
   
    let credentials_path = match config_file {
        None => {
                let mut default_path = (home::home_dir()).ok_or_else(||{"Can not find home directory"})?;
                default_path.push("credentials.toml");
                default_path
            },
        Some(s) => s.into(),
    };
    println!("path: {:?}", credentials_path);
    let contents = std::fs::read_to_string(credentials_path)?;

    let config: Config = toml::from_str(contents.as_str()).unwrap();
    Ok(config)
}

Is there some other feature that could be used to implement this code in a more rusty way?


Solution

  • I can not use unwrap_or() because it doesn't allow multiple statements needed to compute the default path.

    You can use blocks in any expression in Rust (and similarly, the body of a closure is any expression, not necessarily a block):

    let credentials_path = config_file.map(|p| p.into()).unwrap_or({
        let mut default_path = home::home_dir().ok_or_else(|| "Cannot find home directory")?;
        default_path.push("credentials.toml");
        default_path
    });
    

    But it’s preferable not to evaluate this block if its value isn’t required (i.e. to use unwrap_or_else), and the combination of that, the .map into, and the ? operator that returns from the outer function make match a relatively clean option here compared to this kind of pattern:

    let credentials_path = config_file.map(|p| Ok(p.into())).unwrap_or_else(|| {
        let mut default_path = home::home_dir().ok_or("Cannot find home directory")?;
        default_path.push("credentials.toml");
        Ok(default_path)
    })?;
    

    (which might even need more type annotations.)

    So in the end, I’d go with:

    let credentials_path = match config_file {
        None => {
            let mut default_path = home::home_dir().ok_or("Cannot find home directory")?;
            default_path.push("credentials.toml");
            default_path
        }
        Some(s) => s.into(),
    };
    println!("path: {:?}", credentials_path);
    let contents = std::fs::read_to_string(credentials_path)?;
    
    toml::from_str(&contents).into()