RustArena

Step 7 of 7

Putting it together: a config reader

Real programs combine everything from this module. Here's a reader for a small config file: blank lines and # comments are skipped, every other line must be key = number. Bad lines are reported with their line number, and the program keeps going.

use std::io::BufRead;

fn parse_line(line: &str) -> Result<(String, u32), String> {
    let (key, value) = line
        .split_once('=')
        .ok_or_else(|| String::from("expected key = value"))?;
    let key = key.trim();
    if key.is_empty() {
        return Err(String::from("missing key"));
    }
    let value = value
        .trim()
        .parse::<u32>()
        .map_err(|e| format!("bad value for {key}: {e}"))?;
    Ok((key.to_string(), value))
}

fn main() {
    let mut number = 0;
    let mut settings = 0;
    let mut errors = 0;
    for line in std::io::stdin().lock().lines() {
        let line = line.expect("could not read input");
        number += 1;
        let text = line.trim();
        if text.is_empty() || text.starts_with('#') {
            continue;
        }
        match parse_line(text) {
            Ok((key, value)) => {
                settings += 1;
                println!("{key} = {value}");
            }
            Err(e) => {
                errors += 1;
                println!("line {number}: {e}");
            }
        }
    }
    println!("{settings} settings, {errors} errors");
}
# server settings
port = 8080
workers = four

timeout 30
 = 5
retries = 3
port = 8080
line 3: bad value for workers: invalid digit found in string
line 5: expected key = value
line 6: missing key
retries = 3
2 settings, 3 errors

The design decisions are the ones you'll make at work:

Common mistakes

Your task

A warehouse imports inventory rows like widget,4,2.50 (name, quantity, price). Write fn parse_item(line: &str) -> Result<Item, String> that splits the trimmed line on commas and checks, in this order:

  1. not exactly 3 fields: Err("expected 3 fields, got <n>")
  2. empty name (after trimming): Err("missing name")
  3. quantity not a valid u32: Err("bad quantity: <error>")
  4. price not a valid f64: Err("bad price: <error>")

Trim each field. line.split(',').collect() gathers the pieces into a Vec<&str> (the starter does this for you).

Challenge 2

Now the whole import program. The starter has a working parse_item. Read every line from stdin, counting line numbers from 1 (blank lines count too, but skip them). For each non-blank line:

At the end print total value: <total with 2 decimals>. For the input below the output is:

widget: 4 x 2.50
line 2: bad quantity: invalid digit found in string
gear: 2 x 10.00
total value: 30.00

(input: widget,4,2.50, then bolt,many,0.10, then a blank line, then gear,2,10)

Challenge 3

A sign-up form sends a username, an age and an email as text. Write fn validate_signup(username: &str, age: &str, email: &str) -> Result<Signup, String> (the struct is in the starter). Trim all three inputs, then check in this order, using ? where it helps:

  1. username shorter than 3 characters: Err("username too short")
  2. age not a valid u8: Err("invalid age: <error>")
  3. age under 13: Err("must be at least 13")
  4. email without an @: Err("invalid email")

On success return the Signup with the trimmed username and email.

Previous: Combinators: map, and_then, ok_or, map_err