Search code examples
rusttoml

idiomatic way to combine 'key exists' with 'and if its the right type' parsing toml


I am parsing this

[xxxxx]
drive0={}
drive1={path="xxxx"}
...

sometimes there is a path, sometimes not.

I have working code but I am still trying to learn the rust idiomatic way of doing things. Code:

for i in 0..8 {
    let drive_name = format!("drive{}", i);
    if dmap.contains_key(&drive_name) {
        if let Some(d) = config[drive_name].as_table() {
            this.units.push(Rkunit::new(true));
            if d.contains_key("path") {
                if let Some(path) = d["path"].as_str() {
                    let file = OpenOptions::new()
                        .read(true)
                        .write(true)
                        .create(true)
                        .open(path)
                        .unwrap();
                    this.units[i].file.replace(file);
                }
            }
        } else {
            this.units.push(Rkunit::new(false));
        }
    }
}

    

I expected that

if let Some(path) = d["path"].as_str()

(ie without if d.contains() line)

would deal with both cases - ie no "path" and "path" isnt string, but it does not. Same with the contains_key(drive_name) too.

I tried various guessed at syntaxes to see if I could avoid another nested if and could find one.

So is there a better way or is this as good as it gets. Any other comments on parsing toml welcome.


Solution

  • There are a few approaches here that might be valid. Since your code is quite complex and uses non-std API's, it's hard to see if my change would be useful:

    1. Use your general code structure, but combined .contains and applying a function the the contained value into the pattern .get(...).map(...). x.get(y) returns an Option value, which allows you access to the whole Option API, unlike x[y] which would panic if the key doesn't exist.

      if let Some(d) = config.get(&drive_name).map(|c| c.as_table()) {
          this.units.push(Rkunit::new(true);
          if let Some(path) = d.get("path").and_then(String::as_str) {
          }
      } else {
          this.units.push(Rkunit::new(false));
      }
      
    2. You can use a match statement with some pre-work. I personally prefer this, since it makes the match arms very explicit, but i think it's less idiomatic:

      let drive = config.get(&driver_name); // return an option
      let path = drive.map(|d|.get("path")); // returns an option
      match (drive, path) {
          (Some(d), Some(p)) => {
              this.units.push(Rkunit::new(true));
              let file = OpenOptions::new()
                  .read(true)
                  .write(true)
                  .create(true)
                  .open(path)
                  .unwrap();
              this.units[i].file.replace(p);
          }
          (Some(d), None) => {
              this.units.push(Rkunit::new(true);
          }
          _ => {
              this.units.push(Rkunit::new(false);
          }
      }
      

    I think 1. is more idiomatic, but I've certainly seen both and it's probably more a matter of style. Composing Option certainly is idiomatic over contains & access.