Introduction
A field team needs two working flashlights. Two scan lines might describe two different lights, one light scanned twice, or one working light and one damaged light. Your program must preserve those differences before it can answer whether the kit is complete.
In this lab, you will implement a condition parser and an asset collector in Rust. You will retain distinct physical assets, ignore identical repeat scans, and reject contradictory records without changing the source files. The next lab uses these identities to explain each equipment type's shortage or surplus. These are synthetic records, not inspections of physical equipment.
You should already understand Rust structs, enums, HashMap, Result, reading text files, and basic for loops. The prepared parser, command interface, and output formatter let you focus on identity decisions. The author estimates 45 minutes including reading and ordinary corrections; this has not been timed with beginning learners.
Read Conditions Without Guessing
In this step, inspect two real scan files, predict their usable items, and implement the short condition decision that makes the program's normal row view work. Open Terminal. The prepared project is /home/labex/project/field-kit, with Rust 1.99.1.
The following export adds Cargo's directory to this shell's command search path. $PATH keeps the existing directories. This lasts only for the current Terminal session; repeat it in a new shell. It does not edit shell startup files.
export PATH=/home/labex/.cargo/bin:$PATH
Use cd to select the project; repeat it if you lose your directory. pwd prints the current directory so you can confirm where later file edits will occur.
cd /home/labex/project/field-kit
pwd
Expect /home/labex/project/field-kit. Correct a path typo with cd, not a VM reset. cat prints a file without changing it. Read these two actual inputs:
cat data/two-good.tsv
cat data/one-damaged.tsv
TSV means tab-separated values. Each scan has three fields: asset_id, type_id, condition, separated by literal tabs. In the first file, physical assets a and b both have condition good. In the second, a is good and b is damaged. Hand-check the result: there are two physical items in both files, but only the first describes two usable lights. The words good and damaged are the two exact allowed conditions; damaged equipment is valid data, not a bad input row.
The project supplies src/lib.rs with structs and TSV readers, src/main.rs with the command interface and formatter, and src/readiness.rs for the later lab. Keep those files, Cargo.toml, rust-toolchain.toml and every original data/ file unchanged. Here you may edit only src/condition.rs and, in the next step, src/identity.rs, retaining their public names and types.
Read the condition module:
cat src/condition.rs
The enum and display method are already supplied. impl groups methods on Condition; Self names that enum inside the block. pub exposes these names to the reader. derive supplies copying, debug display and equality. parse returns Result<Self, String>: either Ok with a condition or Err with an owned error explanation. The current stub rejects every word, so it does not yet distinguish the two normal files.
Make a source-only undo point before editing. mktemp -d creates a new directory by replacing the six X characters with a unique suffix; it does not reuse an older backup. $() stores the printed path in a session variable. && runs cp -p only after directory creation succeeds; the copy preserves the source metadata. Quotes keep the variable's path together. If either operation fails, read the error and obtain a successful backup before editing:
rs03_before_condition=$(mktemp -d .edit-before-condition.XXXXXX) && cp -p src/condition.rs "$rs03_before_condition/condition.rs"
Open the existing module with nano; -c displays the cursor position:
nano -c src/condition.rs
Nano's ^ notation means Ctrl. Press Ctrl+W, type pub fn parse, and press Enter to locate the function. Read its opening and closing braces. Use arrows and Backspace/Delete to edit only this method's body and its unused parameter binding. Rename _value to value, keeping the parameter type. Preserve the enum, derive and name method.
Write a match value expression with an exact branch for each allowed word and a wildcard for every other string. A branch such as "good" => Ok(Self::Good), shows the syntax. Construct the corresponding Damaged result yourself. _ matches any remaining value, which must return Err rather than silently invent a condition. format! creates a String; {value:?} includes quotes around the unexpected text so spaces and spelling remain visible in diagnostics. Use it for a message explaining that the input must be good or damaged. Close the match and function braces, keeping the match as the method's result.
Press Ctrl+O, then Enter to confirm src/condition.rs; Ctrl+X exits nano. Do not type shell commands while still inside the editor. Read the saved edit:
cat src/condition.rs
cargo run compiles and starts the prepared program. --quiet hides Cargo progress, and -- passes the following arguments to the program. rows validates and displays every row from the actual file; it does not deduplicate yet.
cargo run --quiet -- rows data/two-good.tsv
asset_id type_id condition
a light-a good
b light-a good
The normal row view should now show both IDs and their good conditions. Compare it with the other source:
cargo run --quiet -- rows data/one-damaged.tsv
asset_id type_id condition
a light-a good
b light-a damaged
This matches your hand prediction: still two scanned items, only one good. You have written a real decision in the actual reader's path, not a separate demonstration program.
The controlled input contract
Read the actual plan too:
cat data/plan.tsv
Its fields are type_id, name, required. light-a and light-b both display Flashlight but are distinct type IDs. A name is display text, not identity. Positive requirements without scans are shortages; zero requirements without scans have zero shortage. The supplied reader applies these rules:
- IDs start with a lowercase ASCII letter, followed only by lowercase letters, digits or
-. They are case-sensitive. - Conditions are exactly
goodanddamaged; no other spelling is accepted. - Names are nonempty printable ASCII, including spaces. Quantities are decimal digits valued 0–999, with leading zeros allowed. Plan type IDs cannot repeat even with identical content.
- The exact three-field header must be first. LF/CRLF, a missing final newline and completely empty later lines are allowed. Whitespace-only lines are not empty. Bare carriage returns are not line endings. Fields are not trimmed: spaces in an ID or condition are errors, while spaces in a name are retained.
ASCII here means basic English letters, digits, spaces and punctuation; names exclude control characters. This is a small defined format, not a general CSV reader. Your condition method supplies the two-state decision within that prepared reader.
Recover a source edit
For a Rust error, read Cargo's file and line, reopen that file, search for the affected function and repair the edit. Save, exit and retry the failed command. To discard an accidental edit that is still unsaved, use Ctrl+X, then N at the save question. That does not undo earlier saved changes.
Only when recovering a saved mistake, restore your own condition checkpoint rather than resetting setup. In the same Terminal, its variable still identifies the backup. In a new Terminal, repeat PATH and cd, then list this phase's actual directories:
ls -d .edit-before-condition.*
-d lists directory names rather than contents; * matches generated suffixes. Run the following command, paste one exact printed directory name while the shell waits, and press Enter. read stores that line in the variable for this session:
read rs03_before_condition
Inspect the selected file. If the path or phase is wrong, repeat selection and inspection; if no backup exists, repair with the reference instead of guessing a suffix.
cat "$rs03_before_condition/condition.rs"
After confirming your intended pre-edit source, this command deliberately replaces only the allowed condition file:
cp -p "$rs03_before_condition/condition.rs" src/condition.rs
It restores the unfinished parser, so implement the condition decision again and repeat both row commands. The reference below is for comparing a particular method after your attempt, not for replacing the whole module as the normal editing operation.
Complete condition reference for comparison and recovery
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Condition { Good, Damaged }
impl Condition {
pub fn parse(value: &str) -> Result<Self, String> {
match value {
"good" => Ok(Self::Good),
"damaged" => Ok(Self::Damaged),
_ => Err(format!("condition must be good or damaged, got {value:?}")),
}
}
pub fn name(self) -> &'static str {
match self { Self::Good => "good", Self::Damaged => "damaged" }
}
}
Collect Physical Assets by ID
In this step, implement identity collection while retaining distinct physical assets and counting identical repeats once. Continue in Terminal, in /home/labex/project/field-kit. Read the larger source:
cat data/scans.tsv
Before coding, predict the unique items. Asset a occurs twice with the same type and condition; a, b and c are three separate lights of type light-a. Asset d belongs to light-b, a different type with the same plan display name. e and f belong to the unplanned type other; legitimate extra equipment must not disappear. There should be six physical IDs, not seven and not one per type or name.
Read the existing collector scaffold:
cat src/identity.rs
Scan contains asset_id, type_id, condition and diagnostic line. The supplied use crate::Scan imports that struct from this library. The function borrows &[Scan], so it must not alter or take ownership of the caller's records. Its Result returns a vector of unique records or a useful conflict explanation.
A HashMap can associate one record with each asset ID. An insertion for an existing key replaces its previous value, so look it up before inserting. Decide separately what to do for a new ID, an identical repeated ID/type/condition, and a contradictory repeat. Do not compare the entire Scan with derived equality: identical content naturally occurs on different line numbers. Compare type and condition; retain both line numbers only for the diagnostic.
Preserve this phase's source before editing:
rs03_before_identity=$(mktemp -d .edit-before-identity.XXXXXX) && cp -p src/identity.rs "$rs03_before_identity/identity.rs"
nano -c src/identity.rs
This starter does not import HashMap. Keep use crate::Scan; and add the standard-library import alongside it at the top:
use std::collections::HashMap;
std is Rust's standard library; crate is this project's library. Importing a name makes it available in this module, not in every file. An equivalent ordered-map or sorting implementation is also allowed, with whatever imports it actually needs.
Search with Ctrl+W for pub fn collect_assets and press Enter. Rename _scans to scans, preserving its slice type. Implement the function's decisions; keep other definitions and the completed condition module unchanged.
One map-based approach starts a mutable HashMap<String, Scan>. For each borrowed scan, get(&scan.asset_id) returns Some with an earlier record or None. if let Some(previous) enters only when a record exists. If either the type or condition disagrees, return an Err naming the full asset ID, current line and first line. format! can combine those fields into an owned message. If the content agrees, continue skips insertion of this repeat.
For a new ID, insert a cloned ID and cloned record; .clone() creates owned copies from the borrowed input. After examining every scan, move the stored records into a new Vec and return Ok with that vector. Vec::new() creates an empty vector and push appends an item. In a loop such as for (_, scan) in by_id, the pair is unpacked and _ ignores the key. HashMap iteration order is unspecified; the provided formatter sorts asset IDs for display, so your internal order need not be fixed.
Save with Ctrl+O, Enter, then Ctrl+X. Inspect your saved function before running:
cat src/identity.rs
Compare the actual raw rows first:
cargo run --quiet -- rows data/scans.tsv
The repeated a is still present there. Now exercise your completed collector:
cargo run --quiet -- assets data/scans.tsv
asset_id type_id condition
a light-a good
b light-a good
c light-a good
d light-b damaged
e other good
f other damaged
Confirm all six IDs, types and conditions. Only the identical repeat disappeared from the collected view; the input file was not edited. Distinct IDs of the same type, damage and unplanned equipment all remained. The next step checks contradictions, which must reject the whole collected result rather than choose the first or last record silently.
For a compiler error, repair the reported line in nano, including a missing import if that is the problem. Only when undoing a saved mistake, the following conditional recovery uses your own pre-collector file. In a new Terminal repeat PATH and cd, then list the phase's actual backup names:
ls -d .edit-before-identity.*
Run read, paste one exact printed directory name, and press Enter. Confirm its contents before restoring. If no backup matches, use the reference for a specific repair instead of guessing a path.
read rs03_before_identity
cat "$rs03_before_identity/identity.rs"
After confirming the intended source, restore only the allowed identity file:
cp -p "$rs03_before_identity/identity.rs" src/identity.rs
That restores the original collector stub, including its original imports. Re-add the import needed by your implementation and complete the function before testing again. It does not remove your successful condition parser or change the raw inputs.
Complete identity reference for comparison and recovery
Compare the relevant decision after attempting your own implementation, and make a local repair through nano. This is the single complete identity source used to prepare later independent units.
use crate::Scan;
use std::collections::HashMap;
pub fn collect_assets(scans: &[Scan]) -> Result<Vec<Scan>, String> {
let mut by_id: HashMap<String, Scan> = HashMap::new();
for scan in scans {
if let Some(previous) = by_id.get(&scan.asset_id) {
if previous.type_id != scan.type_id || previous.condition != scan.condition {
return Err(format!(
"line {}: conflicting asset {} (first at line {})",
scan.line, scan.asset_id, previous.line
));
}
continue;
}
by_id.insert(scan.asset_id.clone(), scan.clone());
}
let mut unique = Vec::new();
for (_, scan) in by_id {
unique.push(scan);
}
Ok(unique)
}
Reject Conflicts and Recover from Input Errors
In this step, you will distinguish contradictory evidence, an unknown condition, and a wrong path. Continue in Terminal, in the project directory. These next failures are intentional: they should print an error and return a nonzero exit status. Do not remove or repair the preserved source fixtures to make them disappear.
The error examples below show the reference implementation's wording. Your own message may use different words: a conflict must identify the actual path, full asset ID and both source lines; an unknown condition must identify its actual path and line and explain the rejected value. Matching the exact prose is not the goal.
First read the conflicting evidence:
cat data/conflict.tsv
It describes asset a as both good and damaged. Run the collector:
cargo run --quiet -- assets data/conflict.tsv
data/conflict.tsv: line 3: conflicting asset a (first at line 2)
The supplied command interface adds the actual input path to your collector's error. No success table is printed. A last-record-wins implementation would hide evidence and could change the result just by reversing the file. Identical repeats are allowed, but contradictions require investigation outside this exercise.
Recover by choosing the valid source again:
cargo run --quiet -- assets data/scans.tsv
Now deliberately read the fixture containing an unknown condition:
cargo run --quiet -- rows data/bad-status.tsv
data/bad-status.tsv: line 2: condition must be good or damaged, got "broken"
This is a format decision, not a damaged asset: damaged is valid data, while broken is outside this course's controlled vocabulary. Read a valid damaged record to recover:
cargo run --quiet -- rows data/one-damaged.tsv
Finally, use a nonexistent singular filename on purpose:
cargo run --quiet -- assets data/scan.tsv
The error names data/scan.tsv and says the file cannot be found. That differs from a line error inside an existing file. Correct the path, keeping the actual source unchanged:
cargo run --quiet -- assets data/scans.tsv
The six-asset table should return. If a Rust edit is wrong, reopen the named source in nano and repair the affected function. To undo a saved mistake, use the condition or identity phase checkpoint and the explicit ls/read/cat confirmation procedure taught in the earlier step; restore only that allowed source, then redo its implementation and rerun this final command. Unsaved nano changes can instead be discarded with Ctrl+X and N. Neither case requires setup to run again or a protected input to be edited. Your program now separates source errors from valid repeated, damaged, and extra records. The next lab will use these unique identities to compute shortages.
Summary
You implemented a two-state enum parser and collected equipment by physical asset ID. Different identities survived, identical repeats did not inflate the result, and contradictions produced attributed errors instead of silently replacing evidence. You also recovered from an unknown condition and a wrong filename while retaining the original files. These reliable asset records are the foundation for explaining each type's missing and surplus quantities.



