Skip to content

← The Expert Edge: Idiom, Review and Capstones step 8 of 14

Hard End-to-End

Capstone: compose parser errors and their causes

Repair three parts of the supplied parser: the BadInt display message, the unwrap() in entry, and the missing error documentation on parse_config. Keep the parsing rules, error variants and source-chain renderer unchanged.

pub fn parse_config(lines: Vec<String>, max_port: i64) -> Result<Vec<(String, i64)>, String>

The internal parse_entries function returns a structured ConfigError. The public wrapper converts it to text so the test wrapper can report it as JSON. Structured errors let code inspect categories and fields without parsing messages. A string can be suitable at a reporting boundary, but it no longer provides those typed fields. parse_entries is private in this exercise; exposing a structured API to another crate would require an intentional visibility change.

Parsing rules

Process lines in order. Skip a line if trimming Unicode whitespace makes it empty or makes its first character #. Comments must occupy the remainder of a skipped line; there is no inline-comment syntax.

For every remaining line:

  1. Split at the first =. Trim Unicode whitespace around the key and value.
  2. If there is no =, the whole trimmed line is the key and the value is empty. Further = characters stay in the value and make integer parsing fail.
  3. Parse the value as an i64, then require it to lie in 0..=max_port. This range applies to every key, not just port.

Decimal values may have a leading + or - and leading zeros. Empty values, internal whitespace, underscores, decimal points and out-of-range integers fail integer parsing. A successfully parsed negative value fails the range check, except that -0 is zero. If max_port is negative, no parsed integer passes the range check.

Keys are case-sensitive and otherwise unrestricted after trimming. Empty keys are allowed. Preserve all successful pairs in input order, including duplicate keys; do not collect them into a map.

Stop at the first invalid value. Only after all entries parse successfully, check that at least one key is exactly "port". If it is missing, return MissingKey. Empty input and comment-only input therefore fail with that error. No partial pairs are returned on failure.

Error text

render_chain joins the outer error and each source with ": ". The three outer messages are:

Error variant Outer message
MissingKey missing required key `port`
BadInt key `KEY` is not an integer
OutOfRange key `KEY` value VALUE is outside 0..=MAX

Keys and numeric values are inserted directly into these templates. Only BadInt has a source, the original ParseIntError.

For "port = eighty", the complete error is:

key `port` is not an integer: invalid digit found in string

For "port", the cause is cannot parse integer from empty string. Integer overflow has its own parser message. Preserve the actual ParseIntError instead of replacing every cause with a generic invalid-digit message.

Repair the display and lint failures

The starter's BadInt display includes its source text. render_chain then appends that source again. Make the display describe only the outer layer, and keep Error::source() returning the parser error. This avoids duplicate text with the supplied renderer; not every reporting tool automatically walks sources.

entry also uses parts.next().unwrap(). In this specific use of splitn, the first segment exists, even for an empty string. The call is nevertheless forbidden by the file's explicit clippy::unwrap_used policy. Use a fallback that preserves the parser's behavior, such as unwrap_or(line).

Returning Result does not by itself promise that a function can never panic, and these lints are not a proof of panic freedom. Here the goal is to handle malformed configuration through the documented error path and satisfy the supplied lint policy.

Add a # Errors section to parse_config documenting invalid integers, range failures, the missing required key and conversion of the error chain to a string. The file denies clippy::missing_errors_doc, so this documentation is part of the exercise requirements.

How the supplied pieces fit

value_of uses From<ParseIntError> for ValueError to convert a parser error through ?. It does not yet know the key. entry attaches that context with with_key, producing ConfigError. The collect::<Result<_, _>>() in parse_entries returns the first error instead of keeping a partial success.

Bounded::try_from((value, max)) checks the range before constructing a wrapper. That supports a local invariant when callers use this constructor. It does not store max, and code in the defining module can construct Bounded directly. The current parser immediately unwraps the checked value back to i64; the wrapper does not permanently encode a particular bound in its type.

Keep render_chain, value_of, Bounded and parse_entries unchanged. Your solution must compile, produce the exact outputs and pass the supplied Clippy checks with warnings treated as errors.