Skip to content

← Errors Are Values step 9 of 24

Medium Primitives

Converting between Option and Result

Read an optional setting from key/value pairs. Use transpose to preserve the difference between a missing setting and an invalid value.

pub fn read_optional_port(
    pairs: Vec<(String, String)>,
    key: String,
) -> Result<Option<u16>, String>

Find the first pair whose key exactly matches key, then trim the value and parse it as a u16. Matching is case-sensitive. Do not trim the key or continue to another matching pair if the first value is invalid.

Situation Result
No matching key Ok(None)
Matching value parses as u16 Ok(Some(number))
Matching value fails to parse Err(message)

Format an error exactly as follows, substituting the requested key and the original, untrimmed value:

key `port` is not a valid port: eighty

For key port, " 443 " succeeds with Ok(Some(443)), while " eighty " produces an error that retains both spaces around eighty.

This exercise uses Rust's decimal u16 parsing rules. The accepted range is 0 through 65535, and a leading + is allowed. Empty or whitespace-only values, negative numbers, and overflow are errors. Port zero is accepted here because it is a valid u16; this function does not apply additional network configuration policy.

Two independent questions

Option records whether a setting was found. Result records whether parsing succeeded. Returning Ok(None) leaves the caller free to choose a default, while an error preserves the distinction between missing and malformed input.

Mapping a parser over the lookup produces:

Option<Result<u16, String>>

The desired return type puts the possible error on the outside:

Result<Option<u16>, String>

transpose() converts between these arrangements without discarding the number, error, or absence.

The three transpose cases

Before: Option<Result<T, E>> After: Result<Option<T>, E>
Some(Ok(value)) Ok(Some(value))
Some(Err(error)) Err(error)
None Ok(None)

The reverse method, Result::transpose, maps each right-hand case back to its left-hand case. This complete example checks both directions:

fn main() {
    let cases: [Option<Result<u16, &str>>; 3] =
        [Some(Ok(8080)), Some(Err("invalid")), None];
    let expected = [Ok(Some(8080)), Err("invalid"), Ok(None)];

    for (before, after) in cases.into_iter().zip(expected) {
        assert_eq!(before.transpose(), after);
        assert_eq!(after.transpose(), before);
    }
}

Repair the starter

Its successful parse arm returns Some(number), but the function promises Result<Option<u16>, String>. That mismatch causes E0308. The error arm already has the required error shape.

A nested match could express the same behavior. For this exercise, practice a chain: find the pair, map parsing over it, convert the parse error with map_err, then transpose. The closure can borrow the trimmed value for parsing while retaining the original string for the error message.

Choose conversions that preserve the information you need

ok_or(error) turns None into an error; ok_or_else computes the error only when needed. They suit a required setting, but would give the wrong missing-key behavior here.

Result::ok() discards an error and returns None. Using it on this parse result would make malformed input indistinguishable from absence. Result::err() does the reverse, retaining only an error.

A passing submission must compile, pass the tests, and pass clippy -D warnings. Those checks do not by themselves require a combinator chain; using transpose is the learning objective.

References: Option::transpose and u16 parsing.