Rust learning: lessons, notes, and exercise crates

This commit is contained in:
2026-09-06 23:59:08 +07:00
commit 6ddf41aa81
119 changed files with 10873 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
target/
hello_world/main
.playwright-mcp/
+18
View File
@@ -0,0 +1,18 @@
# Mission: Rust
## Why
Land a backend/CLI job that uses Rust. Two months on the book already happened, but the knowledge did not stick — the goal now is real fluency (can write and reason about Rust under pressure), not a second pass at "having read the book."
## Success looks like
- Explain ownership/borrowing rules without hesitating, and predict compile errors before running `cargo build`.
- Write a small CLI or backend service (e.g. a JSON API) from scratch, handling errors with `Result`/`?`, not `panic!`.
- Use structs, enums, traits, generics, and modules idiomatically in own code, not just recognize them in a book example.
- Pass a Rust-focused technical interview or take-home without needing to relearn fundamentals first.
## Constraints
- ~2 months already spent on [The Rust Book](https://doc.rust-lang.org/stable/book/), chapters 1–9 covered (variables through error handling), but retention is weak — treat as "seen before, not owned" until proven otherwise.
- Sessions are crash-course paced: prioritize fast, high-yield retrieval practice over slow first-pass reading.
## Out of scope
- Embedded / `no_std` Rust.
- Deep async internals — only as much async as a backend job needs (revisit if the job requires more).
+71
View File
@@ -0,0 +1,71 @@
# Notes
- User has 2 months of prior exercises in this dir (ch1–9 of the Rust Book: variables, functions, control flow, ownership, slices, structs, enums, modules, collections, error handling) but reports forgetting all of it. Treat ch1–9 as "exposure, not mastery" until a lesson or diagnostic proves otherwise.
- Goal: backend/CLI job. Bias future lessons toward `Result`/error handling, traits/generics, and eventually a backend framework (axum/tokio) over embedded topics.
- Preference confirmed 2026-08-28: diagnostic-first — find actual gaps before building recap lessons, rather than re-teaching everything blind.
- `get-dependecies/` has an unused `trpl = "0.2.0"` dependency (the async-book helper crate) — signals prior intent to reach ch17 (async), not yet attempted.
- **Lesson format rule (2026-08-28):** typing-first. User forgot how to *write* code, not just what it means — every lesson needs hands on keyboard in a real cargo project, `cargo run`/`cargo test` as the feedback loop. See LR-0001.
- User asked to cover topics the diagnostic marked "solid" too. Do not skip them — fold them in as supporting material in typing lessons rather than dedicating lessons to them.
- Do not let the user copy-paste lesson code. State the no-paste rule explicitly in each lesson.
- Always link `reference/rust-syntax.html` from lessons; forgotten syntax was eating the working memory needed for concepts.
- Verify claimed compiler error text by actually running it before shipping a lesson (done for E0004 non-exhaustive, E0382 moved value in lesson 0002).
- Env: rustc/cargo 1.96.1 installed and working. Chrome opens lesson files via `nohup google-chrome --new-window "file://$(realpath …)" & disown` (plain `wslview` blocks the shell; `rm -rf` is blocked by policy — use `mktemp -d` for scratch projects).
- Next lesson candidates, in priority order: (1) reading compiler errors fluently, (2) Modules & Paths (0/1, and `restauran`/`learn-modules` exist as material), (3) enums + `Option`/`Result` modelling, (4) structs+impl by building something, (5) collections drill.
- **Format shift (lesson 0003):** user finished 0002, reported still "feeling like lacking", and asked for spec-driven work — a project brief with acceptance tests, NOT step-by-step syntax guidance, plus syntax examples they can consult. Self-identified weak: packages, enums, structs. Deliver specs + test suites from here; put syntax help in a *different domain* so it cannot be pasted, and collapse hints behind `<details>` so they choose the difficulty.
- Always verify a shipped test suite is passable by writing a private reference impl in a temp dir first (done for 0003: 17/17). Never ship an unproven spec.
- `cargo new <name> --lib` does NOT create `tests/` — instructions must include `mkdir tests`.
- **Two-lesson rule (2026-09-02):** for any topic the user names as weak, ship a *concept* lesson (reading, real compiler output, from zero) BEFORE the *project* lesson. 0003 shipped project-only and was unusable; 0004 backfilled it. See LR-0002.
- **Specs need prose.** Signatures alone are not a specification. Every project brief needs: what the program is for, what its data is, what its commands are — in plain language, written so the type choices (struct vs enum) fall out of the description. Then signatures.
- Diagnostic scores overstate ability: recognition-style questions pass without design ability. Self-reported weakness > quiz score. (Struct 2/2, Enum 1/2 on 0001, yet both needed teaching from zero.)
- Teaching demos: use a domain *different* from the project domain (café for 0004 vs tasks for 0003) so nothing is pasteable. Capture real `cargo run`/`cargo build` output including deliberate errors — never write error text from memory.
- **0003 result (2026-09-02):** 17/17 green, idiomatic code (closures, `iter_mut`, `?`, private field + `&[Task]` accessor). Structs/enums/packages are now *produced*, not just recognised — stop teaching them as weak. See LR-0003.
- **Untested prose is undone prose.** The spec's stderr/exit-1 requirement was the one thing the 17 integration tests could not reach, and it is the one thing that was not done (`println!` for errors, exit 0, `.unwrap()` panic on `done 9`). Either test it or make it the next lesson's drill.
- Drill format that works: operate on the crate the user already wrote, one shell-level check per step (`cargo run --quiet -- fly ; echo $?`). No new test file, and their real project improves.
- Next lesson candidates after 0005: (1) **0006 project** — `String` → `TaskError` enum with `Display`/`Error`/`From` across all four files, plus persistence to a file (`fs`, `io::Error`, real `From` conversion) — the natural project for traits; (2) collections + iterators drill (`HashMap`, `filter`/`map`/`collect`), still untouched since the 1/2 diagnostic; (3) lifetimes, only once traits are solid; (4) then axum/serde, where traits pay off.
- **Dark mode is the house style (2026-09-02, user request).** `assets/style.css` is dark by default and every lesson/reference doc gets it by linking that one file — so a new lesson needs no colour work, just `<link rel="stylesheet" href="../assets/style.css">`. Never inline colours or a per-lesson palette. Panels use `var(--panel)` / `var(--panel-hover)`; `@media print` re-declares the whole variable block as ink-on-paper so printouts stay readable.
- **Typography (2026-09-02, user request):** the old serif stack was unreadable — none of "Iowan Old Style"/Palatino/Georgia/Charter is installed on this machine, so it fell back to DejaVu Serif. Now a humanist sans stack (`Inter`, `Ubuntu Sans`, `system-ui`, …) at `1.2rem` / `1.68` line-height, measure narrowed to `40em`, code at `0.87em` in `DejaVu Sans Mono`. Only fonts actually installed here: Ubuntu Sans, DejaVu Sans, Liberation Sans, DejaVu Sans Mono, Ubuntu Mono — check with `fc-list : family` before naming a font.
- **Quiz authoring checklist (bug found 2026-09-02).** A recall question without `<button class="reveal-btn">Show answer</button>` renders dead: `initRecall` in `assets/quiz.js` bails out if reveal/answer/grade are not all present, so the Got it / Missed it buttons never appear. Four questions in 0005 shipped that way. Before shipping any lesson with a quiz, click every question in the browser — do not eyeball the HTML.
- `quiz.js` marks a graded recall with `opt-correct` / `opt-incorrect`, which the stylesheet did not define — Got it / Missed it gave no colour feedback in any lesson. Fixed in `assets/style.css`; the graded choice now turns green/red and the other button dims to 0.45 opacity.
- **0005 drill result (2026-09-03): all three checks green.** `Display for Task` correct, `unwrap` gone, `run() -> Result<(), String>`, `Command::parse(args)?` with the match on `Command` values (fixed after review), `eprintln!` + `process::exit(1)`, exit 1 on both failure paths, 17/17 still pass. The stderr/exit-1 contract missed in 0003 is now produced. Remaining nits are cosmetic only (`&args` on a `&[String]` param, `match` where `if let Err` would do). Traits/Display/`?` are produced, not just recognised — 0006 can assume them.
- Concrete material for 0006 (used): `"id not found"` is returned by both `command.rs` (no id *argument* given) and `store.rs` (id *not in the store*) — two different failures, identical prose. Their own defect, not an invented example. Enums scored 0/1 on the 0005 recall for exactly this reason.
- **0006 shipped (2026-09-03):** `TaskError` enum + `Display` + `Error::source()` + `From<ParseIntError>` across `error.rs`/`command.rs`/`store.rs`/`main.rs`, driven by a shipped `tests/errors.rs` (7 tests). Reference impl verified in a temp dir first: 17 + 7 = 24 green. Deliberately NOT in 0006: file persistence (that is 0007, and `From<io::Error>` is easier once `TaskError` exists).
- New in 0006 beyond 0005's recipe: `Error::source()`, plus the std rule that a wrapped cause goes in *either* `source()` or `Display`, never both. Cited from the std page, and the tests enforce it (`BadId` Display says "task id must be a number"; the `ParseIntError` sentence is only reachable via `source()`).
- Real compiler output captured for 0006 (never from memory): `E0004` non-exhaustive after adding a variant, `E0432` unresolved import when `pub mod error;` is missing, `E0308` leftover `String` error, `E0271` `?` with no `From` impl (the annotated-target variant of `E0277`, exactly as 0005 predicted), `E0369` missing `PartialEq` seen from the test file.
- **`reference/book-coverage.html` (new, 2026-09-03):** ch1–21 mapped to Read / Produced / Gap, chapter list taken verbatim from the book's `SUMMARY.md`. Answers "am I missing something important?" without guessing. Keep it updated after every lesson — it is now the thing that picks the next lesson. Named gaps: ch8 collections/`HashMap`, ch11 writing own tests, ch13 iterator chains, ch10.3 lifetimes, ch16/17 concurrency+async.
- Lesson order after 0006: (1) **0007** persistence — `fs`, `io::Error`, `From<io::Error>`, `FromStr` to read a task back from a line; (2) **0008** collections + iterators (`HashMap`, `filter`/`map`/`collect`, first hand-written generic fn); (3) **0009** writing your own tests (ch11) — the user has consumed 24 of my tests and written zero; (4) lifetimes as reading practice; then serde → axum → async.
- Quiz checklist ran for 0006 (2026-09-03): served the workspace over `python3 -m http.server` (Chrome/Playwright blocks `file://`), clicked all 6 questions via Playwright — 4 recalls have reveal+grade, 2 MCQs mark correct/incorrect, summary and Copy report work, only console error is a missing favicon. Do this for every lesson; it is three tool calls.
- Style additions for 0006: `blockquote` (verbatim primary-source quotes) and `.gap` (red cell in the coverage table) now live in `assets/style.css`. Both print correctly because the print block re-declares the variables.
- **0006 drill result (2026-09-03): 24/24 green** (17 spec + 7 errors), `tests/errors.rs` byte-identical to the shipped spec. `TaskError` has all 7 variants, `Display` correct, `source()` returns the `ParseIntError` only for `BadId`, `From<ParseIntError>` written. **But `From` is never exercised**: `command.rs` hand-matches `id.parse()` with `Err(e) => return Err(TaskError::BadId(e))` in both `done` and `remove`, duplicated, so the `?` conversion the lesson taught is dead code. Also unchanged from 0005: `match`/`Ok(_) => ()` in `main` instead of `if let Err`. Traits are produced; the gap is *reaching for* `?`+`From` instead of manual matching.
- **0007 shipped (2026-09-03):** files + `FromStr`. `fs::read_to_string`/`fs::write`, `io::Error` + `ErrorKind::NotFound` match guard, `From<io::Error>` (the second `From`, which answers the user's own question), `FromStr for Task` with an associated `type Err`, `env::var` with a default, and a hand-written `PartialEq` because `io::Error` is not `PartialEq`. Reference impl verified in `/tmp/ref7` first: 17 + 7 + 8 = 32 green, spec shipped as `lessons/0007-persist-spec.rs`.
- Real compiler output captured for 0007 by running it: `E0369` (derive `PartialEq` over an `io::Error` field), `E0277` (`?` with no `From<io::Error>`), `E0277` (`Task: FromStr` not satisfied), `E0046` (missing `type Err`), `E0119` (conflicting `From` impls). Never from memory.
- **Drill steps need a mechanical check, not prose (third repeat — see LR-0005).** 0006 asked in prose for the two `match id.parse()` blocks to collapse to `?`; it did not happen. 0007 makes it step 0 with `grep -c "match id.parse" src/command.rs` → `0`.
- The user asks mechanism questions after each lesson (this session: can two `From` impls exist, where does the wrapped message go, must a dev walk `source()` by hand). Answer with runnable evidence — a scratch binary in `/tmp` and its real output beats prose, and takes one tool call.
- Deliberately left un-idiomatic in 0007 so 0008 has the user's own code to rewrite: the `for` loop in `Store::load` that pushes into a `Vec` is a `collect::<Result<Vec<_>, _>>()` waiting to happen.
- **0008 shipped (2026-09-04):** iterators + `HashMap` + first generic function. Reference impl verified in `/tmp/ref8` first: 17 + 7 + 8 + 14 = **46 green**, `cargo clippy --all-targets` clean apart from one pre-existing `Default for Store` suggestion. Spec shipped as `lessons/0008-collections-spec.rs` (14 tests, 162 lines).
- What 0008 makes the user produce: `Store::load` rewritten as `collect::<Result<Vec<Task>, TaskError>>()?` (the loop 0007 left behind), `count_by_priority` on a `HashMap<Priority, usize>`, `remove_completed` via `Vec::retain`, `titles_with` as a `filter`+`map`+`collect` chain, `find` vs `position` distinguished, and `fn tally<T, K, F>(items: &[T], key: F) -> HashMap<K, usize> where K: Eq + Hash, F: Fn(&T) -> K` written from the signature up in a new `stats.rs`.
- Real compiler output captured for 0008 by running it (never from memory): `unused Map that must be used` + "iterators are lazy and do nothing unless consumed" (no consumer), `E0283` type annotations needed on a bare `collect()`, `E0277` `Priority: Eq`/`Priority: Hash` not satisfied at the `tally` call site, `E0502` an `iter_mut()` borrow held across a `self.books.len()` read, `E0507` cannot move `task.priority` out from behind `&Task`, `E0004` non-exhaustive `match` after adding `Stats`/`Clear`.
- **The `Copy` derive is the interesting one.** Keying a `HashMap` by an enum field read through `&T` forces the user to choose between `.clone()`, `#[derive(Clone, Copy)]`, and keying by `&str` — a real ownership decision with three defensible answers, not a syntax lookup. It is quiz question 4 (Ownership) for that reason.
- 0008 ends with a runnable payoff, not just green tests: two new CLI commands (`stats`, `clear`) wired through `Command`/`main`, so the `HashMap` and the `retain` are visible from the terminal. Real session captured in the lesson verbatim (`stats` → `high 1 / medium 1 / low 1`, `clear` → `cleared 1 completed`, exit 0).
- Reference doc grew three sections, all anchored and linked from 0008: `#iterators` (the one-method trait, the three ways in, adapters vs consumers, `collect` targets, `lines()` vs `split('\n')`, the four errors), `#hashmap-keys` (`Eq + Hash`, the `entry` idiom, arbitrary order, `BTreeMap` as the sorted alternative), and a generic-function block in `#traits` (monomorphisation, the `where` clause as a two-way contract, `Fn`/`FnMut`/`FnOnce`).
- Quiz checklist ran for 0008 (2026-09-04): served over `python3 -m http.server 8899`, clicked all 6 questions through Playwright — 2 MCQs mark correct/incorrect, 4 recalls reveal + grade, summary reports `6 of 6 answered, 6 correct` broken down by topic, every local link and `#anchor` resolves, only console error is the missing favicon.
- Interleaving in the 0008 quiz is deliberate: only 2 of 6 questions are about iterators. The rest revisit `HashMap` keys, the `Copy`/`clone` ownership choice, and trait bounds — spaced retrieval of 0005/0006/0007 material inside a new lesson.
- **Coverage map after 0008:** ch8 and ch13 flip Gap/Partial → Produced, ch10.1 Read → Produced. One core gap remains: **ch11, writing your own tests** — the user has now run 46 of my tests and written zero `#[test]`. That is 0009, and the map's "order that follows" section says so. Then lifetimes (ch10.3) as reading practice — 0008 had the user write one unknowingly, since `titles_with` returns `Vec<&str>` borrowed from `&self` and elision hid the annotation.
- Nothing deliberately left un-idiomatic in 0008 — 0009 supplies its own material, because the target is the tests the user writes rather than the code they refactor.
- **Lesson format rule (2026-09-04, user-reported):** *define the demo domain's data model before the first example.* 0008 shipped with `Book`, `Shelf`, `book()`, and `shelf_of()` used across ten snippets and defined nowhere — the user could not tell what fields a book had, so every example needed guessing. Fixed by adding a "The demo domain, in full" section before Part 1: both type definitions, both helpers, the literal four-book `Vec`, and an explicit note that `shelf` (lowercase) is the `Vec<Book>` while `Shelf` (capitalised) is the enum. Every future lesson with a demo domain does this first.
- Related trap the same complaint exposed: **one variable name must have one element type.** 0008 used `shelf` for both a `Vec<Book>` (closure `|b|`) and a `Vec<&str>` (closure `|t|`), because the two error examples came from a different scratch file. Fixed by re-running `/tmp/demo8/examples/{e1,e2}.rs` with a `titles` binding and re-capturing real rustc output — never hand-edit a variable name inside quoted compiler output, re-run it.
- **Layout rule (2026-09-04, user-decided): narrow prose column, small code font, and let the few verbatim blocks scroll.** 13 of 28 code blocks in 0008 were scrolling horizontally, because prose was capped at `max-width: 40em` while verbatim rustc output runs to ~105 characters. Two fixes were tried and rejected: (1) letting `pre` break out of the prose column with negative margins — the user said code hanging past the paragraph above it looks broken; (2) widening the body to `65rem` so prose and code share one edge — the user said the long measure strained their eyes to read. Final: `body { max-width: 40em }` is back, `pre` drops to `font-size: 0.73em` (0.62em in print), and the handful of blocks still too wide scroll on the wheel.
- What that buys: the column fits ~81 characters of code. **Every authored line in every lesson and in `rust-syntax.html` is now trimmed to fit it** — the pass that did it lives in the session log, but the rule going forward is simply: keep authored code and its trailing `//` comment inside 81 characters.
- What still scrolls, and why it must: verbatim rustc output only — 83 to 103 characters, `E0502`/`E0277`/`E0507`/`E0432`, the `^^^^` and `-----` spans. Those cannot be wrapped or hand-shortened, because the underlines have to stay column-aligned or they point at the wrong token. Measured at 1385px with layout settled: `rust-syntax` 4/51, 0005 3/34, 0007 2/22, 0008 4/28, and 0/N on 0001-0004 and 0006. All quizzes still click through.
- Measure `pre` overflow **after** a settle delay. A bare `iframe.onload` read reported 22/51 on `rust-syntax` where the settled read gives 4/51 — the scrollbar geometry is not final at load.
- **Keep terminal sessions short with a shell helper** (`run(){ ... }`), as 0008 does. 0007 repeated the full `TASKS_FILE=t.txt cargo run -q --manifest-path ...` on every line and was the one block still too wide; re-recorded with a `run()` helper — re-run the session, never retype it by hand.
- **Lesson format rule (2026-09-04, user-reported): a lesson must read top to bottom, and no name may appear before it is defined.** 0008 put `tally(self.tasks(), |task| task.priority)` inside the Part 5 error output, three sections before Part 6 defines `tally` — the user could not read the error, because the call site it pointed at was meaningless. I did not notice, because I wrote the lesson as a whole and read it as a whole; the user reads it in one direction, once. Captured compiler output is verbatim and must never be hand-edited (see the `shelf`/`titles` trap above), so the fix is always to introduce the name *earlier* — a signature plus a one-line gloss before the first sighting — never to alter the quote or reorder the parts. **Ship check: read the lesson in order and list every identifier at its first appearance; if it is not defined above that point, it is a bug.** This is the demo-domain rule one level up: that one covers the demo's data model, this one covers every other name, including ones the drill will define later.
- **The "specs need prose" rule (line 18) covers drill steps too — enforcement failure found 2026-09-05.** 0008 Step 3 shipped as two bare signatures (`titles_with`, `remove_completed`) plus one note about `&str`, and the user was blocked: a signature says what goes in and out, never what the function is *for* or what its edge cases are. I had the behaviour, it was in the six spec tests — order preserved, empty `Vec` not an error, ids not renumbered, counter untouched, `0` is a legal answer — and I left the user to reverse-engineer prose out of `assert_eq!`. Fixed by writing those out. **Rule going forward: every function a drill asks for gets a plain-language sentence on what it is for, plus the edge cases its tests pin down, written from the test file at authoring time. A signature is never the specification.** Step 5 of the same drill is the pattern that works — the captured terminal session specifies both new commands by example, so no prose was needed there.
- **0008 drill result (2026-09-05): 46/46 green, library produced, CLI half missed three requirements.** `tally` written from the signature, `load` as one `collect::<Result<Vec<Task>, TaskError>>()?`, `retain`, the `filter`/`map`/`collect` chain — all correct. But `stats` printed high/low/medium (`main.rs` looped `[Priority::High, Low, Medium]`), `clear` printed nothing and dropped the `usize`, and nothing tested either, because `run` lives in `src/main.rs` where no `tests/` file can import it. Third repeat of "untested prose is undone prose" (LR-0003, LR-0005), first time the cause is structural. See LR-0006.
- **0009 shipped (2026-09-05):** writing your own tests (ch11). Demo domain is `/tmp/heating` — a thermostat with a private field, a private helper, one panicking constructor and one `Result` setter, so the page can show `#[should_panic]` and a `Result`-returning test against the same type. All 28 code/output blocks captured by running them; no new spec file, because the skill is producing assertions rather than satisfying mine.
- **New grading device: `lessons/0009-mutants.sh`.** Copies the crate to a temp dir, applies one `sed` mutation, runs the user's tests, restores nothing (the copy is thrown away) — six mutations covering stats order, the zero-count line, the clear count, the `Display` line, `Status::parse`'s `in-progress` arm, and `Command::parse`'s case folding. Verified to discriminate: `0 killed, 6 survived` against only the 46 shipped tests, `6 killed, 0 survived` against the reference impl in `/tmp/ref9`. Pre-drill run on the user's crate: `0 killed, 3 survived, 3 skipped` (three mutations target `src/cli.rs`, which the drill creates). Drill's finishing condition is the report, not a test count — reuse this device for any lesson where the deliverable is tests.
- The 0009 refactor is the point, not scaffolding: `run` moves from `src/main.rs` to `src/cli.rs` and takes `out: &mut impl Write`, so `main` passes `io::stdout().lock()` and a test passes `Vec<u8>` and asserts on the bytes. That single parameter is what makes the three 0008 CLI defects testable, and it is the same move that makes an axum handler testable later. Reference impl `/tmp/ref9`: 4 unit + 6 cli + 14 + 7 + 3 + 8 + 17 = 59 green, no warnings, CLI session verified end to end (`stats` in high/medium/low order, `cleared 1 completed`, `done 9` → exit 1).
- Deliberately NOT in 0009: doc tests (`///` examples, ch14), `#[bench]`, `assert_cmd`/`predicates` for subprocess testing, `proptest`, and `cargo-mutants` (the real version of the shipped script). Named in a "then stop" section so the user knows they exist and why they are not next.
- **Test names drive the width of captured output.** Five of 28 blocks in 0009 still scroll, all of them on cargo's own 93–97 character `test result:` line, which cannot be shortened. Four more were scrolling only because my demo test names were 34–40 characters long (`a_room_below_the_target_is_heating`, `every_legal_target_survives_a_round_trip`); renaming them to ~17–30 characters and **re-running the captures** fixed those blocks. Never hand-edit the quoted output to fit — rename in the source and re-run (same rule as the `shelf`/`titles` trap, NOTES line 55).
- Quiz checklist ran for 0009 (2026-09-05): served over `python3 -m http.server 8899`, clicked all 6 questions through Playwright — 3 MCQs mark correct/incorrect, 3 recalls reveal + grade, summary reports `6 of 6 answered, 6 correct` split Tests 3/3, Traits 1/1, Collections 1/1, Modules 1/1; every local link and `#anchor` resolves (including the new `rust-syntax.html#tests`); console clean, zero errors this time (the favicon 404 is gone because the page is served, not opened from `file://`).
- Reference doc gained `#tests` (the attribute pair, the three macros and what each failure prints, `should_panic` vs a `Result` test, the unit-vs-integration access table, the `tests/common/mod.rs` spelling, the writer-injection shape, the runner flags, `E0433`/`E0603`/`E0616`) and its title/subtitle now say ch. 1–13. Coverage map: ch8, ch10.1 and ch13 flip to Produced after the 0008 drill; ch11 is "Taught — drill pending"; the "order that follows" section now names lifetimes (10.3) and patterns (19) as the remainder, then serde → axum → async.
- After the 0009 drill the book's core has no Gap left except ch10.3 lifetimes (reading practice) and ch19 patterns (one page). Next real decision is serde vs a first axum service — ask the user which, since both are now reachable.
+32
View File
@@ -0,0 +1,32 @@
# Rust Resources
## Knowledge
- [Book: _The Rust Programming Language_ (official)](https://doc.rust-lang.org/stable/book/)
Primary source for all fundamentals lessons (ch1–9 and beyond). Use for: syntax, ownership rules, canonical examples.
- [Rust by Example](https://doc.rust-lang.org/rust-by-example/)
Terse, runnable code samples per concept, no prose. Use for: quick syntax lookup without re-reading the book.
- [std library docs](https://doc.rust-lang.org/std/)
Use for: exact method signatures (`Vec`, `HashMap`, `Result`, `Option`, …) when writing real code.
- [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/)
Use for: idiomatic backend/library code once past fundamentals (naming, error types, trait design). Specifically [C-GOOD-ERR](https://rust-lang.github.io/api-guidelines/interoperability.html#error-types-are-meaningful-and-well-behaved-c-good-err) — the checklist a reviewer applies to an error type: implement `Error`, be `Send + Sync`, never use `()`, and keep `Display` messages lowercase without trailing punctuation. Quoted verbatim in lesson 0006.
- [std: `std::error::Error`](https://doc.rust-lang.org/std/error/trait.Error.html)
Two screens, and it is the definition of what an error *is*: the `Debug + Display` supertraits, plus `source()` for a wrapped cause. States the rule that a cause belongs in *either* `source()` or `Display`, never both.
- [Book table of contents (`SUMMARY.md`)](https://github.com/rust-lang/book/blob/main/src/SUMMARY.md)
The authoritative chapter list. Use for: checking coverage claims against the real book instead of memory — `reference/book-coverage.html` is built from it.
- [std: `std::str::FromStr`](https://doc.rust-lang.org/std/str/trait.FromStr.html)
One screen, and it is what `.parse()` calls. Use for: the associated-type pattern (`type Err`) and the `Point` example, which is the same shape as `Task` in 0007.
- [std: `std::fs`](https://doc.rust-lang.org/std/fs/) and [`std::io::ErrorKind`](https://doc.rust-lang.org/std/io/enum.ErrorKind.html)
Use for: exact signatures of `read_to_string`/`write`, and the list of io failure kinds you can match on. `ErrorKind::NotFound` is the one that means "first run", not "broken".
- [Book ch12 — An I/O Project](https://doc.rust-lang.org/stable/book/ch12-00-an-io-project.html)
Primary source for lesson 0007: files, stderr, and `env::var`. 12.2 and 12.5 are the two sections that matter.
### Gaps
- No curated async/backend-framework resource yet (axum/tokio). Add once lessons reach networking — a `trpl` dependency already sits unused in `get-dependecies/`, signaling this is coming.
- Deliberately deferred, not missing: [`thiserror`](https://docs.rs/thiserror) (derives the `Display`/`From` code written by hand in 0006) and [`anyhow`](https://docs.rs/anyhow) (application-level `Box<dyn Error>` with context). Both are what real crates use; neither teaches what the trait does. Reach for them on the second real project.
## Wisdom (Communities)
- [r/rust](https://reddit.com/r/rust) — active, well-moderated. Use for: code review requests, "is this idiomatic?" checks.
- [users.rust-lang.org](https://users.rust-lang.org) — official user forum. Use for: "why won't this compile" borrow-checker questions, and the **Code Review** category. First concrete ask, set after 0006: post `tasks/src/error.rs` and ask whether one enum for both parse and store failures is right, or whether those should be two types with a wrapping variant.
- [This Week in Rust](https://this-week-in-rust.org) — weekly newsletter. Use for: staying current once past fundamentals.
+158
View File
@@ -0,0 +1,158 @@
// Shared quiz engine for lessons. Two question types, both graded client-side, no server.
//
// Recall (free-recall self-check — best for storage strength):
// <div class="q" data-type="recall" data-topic="Ownership">
// <p class="topic">Ownership</p>
// <p class="prompt">...question...</p>
// <button class="reveal-btn">Show answer</button>
// <div class="answer hidden">...answer text...</div>
// <div class="grade hidden">
// <button data-grade="hit">Got it</button>
// <button data-grade="miss">Missed it</button>
// </div>
// </div>
//
// Multiple choice (instant feedback):
// <div class="q" data-type="mcq" data-topic="Enums">
// <p class="topic">Enums</p>
// <p class="prompt">...question...</p>
// <div class="options">
// <button class="opt" data-correct="true">...</button>
// <button class="opt" data-correct="false">...</button>
// </div>
// <div class="explain hidden">...explanation...</div>
// </div>
//
// Bottom of page needs:
// <div id="summary"><div id="summary-body"></div><p id="summary-total"></p>
// <button id="report-btn">Copy report</button><pre id="report-output" class="hidden"></pre></div>
(function () {
function all(sel, ctx) {
return Array.from((ctx || document).querySelectorAll(sel));
}
function initRecall(q) {
var revealBtn = q.querySelector(".reveal-btn");
var answer = q.querySelector(".answer");
var grade = q.querySelector(".grade");
if (!revealBtn || !answer || !grade) return;
revealBtn.addEventListener("click", function () {
answer.classList.remove("hidden");
grade.classList.remove("hidden");
revealBtn.classList.add("hidden");
});
all("button", grade).forEach(function (btn) {
btn.addEventListener("click", function () {
q.dataset.result = btn.dataset.grade === "hit" ? "hit" : "miss";
q.classList.add("graded");
all("button", grade).forEach(function (b) {
b.disabled = true;
});
btn.classList.add(btn.dataset.grade === "hit" ? "opt-correct" : "opt-incorrect");
updateSummary();
});
});
}
function initMcq(q) {
var opts = all(".opt", q);
var explain = q.querySelector(".explain");
opts.forEach(function (opt) {
opt.addEventListener("click", function () {
if (q.classList.contains("graded")) return;
var correct = opt.dataset.correct === "true";
q.classList.add("graded");
q.dataset.result = correct ? "hit" : "miss";
opts.forEach(function (o) {
o.disabled = true;
if (o.dataset.correct === "true") o.classList.add("correct");
});
if (!correct) opt.classList.add("incorrect");
if (explain) explain.classList.remove("hidden");
updateSummary();
});
});
}
function byTopic() {
var map = {};
all(".q").forEach(function (q) {
var topic = q.dataset.topic || "General";
map[topic] = map[topic] || { hit: 0, graded: 0, total: 0 };
map[topic].total++;
if (q.dataset.result) {
map[topic].graded++;
if (q.dataset.result === "hit") map[topic].hit++;
}
});
return map;
}
function updateSummary() {
var body = document.getElementById("summary-body");
var total = document.getElementById("summary-total");
var reportBtn = document.getElementById("report-btn");
if (!body) return;
var map = byTopic();
var lines = [];
var sumHit = 0,
sumGraded = 0,
sumAll = 0;
Object.keys(map).forEach(function (topic) {
var t = map[topic];
sumHit += t.hit;
sumGraded += t.graded;
sumAll += t.total;
var pct = t.graded ? Math.round((100 * t.hit) / t.graded) : null;
lines.push(
topic +
": " +
t.hit +
"/" +
t.graded +
(t.graded < t.total ? " (of " + t.total + ")" : "") +
(pct === null ? "" : " — " + pct + "%")
);
});
body.textContent = lines.join("\n");
if (total) total.textContent = sumGraded + " of " + sumAll + " answered, " + sumHit + " correct.";
if (reportBtn) reportBtn.disabled = sumGraded === 0;
}
function buildReport() {
var map = byTopic();
var weak = [];
var solid = [];
Object.keys(map).forEach(function (topic) {
var t = map[topic];
var entry = topic + " (" + t.hit + "/" + t.total + ")";
if (t.hit < t.total) weak.push(entry);
else solid.push(entry);
});
var lines = ["Rust diagnostic result:"];
lines.push(weak.length ? "Weak: " + weak.join(", ") : "Weak: none");
lines.push(solid.length ? "Solid: " + solid.join(", ") : "Solid: none");
return lines.join("\n");
}
document.addEventListener("DOMContentLoaded", function () {
all(".q").forEach(function (q) {
if (q.dataset.type === "recall") initRecall(q);
if (q.dataset.type === "mcq") initMcq(q);
});
updateSummary();
var reportBtn = document.getElementById("report-btn");
if (reportBtn) {
reportBtn.addEventListener("click", function () {
var report = buildReport();
var out = document.getElementById("report-output");
if (out) {
out.textContent = report;
out.classList.remove("hidden");
}
if (navigator.clipboard) navigator.clipboard.writeText(report).catch(function () {});
});
}
});
})();
+224
View File
@@ -0,0 +1,224 @@
/* Shared stylesheet for all lessons and reference docs. Quiet, humanist sans, generous size.
Dark by default; the print block below swaps back to ink-on-paper. */
:root {
--ink: #e4e1d9;
--paper: #15171b;
--muted: #9b978d;
--rule: #2f333a;
--panel: #1d2025;
--panel-hover: #24282e;
--accent: #e3a172;
--good: #74c48a;
--good-bg: #172a1d;
--bad: #e8796e;
--bad-bg: #2b1715;
color-scheme: dark;
}
* { box-sizing: border-box; }
body {
max-width: 40em;
margin: 3rem auto 6rem;
padding: 0 1.5rem;
background: var(--paper);
color: var(--ink);
font-family: "Inter", "Ubuntu Sans", Ubuntu, system-ui, "Segoe UI", -apple-system,
"Noto Sans", "DejaVu Sans", sans-serif;
font-size: 1.2rem;
line-height: 1.68;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3 {
font-weight: 600;
line-height: 1.25;
}
h1 { font-size: 2.1rem; margin-bottom: 0.2rem; letter-spacing: -0.01em; }
h1 + .subtitle { color: var(--muted); font-size: 1.02rem; margin-top: 0; margin-bottom: 2rem; }
h2 { font-size: 1.45rem; margin-top: 2.5rem; border-top: 1px solid var(--rule); padding-top: 1.5rem; }
a { color: var(--accent); text-decoration: underline dotted; text-underline-offset: 2px; }
a:hover { text-decoration-style: solid; }
code, pre {
font-family: "JetBrains Mono", "Cascadia Mono", "DejaVu Sans Mono",
"Ubuntu Sans Mono", Consolas, Menlo, monospace;
font-size: 0.87em;
font-variant-ligatures: none;
}
pre {
background: var(--panel);
/* Smaller than inline code, so long rustc output overflows by as little as possible.
Wrapping is not an option: the `^^^^` underlines must stay column-aligned or they
point at the wrong token. Blocks that still exceed the column scroll sideways. */
font-size: 0.73em;
line-height: 1.5;
border: 1px solid var(--rule);
border-radius: 4px;
padding: 0.9em 1em;
overflow-x: auto;
}
code { background: var(--panel); padding: 0.1em 0.3em; border-radius: 3px; }
pre code { background: none; padding: 0; font-size: 1em; } /* no compounding */
.cite {
font-size: 0.85em;
color: var(--muted);
border-left: 2px solid var(--rule);
padding-left: 0.7em;
margin: 0.5em 0 0;
}
.callout {
border: 1px solid var(--rule);
background: var(--panel);
border-radius: 6px;
padding: 1em 1.2em;
margin: 1.5rem 0;
font-size: 0.95em;
}
/* Quoted primary sources — verbatim text from the book, std docs, or guidelines */
blockquote {
margin: 1.5rem 0 0.4rem;
padding: 0.2em 0 0.2em 1.1em;
border-left: 3px solid var(--accent);
color: var(--ink);
font-size: 0.97em;
}
blockquote p { margin: 0.4em 0; }
blockquote + .cite { margin-top: 0; }
/* Coverage tables: a cell that names a real gap */
.gap { color: var(--bad); font-weight: 600; }
/* Quiz widget */
.q {
border-top: 1px solid var(--rule);
padding: 1.4rem 0;
}
.q .topic {
display: inline-block;
font-size: 0.75rem;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 0.4em;
}
.q .prompt { margin: 0 0 0.8em; }
.q button {
font-family: inherit;
color: inherit;
font-size: 0.92rem;
background: transparent;
border: 1px solid var(--rule);
border-radius: 4px;
padding: 0.35em 0.8em;
cursor: pointer;
margin: 0.2em 0.4em 0.2em 0;
}
.q button:hover:not(:disabled) { background: var(--panel-hover); }
.q button:disabled { cursor: default; }
.options { display: flex; flex-direction: column; align-items: flex-start; }
.opt { text-align: left; width: 100%; }
.opt.correct, .grade button.opt-correct { border-color: var(--good); background: var(--good-bg); color: var(--good); }
.opt.incorrect, .grade button.opt-incorrect { border-color: var(--bad); background: var(--bad-bg); color: var(--bad); }
.q button:disabled { opacity: 1; }
.grade button:disabled:not(.opt-correct):not(.opt-incorrect) { opacity: 0.45; }
.answer, .explain {
background: var(--panel);
border-left: 3px solid var(--accent);
padding: 0.7em 1em;
margin-top: 0.6em;
}
.grade button[data-grade="hit"]:hover { border-color: var(--good); }
.grade button[data-grade="miss"]:hover { border-color: var(--bad); }
.hidden { display: none !important; }
#summary {
margin-top: 2.5rem;
border-top: 2px solid var(--ink);
padding-top: 1rem;
}
#summary-body { white-space: pre-line; }
#report-output {
white-space: pre-line;
background: var(--panel);
padding: 0.8em 1em;
border-radius: 4px;
margin-top: 0.8em;
}
/* Collapsible hints */
details {
border: 1px solid var(--rule);
border-radius: 6px;
padding: 0.6em 1em;
margin: 0.7rem 0;
background: var(--panel);
}
details[open] { background: var(--panel-hover); }
summary {
cursor: pointer;
font-weight: 600;
color: var(--accent);
}
details > *:last-child { margin-bottom: 0; }
/* Spec tables */
table {
border-collapse: collapse;
width: 100%;
margin: 1rem 0;
font-size: 0.95em;
}
th, td {
border-bottom: 1px solid var(--rule);
padding: 0.4em 0.6em;
text-align: left;
vertical-align: top;
}
th { border-bottom: 2px solid var(--ink); }
footer {
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid var(--rule);
color: var(--muted);
font-size: 0.9rem;
}
@media print {
:root {
--ink: #1a1a1a;
--paper: #fff;
--muted: #555;
--rule: #ccc;
--panel: #f4f2ec;
--panel-hover: #eeece5;
--accent: #8a3324;
--good: #2a7a3b;
--good-bg: #eaf5ec;
--bad: #a3312a;
--bad-bg: #fbeceb;
color-scheme: light;
}
body { margin: 0.5rem auto; }
.hidden { display: block !important; }
.q button, #report-btn { display: none !important; }
pre { font-size: 0.62em; } /* paper is narrower than a screen, and cannot scroll */
pre, blockquote, table, .q { break-inside: avoid; }
}
+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "control_flow"
version = "0.1.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "control_flow"
version = "0.1.0"
edition = "2024"
[dependencies]
+62
View File
@@ -0,0 +1,62 @@
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
let number = 12;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
{
let x = 5;
println!("this is five {x}")
}
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "function"
version = "0.1.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "function"
version = "0.1.0"
edition = "2024"
[dependencies]
+29
View File
@@ -0,0 +1,29 @@
fn main() {
println!("Hello, world!");
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {y}");
another_function(32, 'a');
let z = five();
println!("the value of z is {z}");
let x = plus_one(5);
println!("the value of x is {x}")
}
fn another_function(a: usize, b: char) {
println!("Another function. {a}, {b}");
}
fn five() -> i32 {
5
}
fn plus_one(x: i32) -> i32 {
x + 1
}
+1
View File
@@ -0,0 +1 @@
/target
+1981
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "get-dependecies"
version = "0.1.0"
edition = "2024"
[dependencies]
rand = "0.8.5"
trpl = "0.2.0"
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}
+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "grader"
version = "0.1.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "grader"
version = "0.1.0"
edition = "2024"
[dependencies]
+63
View File
@@ -0,0 +1,63 @@
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("usage : cargo run -- [score]");
return;
}
let mut scores: Vec<u32> = Vec::new();
for arg in &args[1..] {
match arg.parse::<u32>() {
Ok(score) => {
println!("{score} -> {}", grade_score(score));
scores.push(score);
}
Err(_) => println!("{arg} not a number"),
}
}
if scores.is_empty() {
println!("not a valid score");
} else {
println!("average scores: {}", average(&scores));
}
println!("{args:?}");
}
fn grade_score(score: u32) -> String {
match score {
90..=100 => "A".to_string(),
80..90 => "B".to_string(),
70..80 => "C".to_string(),
_ => "F".to_string(),
}
}
fn average(scores: &Vec<u32>) -> u32 {
let mut total = 0;
for s in scores {
total = total + s;
}
total / scores.len() as u32
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn grade_map_to_letters() {
assert_eq!(grade_score(95), "A");
assert_eq!(grade_score(70), "B");
assert_eq!(grade_score(43), "F");
}
#[test]
fn average_of_three() {
assert_eq!(average(&vec![80, 90, 70]), 80);
}
}
+1
View File
@@ -0,0 +1 @@
/target
+133
View File
@@ -0,0 +1,133 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "guessing_game"
version = "0.1.0"
dependencies = [
"rand",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "zerocopy"
version = "0.8.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
+7
View File
@@ -0,0 +1,7 @@
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
[dependencies]
rand = "0.8.5"
+36
View File
@@ -0,0 +1,36 @@
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("guess the number!!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is {secret_number}");
loop {
println!("please input your guess");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("too small"),
Ordering::Greater => println!("Too big"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "hello_cargo"
version = "0.1.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2024"
[dependencies]
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("Hello from cargo, world!");
}
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("Hello, World");
}
+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "learn-challenges"
version = "0.1.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "learn-challenges"
version = "0.1.0"
edition = "2024"
[dependencies]
+66
View File
@@ -0,0 +1,66 @@
use std::{collections::HashMap, vec};
fn main() {
let list = vec![3, 1, 4, 1, 5];
let med: usize = list.len() / 2;
println!("this is the median {}", list.get(med).unwrap_or(&0));
let mut count = HashMap::new();
for &v in &list {
let x = count.entry(v).or_insert(0);
*x += 1;
}
let mut mode = 0;
let mut max_count = 0;
for (k, v) in &count {
if *v > max_count {
max_count = *v;
mode = *k;
}
}
println!("this is the mode {}", mode);
let latin = "apple banana".to_string();
println!("{latin} = this is the pig latin: {}", pig_latin(&latin));
let latin = "kdlkjlkdsgjh gkjldgjd".to_string();
println!("{latin} = this is the pig latin: {}", pig_latin(&latin));
}
fn pig_latin(s: &String) -> String {
let mut results_words: Vec<String> = Vec::new();
for word in s.split_whitespace() {
let mut vowel_idx = None;
for (i, v) in word.char_indices() {
match v {
'a' | 'i' | 'e' | 'u' | 'o' => {
vowel_idx = Some(i);
break;
}
_ => {}
}
}
let transformed = match vowel_idx {
Some(0) => format!("{}-hay", word),
Some(idx) => {
let x = &word[..idx];
let rmdr = &word[idx..];
format!("{rmdr}-{x}ay")
}
None => word.to_string(),
};
results_words.push(transformed);
}
results_words.join(" ")
}
+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "learn-collections"
version = "0.1.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "learn-collections"
version = "0.1.0"
edition = "2024"
[dependencies]
+76
View File
@@ -0,0 +1,76 @@
enum SpreadsheetCell {
Int(i32),
Float(f32),
Text(String),
}
fn main() {
println!("Hello, world!");
let mut v = vec![1, 2, 3, 4];
v.push(5);
v.push(6);
v.push(7);
let Some(&x) = v.get(3) else {
println!("index not found");
return;
};
for i in &v {
print!("{i}");
}
println!("");
for i in &mut v {
*i = *i * 10;
}
for i in &v {
print!("{i}");
}
println!("");
println!("this is the value of the 100th index {x}");
let _row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Float(3.14),
SpreadsheetCell::Text("bodo".into()),
];
let _s1 = String::from("Hello");
let _s2 = "World".to_string();
let _s1: String = _s1 + &_s2;
println!("THis is: {_s1}");
let _s3 = "FooBar".to_string();
let _s = format!("{_s1} {_s3}");
println!("{}", _s);
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert("blue".to_string(), 1);
scores.insert("red".to_string(), 3);
let score_blue = scores.get(&"blue".to_string()).copied().unwrap_or(0);
let score_red = scores.get(&"red".to_string()).copied().unwrap_or(0);
println!("this is blue: {}, this is red {}", score_blue, score_red);
for (k, v) in &scores {
println!("team {k}: {v} scores");
}
scores.insert("green".to_string(), 10);
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{scores:?}");
scores.entry("blue".to_string()).or_insert(100);
scores.entry("yellow".to_string()).or_insert(100);
println!("{scores:?}");
}
+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "learn-modules"
version = "0.1.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "learn-modules"
version = "0.1.0"
edition = "2024"
[dependencies]
+1
View File
@@ -0,0 +1 @@
pub mod vegetables;
+1
View File
@@ -0,0 +1 @@
pub struct Asparagus {}
+8
View File
@@ -0,0 +1,8 @@
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let _plant = Asparagus {};
println!("Hello, world!");
}
+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "learn-panic"
version = "0.1.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "learn-panic"
version = "0.1.0"
edition = "2024"
[dependencies]
+1
View File
@@ -0,0 +1 @@
alkdsfjlkafdsj
+30
View File
@@ -0,0 +1,30 @@
use std::{fs::File, io::ErrorKind, panic};
fn main() {
let _v = vec![1, 2, 3];
let greeting_file_result = File::open("hello.txt");
let _greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e}"),
},
_ => {
panic!("Problem opening the file: {error:?}");
}
},
};
// v[99];
}
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "learn-struct"
version = "0.1.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "learn-struct"
version = "0.1.0"
edition = "2024"
[dependencies]
+81
View File
@@ -0,0 +1,81 @@
struct User {
active: bool,
username: String,
email: String,
sign_in_count: usize,
}
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn width(&self) -> bool {
self.width > 0
}
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
fn main() {
println!("Hello, world!");
let mut user1 = User {
active: true,
username: String::from("username123"),
email: String::from("user@ecample.com"),
sign_in_count: 2,
};
user1.username = String::from("kiki");
let user2 = build_user("email@asl.com", "kiki");
let _user3 = User {
active: false,
..user2
};
// println!("{}", user2.username); //Error because the username is a String on heap and its already moved to the user3
let rect1 = Rectangle {
width: 30,
height: 20,
};
println!(
"this is the area of the rect {} square pixels",
rect1.area()
);
println!("rect is {rect1:?}");
if rect1.width() {
println!("The rectange have width")
}
let rect2 = Rectangle::square(42);
println!(
"this is the area of the rect2 {} square pixels",
rect2.area()
);
}
fn build_user(email: &str, username: &str) -> User {
User {
active: true,
username: String::from(username),
email: String::from(email),
sign_in_count: 2,
}
}
+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "learn_enum"
version = "0.1.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "learn_enum"
version = "0.1.0"
edition = "2024"
[dependencies]
+46
View File
@@ -0,0 +1,46 @@
enum IpVer {
V4(String),
V6(String),
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(String),
}
fn main() {
println!("Hello, world!");
let home = IpVer::V4("192.14.1.1".into());
let loopback = IpVer::V6("::1".into());
value_in_cents(Coin::Quarter("alaska".to_string()));
let config_max = Some(3u8);
match config_max {
Some(max) => println!("the maximum is configure to be {max}"),
_ => (),
}
if let Some(max) = config_max
&& max < 4
{
println!("Maximum is {max}");
}
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky Penny");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(s) => {
println!("from state {s}");
25
}
}
}
@@ -0,0 +1,14 @@
# Recognition without production: ch1–9 is recall-only, writing ability is gone
Diagnostic (lesson 0001, 18 questions across ch1–9) scored 8/18, but the decisive signal was the user's own report: *"I even forgot how to write the code."* Recognition of concepts is partially intact; **production from a blank file is not**. Future lessons must be typing-first — quizzes measure this gap but do not close it.
**Evidence** — self-graded diagnostic, 2026-08-28:
- Weak: Ownership 1/2, Error Handling 1/2, Data Types 0/1, References & Borrowing 1/2, Enums & Pattern Matching 1/2, Common Collections 1/2, Modules & Paths 0/1, Control Flow 0/1
- Solid: Variables 1/1, Structs 2/2, Functions 1/1, Slices 1/1
**Implications**
- Every lesson from 0002 on must have the user typing real code in a real `cargo` project, with `cargo run` / `cargo test` as the feedback loop. Pure quiz lessons are diagnostic instruments only.
- The user explicitly asked to cover "solid" topics too — treat the diagnostic as weighting, not as a filter. Solid topics get folded into lessons as supporting material rather than skipped.
- "Solid" results here are low-confidence: 1/1 and 2/2 samples, self-graded, on recognition-style questions. Do not treat Structs/Functions/Slices as owned until seen in produced code.
- A syntax reference was the missing prerequisite — forgotten syntax was consuming the working memory needed for concepts. `reference/rust-syntax.html` now exists and should be linked from every lesson.
- Modules & Paths (0/1) is untouched by lesson 0002 beyond `mod tests`. It needs its own lesson, and the user's existing `restauran`/`learn-modules` projects are the natural material.
@@ -0,0 +1,5 @@
# A spec of signatures is not a spec; concepts must precede the project
Lesson 0003 shipped as type signatures plus a test suite and the user could not start: "you don't even have description about the specification, so i don't know what to make." Signatures answer *what the compiler will accept*, not *what the program is for*. A spec-driven lesson needs a plain-language description of the program's purpose, its data, and its commands **before** any signature appears — and the description should be written so the type choices fall out of it ("a task is an id AND a title AND a status" → struct; "a status is one of three" → enum), letting the learner derive the model instead of reading it off a contract.
The user also asked to be taught structs, enums, and packages as if from zero, having previously said they were weak on them. This corrects an earlier assumption: the diagnostic's per-topic scores (Structs 2/2, Enums 1/2) overstated real understanding, because recognition-style questions can be passed without being able to *design* with the concept. Self-reported weakness outranks diagnostic scores. Lesson 0004 was written to fill this, and the ordering rule going forward is: concept lesson (knowledge, reading, real compiler output) → project lesson (skill, typing, tests) — never a project alone for a topic the user has named as weak.
@@ -0,0 +1,15 @@
# 0003 landed: structs/enums/packages are owned; the untested file is where the gap moved
Lesson 0003 came back with 17/17 tests green and code that is genuinely idiomatic: `?` with `ok_or`, `iter().find(|task| task.id == id)` with a closure, `iter_mut()` to mutate in place, a private `Vec<Task>` behind `pub fn tasks(&self) -> &[Task]`. Structs, enums with data, `impl`, the two-crate package and cross-module `crate::` paths can be treated as **produced, not just recognised** — the first topics in this workspace to earn that. Self-reported weakness on those three is resolved.
**The gap moved to the file no test could reach.** `src/main.rs` violates the spec's error contract in three ways, all confirmed by running the crate:
- `cargo run -- fly` → message on **stdout**, exit **0** (spec: stderr, exit 1)
- `cargo run -- done 9` → `.unwrap()` panic, exit **101**
- errors formatted by hand in `main` instead of by the type
This is exactly the mission's "errors with `Result`, not `panic!`", and it went unnoticed because the 17 tests are integration tests against the library crate — by design they cannot see `main.rs`. **Implication for future specs: any behaviour stated in prose but unreachable by the test suite will not get done.** Either test it (a spec test shelling out to the binary) or make it the explicit drill of the following lesson. Lesson 0005 takes the second route, deliberately, because the fix needs traits.
**Sequencing decision.** Traits were chosen over collections/iterators or async as the next topic, because: the user's own code now has two visible trait-shaped holes (hand-built display strings, `String` errors), every backend crate they will meet (serde, axum, tokio) is trait-driven, and ch10 is the next unread chapter. Per LR-0002's ordering rule, 0005 is the concept lesson; the `String` → `TaskError` enum conversion is the project lesson (0006) and is explicitly deferred in 0005's text so the drill stays inside working memory.
**Format note that worked and should continue:** the drill in 0005 targets the crate the user already wrote, with a shell-level check per step (`echo $?`) rather than a new test file. Feedback is immediate, and the reward is their own project getting better rather than a throwaway exercise. Reference impl proved in a temp copy first: 17/17 still pass after the drill's three steps.
@@ -0,0 +1,23 @@
# Traits are produced, not just recognised — and the CLI contract finally landed
The 0005 drill was completed on the user's own `tasks` crate: `impl fmt::Display for Task` written from the signature
up, `unwrap` removed from `main.rs`, `run(args, store) -> Result<(), String>` extracted, `Command::parse(args)?`
matching on `Command` values, and `eprintln!` + `process::exit(1)` at the edge. All three shell checks pass
(`fly` → exit 1, `done 9` → exit 1, `add "buy milk" 2>/dev/null` → exit 0) and the 17 spec tests still pass.
Traits, `Display`, and `?` can be treated as owned from here — lesson 0006 assumes them instead of teaching them.
## Evidence
Verified by running the drill's own checks against `~/learn-rust/tasks`, not by reading the diff.
The `?`-on-`parse` shape was wrong on first submission (`match Command::parse(&args) { … Err(e) => Err(e) }`) and
was corrected after review, so `?` is now produced but was not the first instinct — worth one more forced repetition
in 0006, where the error type changes under five call sites at once.
## Implications
- The stderr/exit-1 contract has now been missed twice before landing (0003 spec, 0005 step 3). The pattern is
clear: a step whose check the user does not actually paste into a shell does not get done. Every future drill step
needs a one-line runnable check, and the lesson should say "run it, do not eyeball it".
- The recall quiz scored Enums 0/1 while the same session produced three working enums. Recall of *why* a construct
exists lags the ability to use it. Fix by making the compiler state the reason (0006 shows real `E0004` output for
a newly added variant) rather than asserting it in prose.
- Their own duplicated `"id not found"` string across `command.rs` and `store.rs` is the concrete motivation for
`TaskError`; using the user's own defect beats an invented example.
@@ -0,0 +1,38 @@
# A trait impl can be written correctly and still not be reached for
The 0006 drill landed 24/24 with the shipped spec unedited: seven `TaskError` variants, correct `Display`
sentences, `source()` returning the wrapped `ParseIntError` for exactly one variant, and
`impl From<ParseIntError> for TaskError`. Every trait obligation was produced from the signature up.
And the `From` impl was never called. `command.rs` still converted by hand, twice:
```rust
let id: u32 = match id.parse() { Ok(n) => n, Err(e) => return Err(TaskError::BadId(e)) };
```
That is `From::from` typed out longhand. The tests pass either way, so nothing in the feedback loop objected.
## Evidence
Read against the real crate, plus `cargo clippy`: 24 green, `tests/errors.rs` byte-identical to
`lessons/0006-errors-spec.rs`, two duplicated `match id.parse()` blocks in `command.rs`, and the same
`match … Ok(_) => ()` in `main.rs` that LR-0004 flagged after 0005 (clippy's `single_match`).
The follow-up questions were the more useful signal. All three were about the *mechanism*, not the syntax:
can two `From` impls exist for one type, where does the wrapped message go, does a developer walk `source()`
by hand every time. The syntax was owned; the model of what the syntax buys was not.
## Implications
- **A drill step needs a check that fails when the point is missed.** "Both `match` blocks collapse to
`id.parse()?`" was written in the 0006 prose and had no check beside it, so it did not happen — the same
failure mode as the stderr/exit-1 contract in LR-0004, now seen three times. 0007 gives it a grep check
(`grep -c "match id.parse" src/command.rs` → `0`) as step 0.
- **Force the impl, do not suggest it.** In 0007 the second conversion (`From<io::Error>`) cannot be hand-rolled
around without the compiler complaining, because `?` on `fs::write` is the only reasonable shape. `E0277`
does the teaching that prose could not.
- **Recall of "why" still lags production.** Same pattern as the 0005 quiz (Enums 0/1 while writing three enums).
The fix that works is showing real compiler output for the failure mode, not asserting the rule — so 0007
ships `E0119`, `E0277`, `E0046`, and `E0369`, each captured by running it.
- The user asks precise mechanism questions when given room to. Budget for them: leaving the ladder of
`?` → `From` → `source()` → `anyhow` explicit in the lesson costs less than answering it four times after.
@@ -0,0 +1,47 @@
# A green suite says nothing about the surface it cannot reach
The 0008 drill landed 46/46 with the shipped spec unedited. The library half is genuinely produced:
`tally<T, K, F>` written from its signature and used at three `T`/`K` pairs, `Store::load` as one
`collect::<Result<Vec<Task>, TaskError>>()?`, `remove_completed` on `Vec::retain`, `titles_with` as a
`filter`/`map`/`collect` chain, `count_by_priority` a one-line delegate. Nothing in `src/store.rs` or
`src/stats.rs` needed correcting beyond a commented-out loop left behind.
The CLI half of the same drill missed three of its requirements, and every one of them was invisible to
those 46 tests:
- `stats` printed `high / low / medium`, because `main.rs` looped over `[Priority::High, Low, Medium]`.
The spec asked for high, medium, low.
- `clear` printed nothing, discarding the `usize` that `remove_completed` returns. Expected
`cleared 1 completed`.
- Untested-because-unreachable, so also unfixed: the `in-progress` arm of `Status::parse`, the `Display`
line format, and `Command::parse`'s case folding.
## Evidence
`cargo test` in `tasks/`: 17 + 7 + 8 + 14 = 46 passed, 0 failed. Every one of those tests lives in
`tests/` and therefore imports the *library*; `run` lives in `src/main.rs`, which a binary crate does not
export, so no test in the workspace can call it. The three defects sit entirely inside `run`.
Measured rather than argued: six one-line mutations planted in a copy of the crate
(`lessons/0009-mutants.sh`) and the user's suite run against each. Result before 0009: `0 killed,
3 survived, 3 skipped`. The same six against a reference implementation with 13 more tests: `6 killed,
0 survived`. The suite's blindness is not a matter of degree — it is a whole surface.
## Implications
- **This is the third repeat of LR-0003's finding**, and the first time the cause is structural rather
than a missing check. 0003 lost the stderr/exit-1 contract, 0006 lost the `?`/`From` collapse, 0008 lost
the CLI output shape. A drill step whose result no test can observe does not land, however clearly the
prose states it.
- **The fix is architectural, so it is the drill.** 0009 moves `run` into `src/cli.rs` and gives it
`out: &mut impl Write`. That is not a lesson about tests bolted onto a refactor; the refactor is the only
way the tests can exist, which is exactly the book's argument for a thin `main.rs`.
- **Grade a test suite by planted bugs, not by test count.** The user has now run 46 tests and would
reasonably infer the crate is well covered. Six mutations refute that in four seconds and give a
finishing condition (`6 killed, 0 survived`) that counting cannot.
- **Ship no new spec file for 0009.** Every earlier lesson handed over `assert_eq!`s to satisfy; the skill
being built here is writing them, so the only deliverable is the mutation script. The drill names the
behaviour to pin in prose — per the rule in NOTES line 62 — and leaves the assertions to the user.
- Watch for on the next read: whether the assertions are exact (`assert_eq!` on the whole printed string)
or hedged (`assert!(out.contains("cleared"))`). The hedged form passes the mutants that matter least and
is the likeliest way this drill goes green while staying blind.
+239
View File
@@ -0,0 +1,239 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Diagnostic: Rust Book ch. 1–9</title>
<link rel="stylesheet" href="../assets/style.css" />
<script src="../assets/quiz.js" defer></script>
</head>
<body>
<h1>Diagnostic: what's still there from ch. 1–9</h1>
<p class="subtitle">Lesson 0001 · Rust Book chapters 1–9 · no new material, pure retrieval</p>
<p>Your <code>learn-*</code> folders prove you worked through variables, functions, control flow, ownership, borrowing, slices, structs, enums, modules, collections, and error handling. This lesson does not re-teach any of it. It tests what is still <em>retrievable</em> — that is the only kind of "knowing" that helps in an interview or on the job.</p>
<div class="callout">
<strong>How to take this.</strong> For each question, think of your answer <em>before</em> revealing it. Grading yourself honestly matters more than "winning" — this diagnostic decides which lessons get built next. Multiple-choice ones grade themselves; short-answer ones ask you to self-grade after reading the answer.
</div>
<div class="q" data-type="recall" data-topic="Variables">
<p class="topic">Variables — ch03.1</p>
<p class="prompt">This code:</p>
<pre><code>let x = 5;
x = 6;
println!("{x}");</code></pre>
<p class="prompt">Does it compile? If not, what's the fix?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">No — <code>cannot assign twice to immutable variable</code>. Rust variables are immutable by default; fix by declaring <code>let mut x = 5;</code>.</div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch03-01-variables-and-mutability.html">3.1 Variables and Mutability</a></p>
</div>
<div class="q" data-type="recall" data-topic="Ownership">
<p class="topic">Ownership — ch04.1</p>
<p class="prompt">This code:</p>
<pre><code>let s1 = String::from("hello");
let s2 = s1;
println!("{s1}");</code></pre>
<p class="prompt">Does it compile? Why or why not?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">No. <code>String</code> owns heap data and is not <code>Copy</code>, so <code>let s2 = s1;</code> <em>moves</em> ownership from <code>s1</code> to <code>s2</code>. Using <code>s1</code> afterward is a compile error: "borrow of moved value."</div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch04-01-what-is-ownership.html">4.1 What Is Ownership?</a></p>
</div>
<div class="q" data-type="mcq" data-topic="Structs">
<p class="topic">Structs — ch05.3</p>
<p class="prompt">Given <code>struct Rectangle { width: u32, height: u32 }</code>, where do you define a method like <code>fn area(&self) -> u32</code>?</p>
<div class="options">
<button class="opt" data-correct="true">Inside an impl block.</button>
<button class="opt" data-correct="false">Inside the struct body.</button>
<button class="opt" data-correct="false">Inside the main function.</button>
<button class="opt" data-correct="false">Inside a trait block.</button>
</div>
<div class="explain hidden">Methods are defined in a separate <code>impl Rectangle { ... }</code> block, not inside the struct definition itself.</div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch05-03-method-syntax.html">5.3 Method Syntax</a></p>
</div>
<div class="q" data-type="recall" data-topic="Error Handling">
<p class="topic">Error Handling — ch09</p>
<p class="prompt">What's the real difference between calling <code>panic!</code> and returning <code>Err(...)</code> from a function whose signature returns <code>Result</code>?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden"><code>panic!</code> unwinds and stops the program right there — unrecoverable. Returning <code>Err</code> hands the failure back to the caller, who decides what to do — recoverable. Libraries should almost always prefer <code>Result</code>.</div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch09-00-error-handling.html">9. Error Handling</a></p>
</div>
<div class="q" data-type="mcq" data-topic="Data Types">
<p class="topic">Data Types — ch03.2</p>
<p class="prompt">With no suffix and no other constraint, what type does Rust infer for <code>let x = 5;</code>?</p>
<div class="options">
<button class="opt" data-correct="true">i32</button>
<button class="opt" data-correct="false">i64</button>
<button class="opt" data-correct="false">u32</button>
<button class="opt" data-correct="false">f64</button>
</div>
<div class="explain hidden"><code>i32</code> is Rust's default integer type — fastest on most platforms, per the book.</div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch03-02-data-types.html">3.2 Data Types</a></p>
</div>
<div class="q" data-type="mcq" data-topic="References & Borrowing">
<p class="topic">References &amp; Borrowing — ch04.2</p>
<p class="prompt">Inside <code>fn calculate_length(s: &String) -> usize</code>, can you modify the <code>String</code> through <code>s</code>?</p>
<div class="options">
<button class="opt" data-correct="true">No, references are read only.</button>
<button class="opt" data-correct="false">Yes, references allow all edits.</button>
<button class="opt" data-correct="false">No, only String owns access.</button>
<button class="opt" data-correct="false">Yes, but only for numbers.</button>
</div>
<div class="explain hidden">References are immutable by default. Modifying through a reference needs <code>&mut String</code>, and the caller must pass <code>&mut s</code>.</div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html">4.2 References and Borrowing</a></p>
</div>
<div class="q" data-type="recall" data-topic="Enums & Pattern Matching">
<p class="topic">Enums &amp; Pattern Matching — ch06.2</p>
<p class="prompt">Why must a <code>match</code> expression cover every variant of an enum?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">The compiler enforces exhaustiveness — every possible value must be handled, or it's a compile error. Use <code>_</code> as a catch-all arm when you don't care about the rest.</div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch06-02-match.html">6.2 The match Control Flow Construct</a></p>
</div>
<div class="q" data-type="mcq" data-topic="Common Collections">
<p class="topic">Common Collections — ch08.1</p>
<p class="prompt">Which type owns and can grow a heap-allocated list of values of the same type?</p>
<div class="options">
<button class="opt" data-correct="true">Vec, a growable vector.</button>
<button class="opt" data-correct="false">Array, a fixed list.</button>
<button class="opt" data-correct="false">Slice, a borrowed view.</button>
<button class="opt" data-correct="false">Tuple, a fixed grouping.</button>
</div>
<div class="explain hidden"><code>Vec&lt;T&gt;</code> owns its elements on the heap and grows/shrinks at runtime. Arrays are fixed-size and stack-allocated; slices only borrow.</div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch08-01-vectors.html">8.1 Storing Lists of Values with Vectors</a></p>
</div>
<div class="q" data-type="recall" data-topic="Functions">
<p class="topic">Functions — ch03.3</p>
<p class="prompt">Does the last expression in a function body need a <code>return</code> keyword to become the return value? What one character must you drop from that line for it to count as an expression, not a statement?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">No <code>return</code> needed. Drop the trailing semicolon — a line ending in <code>;</code> is a statement (no value); without it, it's an expression whose value becomes the function's return value.</div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch03-03-how-functions-work.html">3.3 Functions</a></p>
</div>
<div class="q" data-type="recall" data-topic="Slices">
<p class="topic">Slices — ch04.3</p>
<pre><code>let s = String::from("hello world");
let hello = &s[0..5];</code></pre>
<p class="prompt">What Rust type is <code>hello</code>? A slice stores two things internally — what are they?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden"><code>hello</code> is a <code>&str</code> (string slice). Internally a slice is a pointer into the buffer plus a length — it never owns the data.</div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch04-03-slices.html">4.3 The Slice Type</a></p>
</div>
<div class="q" data-type="recall" data-topic="Modules & Paths">
<p class="topic">Modules &amp; Paths — ch07</p>
<p class="prompt">In your <code>restauran</code> project you called <code>crate::front_of_house::hosting::add_to_waitlist()</code>. What does the <code>pub</code> keyword do to <code>hosting</code> and to <code>add_to_waitlist</code>? What breaks if you remove it?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden"><code>pub</code> makes an item visible outside its parent module. Everything is private by default, so removing <code>pub</code> from either the module or the function makes it inaccessible from <code>eat_at_restaurant</code>'s call site — a compile error ("module/function is private").</div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html">7. Packages, Crates, and Modules</a></p>
</div>
<div class="q" data-type="mcq" data-topic="Control Flow">
<p class="topic">Control Flow — ch03.5</p>
<p class="prompt">Can <code>if</code> sit on the right side of a <code>let</code> to assign a value, like a ternary?</p>
<div class="options">
<button class="opt" data-correct="true">Yes, if arms must match.</button>
<button class="opt" data-correct="false">No, if never returns values.</button>
<button class="opt" data-correct="false">Yes, but types can differ.</button>
<button class="opt" data-correct="false">No, only loops return values.</button>
</div>
<div class="explain hidden">Yes — <code>let x = if cond { 5 } else { 6 };</code> works, but every arm must produce the <em>same</em> type, since <code>x</code> needs one fixed type at compile time.</div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch03-05-control-flow.html">3.5 Control Flow</a></p>
</div>
<div class="q" data-type="recall" data-topic="Ownership">
<p class="topic">Ownership — ch04.1</p>
<p class="prompt">Name the trait that lets simple stack-only types like <code>i32</code> get duplicated instead of moved on assignment.</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">The <code>Copy</code> trait. Types with a known, fixed size that live entirely on the stack (integers, bools, chars, tuples of Copy types, …) can implement it, so assignment copies instead of moving.</div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch04-01-what-is-ownership.html">4.1 What Is Ownership? — Stack-Only Data: Copy</a></p>
</div>
<div class="q" data-type="recall" data-topic="Structs">
<p class="topic">Structs — ch05.3</p>
<p class="prompt">Why do most struct methods take <code>&self</code> (borrowed) instead of <code>self</code> (owned)?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Taking <code>self</code> by value would move the instance into the method, consuming it — the caller couldn't use it afterward. <code>&self</code> borrows it, so the instance survives the call.</div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch05-03-method-syntax.html">5.3 Method Syntax</a></p>
</div>
<div class="q" data-type="recall" data-topic="Common Collections">
<p class="topic">Common Collections — ch08.3</p>
<p class="prompt">From your own <code>learn-challenges</code> code:</p>
<pre><code>let x = count.entry(v).or_insert(0);
*x += 1;</code></pre>
<p class="prompt">What does <code>.entry(v)</code> return, and what does <code>.or_insert(0)</code> do whether <code>v</code> is a new key or an existing one?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden"><code>.entry(v)</code> returns an <code>Entry</code> enum for that key. <code>.or_insert(0)</code> inserts <code>0</code> only if the key is missing, and in both cases returns a mutable reference to the value — so <code>*x += 1</code> works whether it's brand-new or already there.</div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html">8.3 Storing Keys with Associated Values in Hash Maps</a></p>
</div>
<div class="q" data-type="mcq" data-topic="Enums & Pattern Matching">
<p class="topic">Enums &amp; Pattern Matching — ch06.3</p>
<p class="prompt">What's the shorthand for a <code>match</code> that only cares about one pattern and ignores every other case?</p>
<div class="options">
<button class="opt" data-correct="true">Use an if let.</button>
<button class="opt" data-correct="false">Use a for loop.</button>
<button class="opt" data-correct="false">Use a while let.</button>
<button class="opt" data-correct="false">Use the unwrap method.</button>
</div>
<div class="explain hidden"><code>if let Some(x) = opt { ... }</code> handles one pattern concisely without a full exhaustive <code>match</code>. <code>while let</code> is the loop version, not this case.</div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch06-03-if-let.html">6.3 Concise Control Flow with if let</a></p>
</div>
<div class="q" data-type="mcq" data-topic="Error Handling">
<p class="topic">Error Handling — ch09.2</p>
<p class="prompt">From your own <code>learn-panic</code> code:</p>
<pre><code>let mut username_file = File::open("hello.txt")?;</code></pre>
<p class="prompt">What does the <code>?</code> do when <code>File::open</code> returns <code>Err</code>?</p>
<div class="options">
<button class="opt" data-correct="true">It returns Err early.</button>
<button class="opt" data-correct="false">It panics the program.</button>
<button class="opt" data-correct="false">It retries the call.</button>
<button class="opt" data-correct="false">It ignores the error.</button>
</div>
<div class="explain hidden"><code>?</code> propagates the error immediately: the enclosing function returns that <code>Err</code> right away instead of continuing — the function's return type must be a compatible <code>Result</code>.</div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch09-02-recoverable-errors-with-result.html">9.2 Recoverable Errors with Result</a></p>
</div>
<div class="q" data-type="recall" data-topic="References & Borrowing">
<p class="topic">References &amp; Borrowing — ch04.2</p>
<p class="prompt">True or false: at any single point, Rust allows either one mutable reference OR any number of immutable references to the same data, never a mix. Why does this rule exist?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">True. It's enforced at compile time by the borrow checker, and it exists to rule out data races and "reader sees a half-written value" bugs without needing a garbage collector or runtime lock.</div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html">4.2 References and Borrowing</a></p>
</div>
<div id="summary">
<h2>Results</h2>
<div id="summary-body">No questions answered yet.</div>
<p id="summary-total"></p>
<button id="report-btn" disabled>Copy report</button>
<pre id="report-output" class="hidden"></pre>
<p class="cite">Copying the report and pasting it into the chat is the fastest way to tell the agent what to build next.</p>
</div>
<footer>
<p><strong>Primary source:</strong> <a href="https://doc.rust-lang.org/stable/book/">The Rust Programming Language</a>, chapters 1–9, by Klabnik, Nichols &amp; Krycho.</p>
<p><strong>Next:</strong> <a href="0002-write-a-cli-from-blank.html">0002 — Write a CLI from a blank file</a> · <strong>Reference:</strong> <a href="../reference/rust-syntax.html">Rust syntax reference (ch. 1–9)</a></p>
<p>Stuck on anything above, or a question phrased badly? Ask the agent — that's what it's there for. Paste your weak topics from the report and the next lesson will target exactly those.</p>
</footer>
</body>
</html>
+203
View File
@@ -0,0 +1,203 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Write a CLI from a blank file</title>
<link rel="stylesheet" href="../assets/style.css" />
<script src="../assets/quiz.js" defer></script>
</head>
<body>
<h1>Write a CLI from a blank file</h1>
<p class="subtitle">Lesson 0002 · production, not recognition · ~25 minutes</p>
<p>Your <a href="0001-diagnostic-ch1-9.html">diagnostic</a> said something more useful than the score: you recognise Rust but can't <em>produce</em> it. That's a different skill, and re-reading the book does not fix it. Only typing does.</p>
<p>So this lesson has no reading section. You will type a working command-line tool from an empty file, running it after every stage. By the end you'll have touched — <em>in your own fingers</em> — types, functions, control flow, <code>match</code>, <code>Result</code>, <code>Vec</code>, and borrowing. Six of your eight weak topics, in one 45-line program.</p>
<div class="callout">
<strong>Keep <a href="../reference/rust-syntax.html">the syntax reference</a> open in another tab.</strong> Looking up syntax is not cheating — a working memory clogged with "how do I write a for loop again" has nothing left for the actual concept. Look it up, type it, move on.
<br /><br />
<strong>Rule for this lesson: type every line by hand.</strong> Do not copy-paste. The muscle memory <em>is</em> the lesson.
</div>
<h2>What you're building</h2>
<p>A grade tool. You pass it scores; it prints a letter for each, skips garbage input, and prints the average:</p>
<pre><code>$ cargo run -- 95 83 71 abc 40
95 -> A
83 -> B
71 -> C
abc -> not a number, skipped
40 -> F
average: 72</code></pre>
<h2>Stage 0 — new project</h2>
<pre><code>cd ~/learn-rust
cargo new grader
cd grader</code></pre>
<p>Open <code>src/main.rs</code>. Cargo wrote a hello-world in it. Delete all of it — blank file.</p>
<h2>Stage 1 — read the arguments</h2>
<p>Type this:</p>
<pre><code>use std::env;
fn main() {
let args: Vec&lt;String&gt; = env::args().collect();
println!("{args:?}");
}</code></pre>
<p>Run it: <code>cargo run -- 95 83</code></p>
<div class="q" data-type="recall" data-topic="Data Types">
<p class="topic">Predict before you run</p>
<p class="prompt">How many items will be in <code>args</code>, and what is the first one?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Three. <code>args[0]</code> is the path to your own binary — the program name always comes first. Your real input starts at <code>args[1]</code>. Output looks like <code>["target/debug/grader", "95", "83"]</code>.</div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
</div>
<p>Two things to notice in what you just typed: the type annotation <code>Vec&lt;String&gt;</code> is <em>required</em> here, because <code>.collect()</code> can build many different collections and needs to be told which. And <code>{args:?}</code> uses <code>Debug</code> formatting — <code>{}</code> alone would not compile, because a <code>Vec</code> has no <code>Display</code> impl.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch12-01-accepting-command-line-arguments.html">12.1 Accepting Command Line Arguments</a></p>
<h2>Stage 2 — guard against no input</h2>
<p>Replace the <code>println!</code> with:</p>
<pre><code> if args.len() &lt; 2 {
println!("usage: cargo run -- &lt;score&gt; [more scores...]");
return;
}</code></pre>
<p>Run <code>cargo run</code> with no arguments — you should get the usage line. This is your first <em>trust boundary</em>: never assume input exists. Backend code lives or dies on this habit.</p>
<div class="q" data-type="mcq" data-topic="Data Types">
<p class="topic">Checkpoint</p>
<p class="prompt">What type does <code>args.len()</code> return?</p>
<div class="options">
<button class="opt" data-correct="true">usize</button>
<button class="opt" data-correct="false">u32</button>
<button class="opt" data-correct="false">i32</button>
<button class="opt" data-correct="false">u64</button>
</div>
<div class="explain hidden"><code>usize</code> — the pointer-sized unsigned integer. Every length and index in Rust is <code>usize</code>, which is why mixing it with <code>u32</code> needs an explicit <code>as</code> cast. You'll hit exactly that in Stage 5.</div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch03-02-data-types.html">3.2 Data Types</a></p>
</div>
<h2>Stage 3 — parse each argument</h2>
<p>Below the guard, add:</p>
<pre><code> for arg in &amp;args[1..] {
match arg.parse::&lt;u32&gt;() {
Ok(score) =&gt; println!("{score} -&gt; ok"),
Err(_) =&gt; println!("{arg} -&gt; not a number, skipped"),
}
}</code></pre>
<p>Run: <code>cargo run -- 95 abc 40</code>. You should see two <code>ok</code> lines and one skip.</p>
<p>Three weak topics just collided in five lines, so slow down here:</p>
<ul>
<li><code>&amp;args[1..]</code> is a <strong>slice</strong> — a borrowed view of the vector from index 1 onward. You did not copy the arguments and you did not take ownership of them.</li>
<li><code>.parse()</code> returns a <strong><code>Result</code></strong>, because parsing can fail. <code>::&lt;u32&gt;</code> is the turbofish telling it which type to aim for.</li>
<li><code>match</code> forces you to handle <em>both</em> arms. This is the whole point of <code>Result</code>: the compiler will not let you forget the failure case. Compare this to your <code>learn-panic</code> project, where you reached for <code>panic!</code> — here, bad input just gets skipped and the program carries on. That is the difference between a script and a tool.</li>
</ul>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch09-02-recoverable-errors-with-result.html">9.2 Recoverable Errors with Result</a></p>
<h2>Stage 4 — the grading function</h2>
<p>Below <code>main</code>'s closing brace, add a new function:</p>
<pre><code>fn grade(score: u32) -&gt; String {
match score {
90..=100 =&gt; "A".to_string(),
80..=89 =&gt; "B".to_string(),
70..=79 =&gt; "C".to_string(),
_ =&gt; "F".to_string(),
}
}</code></pre>
<p>Now use it — change the <code>Ok</code> arm inside <code>main</code> to:</p>
<pre><code> Ok(score) =&gt; println!("{score} -&gt; {}", grade(score)),</code></pre>
<p>Run: <code>cargo run -- 95 83 71 40</code> → <code>A B C F</code>.</p>
<div class="q" data-type="recall" data-topic="Enums &amp; Pattern Matching">
<p class="topic">Checkpoint</p>
<p class="prompt">Delete the <code>_ =&gt; "F".to_string(),</code> arm and run <code>cargo build</code>. What does the compiler say, and why?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Something like <code>non-exhaustive patterns: `0_u32..=69_u32` and `101_u32..=u32::MAX` not covered</code>. <code>match</code> must handle every possible value of the type — and <code>u32</code> includes 0–69 and everything above 100. The <code>_</code> arm is what makes it exhaustive. <strong>Put the arm back before continuing.</strong></div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch06-02-match.html">6.2 The match Control Flow Construct</a></p>
</div>
<p>Note the return type: <code>String</code>, not <code>&amp;str</code>. The function builds a value and hands ownership to its caller. Returning a borrowed <code>&amp;str</code> here would force you to answer "borrowed from <em>what</em>, and does that thing outlive the caller?" — which is lifetimes, chapter 10, and deliberately not today's problem.</p>
<h2>Stage 5 — collect and average</h2>
<p>Above the <code>for</code> loop, add a vector to accumulate into:</p>
<pre><code> let mut scores: Vec&lt;u32&gt; = Vec::new();</code></pre>
<p>Change the <code>Ok</code> arm to a block, so it can do two things:</p>
<pre><code> Ok(score) =&gt; {
println!("{score} -&gt; {}", grade(score));
scores.push(score);
}</code></pre>
<p>After the loop, add:</p>
<pre><code> if scores.is_empty() {
println!("no valid scores");
} else {
println!("average: {}", average(&amp;scores));
}</code></pre>
<p>And a second function at the bottom of the file:</p>
<pre><code>fn average(scores: &amp;[u32]) -&gt; u32 {
let mut total = 0;
for score in scores {
total += score;
}
total / scores.len() as u32
}</code></pre>
<p>Run: <code>cargo run -- 95 83 71 abc 40</code> → you should get exactly the output from the top of this page, ending in <code>average: 72</code>.</p>
<div class="q" data-type="recall" data-topic="Ownership">
<p class="topic">Checkpoint — the important one</p>
<p class="prompt">Change the call to <code>average(scores)</code> and the parameter to <code>scores: Vec&lt;u32&gt;</code>, then try to print <code>scores.len()</code> on the line <em>after</em> that call. What happens, and why does the <code>&amp;</code> version not have this problem?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">It fails to compile: <code>borrow of moved value: `scores`</code>. Passing a <code>Vec</code> by value <em>moves</em> ownership into the function, which then drops it at the end — so <code>main</code> has nothing left to read. Passing <code>&amp;scores</code> only <em>borrows</em> it: the function reads it, the borrow ends when the function returns, and <code>main</code> still owns it. This is why function signatures in real Rust take <code>&amp;</code> by default and take ownership only on purpose. <strong>Put the <code>&amp;</code> version back.</strong></div>
<div class="grade hidden"><button data-grade="hit">Got it</button><button data-grade="miss">Missed it</button></div>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html">4.2 References and Borrowing</a></p>
</div>
<p>Two details worth burning in:</p>
<ul>
<li>The parameter is <code>&amp;[u32]</code>, not <code>&amp;Vec&lt;u32&gt;</code>. A slice accepts a <code>Vec</code>, an array, or part of either — strictly more useful, same speed. Idiomatic Rust prefers <code>&amp;[T]</code> in every read-only signature.</li>
<li><code>scores.len() as u32</code> needs the cast because <code>len()</code> is <code>usize</code> and <code>total</code> is <code>u32</code>. Rust does no implicit numeric conversion, ever.</li>
</ul>
<h2>Stage 6 — a feedback loop that outlives you</h2>
<p>At the very bottom of the file:</p>
<pre><code>#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grades_map_to_letters() {
assert_eq!(grade(95), "A");
assert_eq!(grade(80), "B");
assert_eq!(grade(42), "F");
}
#[test]
fn average_of_three() {
assert_eq!(average(&amp;[90, 80, 70]), 80);
}
}</code></pre>
<p>Run <code>cargo test</code>. Expect <code>2 passed</code>.</p>
<p>Now break something on purpose — change <code>80..=89</code> to <code>81..=89</code> and run <code>cargo test</code> again. One test fails and tells you exactly what it expected. That loop, not the compiler, is what you'll lean on when programs get big enough that you can't hold them in your head. Change it back.</p>
<p>You just met three things at once: <code>#[cfg(test)]</code> (compile this module only during tests), <code>mod tests</code> (a module — the same feature as your <code>restauran</code> project), and <code>use super::*</code> (pull in everything from the parent module, which is how the test sees <code>grade</code>). Testing proper is chapter 11; today it's just the loop.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html">11.1 How to Write Tests</a></p>
<h2>Your win</h2>
<p>Forty-five lines, typed by hand, that read real input, reject bad input without crashing, and prove themselves with tests. That is a smaller program than your <code>learn-challenges</code> exercise — but you wrote this one from a blank file, which is the thing you said you'd lost.</p>
<div id="summary">
<h2>Checkpoint results</h2>
<div id="summary-body">No checkpoints answered yet.</div>
<p id="summary-total"></p>
<button id="report-btn" disabled>Copy report</button>
<pre id="report-output" class="hidden"></pre>
</div>
<h2>Stretch task (optional, do it before the next lesson)</h2>
<p>Without looking at this page: add a <code>highest</code> function that returns the top score, and print it. You'll need <code>Option</code>, because an empty slice has no maximum. If you get stuck on the signature, that's a real question — ask the agent.</p>
<footer>
<p><strong>Primary source:</strong> <a href="https://doc.rust-lang.org/stable/book/ch12-00-an-io-project.html">The Rust Book, ch. 12 — An I/O Project: Building a Command Line Program</a>. It builds a bigger version of exactly what you just wrote, and it's the best next read for a backend/CLI goal.</p>
<p><strong>Reference:</strong> <a href="../reference/rust-syntax.html">Rust syntax reference (ch. 1–9)</a> · <strong>Previous:</strong> <a href="0001-diagnostic-ch1-9.html">0001 Diagnostic</a></p>
<p>Stuck, or got a compiler error this page didn't predict? Paste it to the agent — reading compiler errors fluently is itself a skill worth a lesson, and that's a good excuse to start one.</p>
</footer>
</body>
</html>
+418
View File
@@ -0,0 +1,418 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Project: task CLI (spec-driven)</title>
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body>
<h1>Project: a task CLI</h1>
<p class="subtitle">Lesson 0003 · spec-driven · no walkthrough · 1–3 hours</p>
<p>You asked for a specification instead of a guided build. This is that. There are no numbered stages here and no code to copy. You get four things: a <strong>plain-language description</strong> of the program, a <strong>contract</strong> the code must satisfy, a <strong>test suite</strong> that checks it, and a <strong>syntax crib</strong> written in a different domain so it shows you the shape without handing you the answer.</p>
<p>Target: packages, structs, and enums — the three you named. Modules &amp; Paths was <a href="0001-diagnostic-ch1-9.html">0/1 on your diagnostic</a>, so this project is deliberately split across four files that must see each other.</p>
<div class="callout">
<strong>Read <a href="0004-structs-enums-packages.html">lesson 0004</a> first.</strong> It teaches structs, enums, and packages from zero, with real compiler output. This page assumes you have read it and does not re-explain the concepts.
<br /><br />
<strong>The feedback loop is <code>cargo test</code>.</strong> 17 tests define done. They will all fail at first — that is correct. Make them go green one at a time.
<br /><br />
<strong>Rules.</strong> Look up syntax as often as you like — <a href="../reference/rust-syntax.html">your reference sheet</a> and the crib below exist for that. Do not read a hint until you have been stuck on that specific thing for ten minutes. Being stuck is the lesson; a hint spent too early is a lesson wasted.
</div>
<h2>What you are building, in words</h2>
<p>A command-line to-do list. You run it with a command and some arguments, it does one thing, prints one line, and exits. It holds tasks in memory for the duration of a single run — there is no file, no database, no interactive prompt, and no loop. One run, one command, done.</p>
<p>A <strong>task</strong> is four pieces of information:</p>
<ul>
<li>an <strong>id</strong> — a number the program assigns, so you can refer to the task later</li>
<li>a <strong>title</strong> — the text you typed, e.g. <code>"buy milk"</code></li>
<li>a <strong>status</strong> — where it is up to: not started, being worked on, or finished</li>
<li>a <strong>priority</strong> — how important: low, medium, or high</li>
</ul>
<p>Read that list again with <a href="0004-structs-enums-packages.html">0004</a> in mind, because it is telling you the types. A task is an id <em>and</em> a title <em>and</em> a status <em>and</em> a priority — four things at once, so it is a <strong>struct</strong>. A status is not-started <em>or</em> in-progress <em>or</em> done — one of three, so it is an <strong>enum</strong>. Priority likewise. That is the whole modelling decision, and it is why this project targets the topics it does.</p>
<p>The program supports <strong>four commands</strong>:</p>
<table>
<tr><th align="left">Command</th><th align="left">What it does</th></tr>
<tr><td><code>add &lt;title&gt; [priority]</code></td><td>Creates a task. Priority is optional and defaults to medium. Prints the new id.</td></tr>
<tr><td><code>list</code></td><td>Prints every task, one per line, in the order they were added.</td></tr>
<tr><td><code>done &lt;id&gt;</code></td><td>Marks that task finished.</td></tr>
<tr><td><code>remove &lt;id&gt;</code></td><td>Deletes that task.</td></tr>
</table>
<p>Anything else — a command that does not exist, a missing argument, an id that is not a number, an id with no matching task — prints an error to stderr and exits with status 1.</p>
<p>Four commands, each needing different information: <code>add</code> needs a title and a priority, <code>done</code> and <code>remove</code> need an id, <code>list</code> needs nothing at all. That is an enum whose variants carry different data — the Part 2 idea from 0004, applied. Once the user's input is a <code>Command</code> value, every impossible combination is gone: there is no way to hold a <code>done</code> with no id, or an <code>add</code> with an id and no title.</p>
<p>The work splits into three jobs, which is where the four files come from:</p>
<ol>
<li><strong>Describing a task</strong> — the types and their small helpers. This is <code>task.rs</code>. It knows nothing about command lines.</li>
<li><strong>Understanding what the user typed</strong> — turning a list of strings into one of four commands, or into an error explaining why not. This is <code>command.rs</code>. It knows nothing about storage.</li>
<li><strong>Holding the tasks and changing them</strong> — the list, the id counter, add/complete/remove/find. This is <code>store.rs</code>. It knows nothing about command lines either.</li>
</ol>
<p>Then <code>main.rs</code> is the thin layer that connects them: read the arguments, ask <code>command.rs</code> what they mean, tell <code>store.rs</code> to do it, print the result or the error. It contains no logic of its own worth testing — which is exactly why the tests can live entirely against the other three.</p>
<p>That separation is the real subject of this project. Each of the three has one job, does not know about the others' jobs, and can be tested on its own. It is the same shape as a backend service: request parsing, domain types, storage, and a thin handler wiring them together.</p>
<h2>What it looks like when it runs</h2>
<pre><code>$ cargo run -- add "buy milk"
added task 1
$ cargo run -- add "ship the feature" high
added task 2
$ cargo run -- list
1 [todo] buy milk (medium)
2 [todo] ship the feature (high)
$ cargo run -- done 1
completed 1
$ cargo run -- remove 9
error: no task with id 9 # and exits with status 1
$ cargo run -- fly
error: unknown command: fly # and exits with status 1</code></pre>
<p>Tasks live in memory only. Each run starts empty — persistence is a stretch goal, not part of the spec.</p>
<h2>Setup</h2>
<pre><code>cd ~/learn-rust
cargo new tasks --lib
cd tasks
mkdir tests
cp ../lessons/0003-tasks-spec.rs tests/spec.rs
cargo test # 3 unresolved-import errors. Good. Start here.</code></pre>
<p>That first failure is the right one to see:</p>
<pre><code>error[E0432]: unresolved import `tasks::command`
error[E0432]: unresolved import `tasks::store`
error[E0432]: unresolved import `tasks::task`</code></pre>
<p>The tests are asking for three modules that do not exist yet. Your first job is to make those three names resolve — empty files and one line each in <code>lib.rs</code> is enough to change the error.</p>
<p>The package <strong>must</strong> be named <code>tasks</code> — the tests import it by that name.</p>
<h2>Required file layout</h2>
<pre><code>tasks/
├── Cargo.toml
├── src/
│ ├── lib.rs ← library crate root: declares the modules
│ ├── task.rs ← Task struct, Status enum, Priority enum
│ ├── command.rs ← Command enum + parsing
│ ├── store.rs ← Store struct, owns the task list
│ └── main.rs ← binary crate: args in, text out, exit codes
└── tests/
└── spec.rs ← the tests (do not edit)</code></pre>
<p>This layout is the point of the exercise, so here is <em>why</em> rather than <em>how</em>:</p>
<p><code>cargo new tasks --lib</code> gives you <code>src/lib.rs</code> and no <code>src/main.rs</code>. You create <code>main.rs</code> yourself. You do <strong>not</strong> add anything to <code>Cargo.toml</code> — no <code>[lib]</code>, no <code>[[bin]]</code>. Cargo finds both by filename convention.</p>
<p>You now have one <strong>package</strong> containing two <strong>crates</strong>:</p>
<ul>
<li><code>src/lib.rs</code> → a library crate named <code>tasks</code>. All the logic lives here.</li>
<li><code>src/main.rs</code> → a binary crate. It is a <em>consumer</em> of the library, exactly like an outside user.</li>
</ul>
<p>So <code>main.rs</code> reaches your code the same way the tests do — <code>use tasks::store::Store;</code> — not with <code>crate::</code>. Inside the library, modules refer to each other with <code>crate::</code>. Getting this wrong is the most common stumble in this project; when a path will not resolve, first ask <em>which crate am I in right now?</em></p>
<p>Integration tests in <code>tests/</code> can only reach <code>pub</code> items through the library crate. That is what makes the visibility rules bite for real, instead of in theory.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch07-01-packages-and-crates.html">7.1 Packages and Crates</a> · <a href="https://doc.rust-lang.org/stable/book/ch07-05-separating-modules-into-different-files.html">7.5 Separating Modules into Different Files</a> · <a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 Test Organization</a></p>
<h2>The contract</h2>
<p>These signatures are fixed — the tests call exactly these. Everything else is yours: field order, private helpers, how you search a <code>Vec</code>, how you word error messages.</p>
<h3><code>task.rs</code></h3>
<pre><code>pub enum Status { Todo, InProgress, Done }
pub enum Priority { Low, Medium, High }
pub struct Task {
pub id: u32,
pub title: String,
pub status: Status,
pub priority: Priority,
}
impl Status { pub fn label(&self) -&gt; &str }
impl Priority { pub fn label(&self) -&gt; &str }
impl Priority { pub fn parse(text: &str) -&gt; Option&lt;Priority&gt; }
impl Task { pub fn new(id: u32, title: &str, priority: Priority) -&gt; Task }</code></pre>
<ul>
<li><code>label</code> returns <code>"todo"</code>, <code>"in-progress"</code>, <code>"done"</code>, <code>"low"</code>, <code>"medium"</code>, <code>"high"</code>.</li>
<li><code>Priority::parse</code> accepts exactly <code>"low"</code>, <code>"medium"</code>, <code>"high"</code>. Anything else is <code>None</code>. Note it returns <code>Option</code>, not <code>Result</code> — there is no reason to report beyond "that is not a priority".</li>
<li><code>Task::new</code> always starts a task at <code>Status::Todo</code>.</li>
<li><code>InProgress</code> is never produced by any command. Define it anyway — it is there so your <code>match</code> arms have a third case to handle.</li>
</ul>
<h3><code>command.rs</code></h3>
<pre><code>pub enum Command {
Add { title: String, priority: Priority },
List,
Done { id: u32 },
Remove { id: u32 },
}
impl Command {
pub fn parse(args: &[String]) -&gt; Result&lt;Command, String&gt;
}</code></pre>
<p><code>parse</code> receives arguments <strong>with the program name already removed</strong>. The error type is <code>String</code> — a real project would define an error enum, but that needs traits, so not today.</p>
<table>
<tr><th align="left">Input</th><th align="left">Result</th></tr>
<tr><td><code>["add", "buy milk"]</code></td><td><code>Add { title: "buy milk", priority: Medium }</code></td></tr>
<tr><td><code>["add", "ship it", "high"]</code></td><td><code>Add { title: "ship it", priority: High }</code></td></tr>
<tr><td><code>["list"]</code></td><td><code>List</code></td></tr>
<tr><td><code>["done", "7"]</code></td><td><code>Done { id: 7 }</code></td></tr>
<tr><td><code>["remove", "12"]</code></td><td><code>Remove { id: 12 }</code></td></tr>
<tr><td><code>[]</code></td><td><code>Err</code> — no command given</td></tr>
<tr><td><code>["fly"]</code></td><td><code>Err</code> — unknown command</td></tr>
<tr><td><code>["add"]</code></td><td><code>Err</code> — add needs a title</td></tr>
<tr><td><code>["add", "x", "urgent"]</code></td><td><code>Err</code> — not a priority word</td></tr>
<tr><td><code>["done"]</code></td><td><code>Err</code> — needs an id</td></tr>
<tr><td><code>["done", "abc"]</code></td><td><code>Err</code> — id must be a number</td></tr>
<tr><td><code>["remove", "-1"]</code></td><td><code>Err</code> — negative is not a <code>u32</code></td></tr>
</table>
<p>The last row needs no special handling. Think about why before you write anything for it.</p>
<p>Titles are a single argument. <code>add buy milk</code> without quotes is not your problem — the shell splits it, and <code>"milk"</code> is simply not a priority word, so it is an error. That is acceptable behaviour.</p>
<h3><code>store.rs</code></h3>
<pre><code>pub struct Store { /* private fields — your choice */ }
impl Store {
pub fn new() -&gt; Store
pub fn add(&mut self, title: &str, priority: Priority) -&gt; u32 // -&gt; new id
pub fn complete(&mut self, id: u32) -&gt; Result&lt;(), String&gt;
pub fn remove(&mut self, id: u32) -&gt; Result&lt;(), String&gt;
pub fn tasks(&self) -&gt; &[Task]
pub fn find(&self, id: u32) -&gt; Option&lt;&Task&gt;
}</code></pre>
<p>Rules the tests enforce:</p>
<ul>
<li>Ids start at <strong>1</strong> and increase by one on every <code>add</code>.</li>
<li><strong>Ids are never reused</strong>, even after a removal. Add, remove, add again → the second id must differ. This decides how you store the counter.</li>
<li><code>tasks()</code> returns them in insertion order.</li>
<li><code>complete</code> and <code>remove</code> on an unknown id return <code>Err</code>. The message is yours; the tests only check <code>is_err()</code>.</li>
</ul>
<p><strong><code>Store</code>'s fields must be private.</strong> Note that <code>pub struct</code> does <em>not</em> make fields public — each field needs its own <code>pub</code>, and here you want none of them. Everything outside goes through the methods. This is why <code>tasks()</code> exists and why it hands back <code>&[Task]</code> rather than the <code>Vec</code> itself: callers may read the list, and cannot touch your id counter or reorder anything. Encapsulation, enforced by the compiler.</p>
<h3><code>main.rs</code></h3>
<p>Not covered by the tests — this part is yours to judge. It must:</p>
<ul>
<li>Collect arguments and hand <em>everything after the program name</em> to <code>Command::parse</code>.</li>
<li><code>match</code> the resulting <code>Command</code> and call the right <code>Store</code> method.</li>
<li>Print the success text to stdout, matching the session at the top of this page.</li>
<li>Print <code>error: ...</code> to <strong>stderr</strong> and exit with status <strong>1</strong> on any failure.</li>
</ul>
<p>Since state is in memory, <code>list</code> after a fresh <code>add</code> shows only that run. That is expected. Seed a task or two in <code>main</code> if you want <code>list</code> to show something.</p>
<h2>Derives you will need</h2>
<p>The tests use <code>assert_eq!</code> on your types and print them on failure. That requires two abilities you met in our <a href="../reference/rust-syntax.html">Debug vs Display</a> discussion:</p>
<pre><code>#[derive(Debug, PartialEq)]</code></pre>
<p><code>PartialEq</code> gives <code>==</code>. <code>Debug</code> gives <code>{:?}</code> for the failure output. Add <code>Clone</code> and <code>Copy</code> where it makes life easier — think about which of these types are small enough to copy, and which own heap data and therefore cannot be <code>Copy</code>.</p>
<p>If you forget these, the compiler tells you exactly which trait is missing on which type. That error is the lesson; read it rather than pattern-matching against this paragraph.</p>
<h2>Syntax crib</h2>
<p>A vending machine, not a task list. Same shapes, different domain — you cannot paste any of this.</p>
<h3>Modules across files</h3>
<pre><code>// src/lib.rs — the library crate root
pub mod coin;
pub mod machine;
// src/coin.rs
pub enum Coin { Nickel, Dime }
// src/machine.rs — one module reaching another, inside the same crate
use crate::coin::Coin;
// src/main.rs — a DIFFERENT crate, so use the package name
use vending::machine::Machine;</code></pre>
<h3>Enum with unit variants, and a method that matches on itself</h3>
<pre><code>#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Coin { Nickel, Dime, Quarter }
impl Coin {
pub fn value(&self) -&gt; u32 {
match self {
Coin::Nickel =&gt; 5,
Coin::Dime =&gt; 10,
Coin::Quarter =&gt; 25,
}
}
pub fn parse(text: &str) -&gt; Option&lt;Coin&gt; {
match text {
"nickel" =&gt; Some(Coin::Nickel),
"dime" =&gt; Some(Coin::Dime),
_ =&gt; None,
}
}
}</code></pre>
<h3>Enum whose variants carry data, and matching it apart</h3>
<pre><code>#[derive(Debug, PartialEq)]
pub enum Event {
Insert { coin: Coin, count: u32 }, // named fields
Select { slot: u32 },
Refund, // no data
}
// building one
let event = Event::Insert { coin: Coin::Dime, count: 2 };
// taking one apart — the names bind as variables
match event {
Event::Insert { coin, count } =&gt; println!("{count} x {}", coin.value()),
Event::Select { slot } =&gt; println!("slot {slot}"),
Event::Refund =&gt; println!("refunding"),
}</code></pre>
<h3>Struct with private fields, and its impl block</h3>
<pre><code>pub struct Machine {
credit: u32, // private: no `pub`
coins: Vec&lt;Coin&gt;,
}
impl Machine {
pub fn new() -&gt; Machine { // associated fn: no self, called Machine::new()
Machine { credit: 0, coins: Vec::new() }
}
pub fn insert(&mut self, coin: Coin) -&gt; u32 { // &mut self: may change it
self.credit += coin.value();
self.coins.push(coin);
self.credit
}
pub fn credit(&self) -&gt; u32 { self.credit } // &self: read only
pub fn coins(&self) -&gt; &[Coin] { &self.coins } // borrowed view, not the Vec
}</code></pre>
<h3>Reading optional arguments</h3>
<pre><code>let words: Vec&lt;String&gt; = std::env::args().collect();
words.first() // Option&lt;&String&gt; — the first, if any
words.get(2) // Option&lt;&String&gt; — index 2, if any
&words[1..] // slice of everything after the first
// Option -&gt; Result, so ? can carry it
let name = words.first().ok_or("nothing to do")?;
// match on a &String needs &str
match name.as_str() {
"insert" =&gt; { }
other =&gt; return Err(format!("unknown: {other}")),
}</code></pre>
<h3>Turning a parse failure into your own error type</h3>
<pre><code>// parse gives Result&lt;u32, ParseIntError&gt;; map_err rewrites the error side
let slot = text.parse::&lt;u32&gt;()
.map_err(|_| format!("slot must be a number, got: {text}"))?;
// Option -&gt; Result with a message
let coin = Coin::parse(text).ok_or(format!("unknown coin: {text}"))?;</code></pre>
<h3>Searching and changing a <code>Vec</code></h3>
<pre><code>// read-only search, returning a borrow of the item
for coin in &self.coins {
if coin.value() == 10 { return Some(coin); }
}
// mutable pass — change an item in place
for coin in &mut self.coins {
// *coin = Coin::Dime;
}
// keep only what matches
self.coins.retain(|coin| coin.value() &gt; 5);
// the short forms — |coin| ... is a closure (ch 13); both are worth knowing now
self.coins.iter().find(|coin| coin.value() == 10) // -&gt; Option&lt;&Coin&gt;
self.coins.iter().position(|coin| coin.value() == 10) // -&gt; Option&lt;usize&gt;</code></pre>
<p>A plain <code>for</code> loop does everything here. Use it if the closure forms feel unfamiliar — clarity beats brevity while you rebuild.</p>
<h3>Exiting with a status code</h3>
<pre><code>eprintln!("error: {message}"); // stderr, not stdout
std::process::exit(1);</code></pre>
<h2>Suggested order</h2>
<p>Not stages — just the order that keeps the compiler useful. Run <code>cargo test</code> after each.</p>
<ol>
<li><code>lib.rs</code> + <code>task.rs</code>. Four tests should go green. This proves your module wiring works before any logic exists.</li>
<li><code>command.rs</code>. Four more. The <code>rejects_bad_input</code> test is the interesting one.</li>
<li><code>store.rs</code>. The remaining nine.</li>
<li><code>main.rs</code>. Untested — verify by running the session from the top of this page yourself.</li>
</ol>
<h2>Hints</h2>
<p>Ten minutes stuck on the <em>same</em> thing first. Earlier than that and you are buying a smaller lesson.</p>
<details>
<summary>My module paths will not resolve</summary>
<p>Three separate rules, and mixing them up causes most of these errors:</p>
<ul>
<li>Inside the library (any file under <code>src/</code> except <code>main.rs</code>), reach a sibling module with <code>use crate::task::Priority;</code>.</li>
<li>In <code>main.rs</code> and in <code>tests/</code>, you are in a different crate. Use the package name: <code>use tasks::task::Priority;</code>.</li>
<li>A module does not exist until <code>lib.rs</code> declares it. <code>pub mod task;</code> in <code>lib.rs</code> is what makes the file <code>src/task.rs</code> part of the crate. No declaration, no module — regardless of the file existing.</li>
</ul>
<p>Also: <code>pub</code> is needed at every level of a path. A <code>pub fn</code> inside a private <code>mod</code> is still unreachable from outside.</p>
</details>
<details>
<summary>Ids get reused after a removal and I cannot see why</summary>
<p>If the next id is derived from the list — its length, or the largest id present — then deleting changes it. The counter has to be its own field that only ever increases, independent of what the list currently holds. Two fields, not one.</p>
</details>
<details>
<summary>The borrow checker rejects my complete() or remove()</summary>
<p>You are likely holding a read borrow and then asking for a write borrow while the first is still live — the <code>E0502</code> pattern we walked through. Options, in order of simplicity:</p>
<ul>
<li>Iterate mutably in one pass: <code>for task in &mut self.tasks</code>, and mutate when the id matches.</li>
<li>Find the <em>index</em> first (a <code>usize</code>, which is <code>Copy</code> and holds no borrow), let that borrow end, then index to mutate or remove.</li>
<li>For <code>remove</code>, <code>retain</code> does it in one call with no explicit borrow at all.</li>
</ul>
<p>The question to ask is always: <em>which two borrows overlap, and can I end the first sooner?</em></p>
</details>
<details>
<summary>Something about a missing trait on my types</summary>
<p>Read which trait and which type the error names, then add it to that type's <code>derive</code> list. <code>assert_eq!</code> needs <code>PartialEq</code> to compare and <code>Debug</code> to print the failure. If a type contains a <code>String</code>, it cannot be <code>Copy</code> — <code>String</code> owns heap data, and that is exactly the move-versus-copy distinction from chapter 4.</p>
</details>
<details>
<summary>How do I return &str from label() without lifetime annotations?</summary>
<p><code>pub fn label(&self) -&gt; &str</code> compiles as written. The returned lifetime is inferred from <code>&self</code>, and a string literal outlives everything, so it fits. You do not need to write <code>'static</code> or any annotation. If you would rather sidestep it entirely, return <code>String</code> and adjust nothing else — but try the borrowed version first, since it is the idiomatic one.</p>
</details>
<details>
<summary>I cannot get the "-1 is an error" test to pass</summary>
<p>Try it in isolation: what does <code>"-1".parse::&lt;u32&gt;()</code> return? Write four lines in a scratch project and look. The answer means this row needs no code of its own — your existing numeric parsing already covers it.</p>
</details>
<h2>Done means</h2>
<ul>
<li><code>cargo test</code> → <code>17 passed; 0 failed</code>.</li>
<li>Every session line at the top of this page reproduces.</li>
<li>Failures print to stderr and exit non-zero: <code>cargo run -q -- fly; echo $?</code> → <code>1</code>.</li>
<li><code>cargo build</code> is warning-free. Warnings are findings, not noise — read each one.</li>
<li><code>Store</code>'s fields are private, and nothing outside <code>store.rs</code> needs them.</li>
</ul>
<h2>Stretch goals</h2>
<p>Only after 17/17. Each one drags in the next chapter you will need for backend work:</p>
<ol>
<li><strong><code>Display</code> instead of <code>label()</code>.</strong> Implement <code>std::fmt::Display</code> for <code>Status</code> and <code>Priority</code>, then print with <code>{}</code>. This is your first hand-written trait impl (ch10). Keep <code>label()</code> so the tests still pass.</li>
<li><strong>An error enum.</strong> Replace <code>String</code> errors with <code>enum TaskError { UnknownCommand(String), NotFound(u32), ... }</code>. You will need <code>Display</code> on it, and the tests will keep passing since they only check <code>is_err()</code>. This is how real Rust reports errors.</li>
<li><strong>A <code>start</code> command</strong> that sets <code>Status::InProgress</code> — and notice how the compiler lists every <code>match</code> you now have to update. That is exhaustiveness paying you back.</li>
<li><strong>Sort <code>list</code> by priority</strong>, high first. Needs <code>#[derive(PartialOrd, Ord)]</code> and awareness that variant declaration order defines the ordering.</li>
<li><strong>Persist to a file</strong> between runs. Plain text is enough; no dependencies needed. This is where <code>?</code>, <code>io::Error</code>, and real error mixing stop being an exercise.</li>
</ol>
<footer>
<p><strong>Primary source:</strong> <a href="https://doc.rust-lang.org/stable/book/ch12-00-an-io-project.html">The Rust Book, ch. 12 — An I/O Project</a>, which builds a CLI with this same separation and is the best companion read. For the module rules specifically: <a href="https://doc.rust-lang.org/stable/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html">ch. 7</a>.</p>
<p><strong>Concepts:</strong> <a href="0004-structs-enums-packages.html">0004 — Structs, enums, and packages</a> · <strong>Reference:</strong> <a href="../reference/rust-syntax.html">Rust syntax reference</a> · <strong>Previous:</strong> <a href="0002-write-a-cli-from-blank.html">0002 — Write a CLI from blank</a></p>
<p>Stuck past the hints, or want the design decisions critiqued once it is green? Bring it to me — reviewing your structure is worth more than another lesson. Paste the compiler error verbatim; the exact text usually names the fix.</p>
</footer>
</body>
</html>
+184
View File
@@ -0,0 +1,184 @@
// The specification, as executable tests. Do not edit this file — make it pass.
// Copy to: tasks/tests/spec.rs
// Run with: cargo test
use tasks::command::Command;
use tasks::store::Store;
use tasks::task::{Priority, Status, Task};
/// Helper: build an argument list the way main() would pass it in
/// (program name already stripped).
fn args(list: &[&str]) -> Vec<String> {
list.iter().map(|s| s.to_string()).collect()
}
// ---------- task.rs ----------
#[test]
fn new_task_starts_as_todo() {
let task = Task::new(1, "write the spec", Priority::High);
assert_eq!(task.id, 1);
assert_eq!(task.title, "write the spec");
assert_eq!(task.status, Status::Todo);
assert_eq!(task.priority, Priority::High);
}
#[test]
fn status_labels() {
assert_eq!(Status::Todo.label(), "todo");
assert_eq!(Status::InProgress.label(), "in-progress");
assert_eq!(Status::Done.label(), "done");
}
#[test]
fn priority_labels() {
assert_eq!(Priority::Low.label(), "low");
assert_eq!(Priority::Medium.label(), "medium");
assert_eq!(Priority::High.label(), "high");
}
#[test]
fn priority_parses_known_words_only() {
assert_eq!(Priority::parse("low"), Some(Priority::Low));
assert_eq!(Priority::parse("medium"), Some(Priority::Medium));
assert_eq!(Priority::parse("high"), Some(Priority::High));
assert_eq!(Priority::parse("urgent"), None);
assert_eq!(Priority::parse(""), None);
}
// ---------- command.rs ----------
#[test]
fn parses_add_with_default_priority() {
let command = Command::parse(&args(&["add", "buy milk"])).unwrap();
assert_eq!(
command,
Command::Add {
title: "buy milk".to_string(),
priority: Priority::Medium,
}
);
}
#[test]
fn parses_add_with_explicit_priority() {
let command = Command::parse(&args(&["add", "ship it", "high"])).unwrap();
assert_eq!(
command,
Command::Add {
title: "ship it".to_string(),
priority: Priority::High,
}
);
}
#[test]
fn parses_list_done_and_remove() {
assert_eq!(Command::parse(&args(&["list"])).unwrap(), Command::List);
assert_eq!(
Command::parse(&args(&["done", "7"])).unwrap(),
Command::Done { id: 7 }
);
assert_eq!(
Command::parse(&args(&["remove", "12"])).unwrap(),
Command::Remove { id: 12 }
);
}
#[test]
fn rejects_bad_input() {
assert!(Command::parse(&args(&[])).is_err(), "no command at all");
assert!(Command::parse(&args(&["fly"])).is_err(), "unknown command");
assert!(Command::parse(&args(&["add"])).is_err(), "add with no title");
assert!(
Command::parse(&args(&["add", "x", "urgent"])).is_err(),
"unknown priority word"
);
assert!(Command::parse(&args(&["done"])).is_err(), "done with no id");
assert!(
Command::parse(&args(&["done", "abc"])).is_err(),
"id is not a number"
);
assert!(
Command::parse(&args(&["remove", "-1"])).is_err(),
"negative id is not a u32"
);
}
// ---------- store.rs ----------
#[test]
fn add_returns_ids_starting_at_one() {
let mut store = Store::new();
assert_eq!(store.add("first", Priority::Low), 1);
assert_eq!(store.add("second", Priority::Low), 2);
assert_eq!(store.add("third", Priority::Low), 3);
}
#[test]
fn tasks_come_back_in_insertion_order() {
let mut store = Store::new();
store.add("first", Priority::Low);
store.add("second", Priority::High);
let listed = store.tasks();
assert_eq!(listed.len(), 2);
assert_eq!(listed[0].title, "first");
assert_eq!(listed[1].title, "second");
}
#[test]
fn new_store_is_empty() {
let store = Store::new();
assert!(store.tasks().is_empty());
}
#[test]
fn complete_sets_status_to_done() {
let mut store = Store::new();
let id = store.add("do the thing", Priority::Medium);
assert_eq!(store.find(id).unwrap().status, Status::Todo);
assert!(store.complete(id).is_ok());
assert_eq!(store.find(id).unwrap().status, Status::Done);
}
#[test]
fn complete_unknown_id_is_an_error() {
let mut store = Store::new();
store.add("only task", Priority::Low);
assert!(store.complete(99).is_err());
}
#[test]
fn remove_deletes_only_that_task() {
let mut store = Store::new();
let first = store.add("first", Priority::Low);
let second = store.add("second", Priority::Low);
assert!(store.remove(first).is_ok());
assert_eq!(store.tasks().len(), 1);
assert!(store.find(first).is_none());
assert!(store.find(second).is_some());
}
#[test]
fn remove_unknown_id_is_an_error() {
let mut store = Store::new();
assert!(store.remove(1).is_err());
}
#[test]
fn find_returns_none_for_missing_id() {
let store = Store::new();
assert!(store.find(1).is_none());
}
#[test]
fn ids_are_not_reused_after_remove() {
let mut store = Store::new();
let first = store.add("first", Priority::Low);
store.remove(first).unwrap();
let second = store.add("second", Priority::Low);
assert_ne!(first, second, "a removed id must not be handed out again");
}
+398
View File
@@ -0,0 +1,398 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Structs, enums, and packages — from zero</title>
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body>
<h1>Structs, enums, and packages</h1>
<p class="subtitle">Lesson 0004 · read this before <a href="0003-build-a-task-cli.html">0003</a> · reading, not typing · ~25 minutes</p>
<p>Three ideas, taught from nothing. Every code block below was run in a real project and every output and error message on this page is copied from that run — none of it is written from memory.</p>
<p>The domain is a café, deliberately. <a href="0003-build-a-task-cli.html">Lesson 0003</a> is a task CLI, so nothing here can be pasted into it. You will have to translate, and translating is where the understanding happens.</p>
<div class="callout">
<strong>Read this one. Do not type it.</strong> This is the knowledge half. 0003 is the skill half — that is where your hands go on the keyboard. Reading is cheap and this page is short; the project is where it sticks.
</div>
<h2>Part 1 — Structs</h2>
<p>A struct is a <strong>named bundle of fields</strong>. That is genuinely all it is. If you have used an object, a record, or a dictionary with fixed keys, you already have the idea.</p>
<pre><code>struct Drink {
name: String,
shots: u32,
iced: bool,
}</code></pre>
<p>This defines a <em>type</em>. It creates nothing and allocates nothing — it tells the compiler that a thing called <code>Drink</code> has exactly these three fields with exactly these types.</p>
<h3>Building one</h3>
<pre><code>let latte = Drink {
name: String::from("latte"),
shots: 1,
iced: false,
};
println!("{} / {} shots / iced={}", latte.name, latte.shots, latte.iced);</code></pre>
<pre><code>1. latte / 1 shots / iced=false</code></pre>
<p>Two rules that catch people coming from other languages:</p>
<ul>
<li><strong>Every field must be given a value.</strong> There are no defaults and no null. If you want "no value", the type has to say so — that is what <code>Option</code> is for, and we get to it in Part 2.</li>
<li>Field order in the literal does not matter. Names do.</li>
</ul>
<h3>Methods: the <code>impl</code> block</h3>
<p>Functions that belong to a type live in a separate block. The struct says <em>what it is</em>; the <code>impl</code> block says <em>what it can do</em>.</p>
<pre><code>impl Drink {
fn new(name: &str, shots: u32) -&gt; Drink { // no self
Drink { name: name.to_string(), shots, iced: false }
}
fn price(&self) -&gt; u32 { // &self
250 + self.shots * 50
}
fn add_shot(&mut self) { // &mut self
self.shots += 1;
}
fn into_name(self) -&gt; String { // self
self.name
}
}</code></pre>
<p>The first parameter is the whole lesson here. There are four possibilities and they mean four different things:</p>
<table>
<tr><th align="left">First parameter</th><th align="left">Name</th><th align="left">Called as</th><th align="left">Means</th></tr>
<tr><td>none</td><td>associated function</td><td><code>Drink::new(..)</code></td><td>Related to the type, but there is no instance yet. This is how you make one.</td></tr>
<tr><td><code>&self</code></td><td>method</td><td><code>drink.price()</code></td><td>Borrows to <strong>read</strong>. Cannot change anything.</td></tr>
<tr><td><code>&mut self</code></td><td>method</td><td><code>drink.add_shot()</code></td><td>Borrows to <strong>change</strong>. Requires the variable be <code>mut</code>.</td></tr>
<tr><td><code>self</code></td><td>consuming method</td><td><code>drink.into_name()</code></td><td><strong>Takes ownership.</strong> The caller cannot use the value afterwards.</td></tr>
</table>
<p>Rust has no <code>constructor</code> keyword. <code>new</code> is an ordinary associated function that people agreed to call <code>new</code>. Nothing enforces the name.</p>
<p>Real output from all four:</p>
<pre><code>2. price = 300
3. after add_shot: 2 shots, price = 350
4. Drink::new gave: espresso / 2 shots / iced=false
5. into_name took ownership, returned: espresso</code></pre>
<h3>What the compiler enforces</h3>
<p>Call <code>add_shot</code> on a binding that is not <code>mut</code>:</p>
<pre><code>let d = Drink::new("mocha", 1);
d.add_shot();</code></pre>
<pre><code>error[E0596]: cannot borrow `d` as mutable, as it is not declared as mutable
help: consider changing this to be mutable</code></pre>
<p>Use a value after a consuming method took it:</p>
<pre><code>let e = Drink::new("espresso", 2);
let name = e.into_name();
println!("{} {}", name, e.shots);</code></pre>
<pre><code>error[E0382]: borrow of moved value: `e`
| ----------- `e` moved due to this method call
note: `Drink::into_name` takes ownership of the receiver `self`, which moves `e`</code></pre>
<p>That second message is ownership from chapter 4, showing up in the design of your own API. The choice between <code>&self</code> and <code>self</code> is a promise to your callers about whether they keep their value. It is a design decision, not a syntax detail.</p>
<h3>Private fields — the point of structs in a library</h3>
<p><code>pub struct</code> makes the <em>type</em> visible. It does <strong>not</strong> make the fields visible. Each field needs its own <code>pub</code>, and often you want none of them:</p>
<pre><code>pub struct Menu {
items: Vec&lt;String&gt;, // private: no `pub`
}
impl Menu {
pub fn new() -&gt; Menu { Menu { items: Vec::new() } }
pub fn add(&mut self, name: &str) { self.items.push(name.to_string()); }
pub fn items(&self) -&gt; &[String] { &self.items }
}</code></pre>
<p>From outside the module, reaching for the field directly fails:</p>
<pre><code>error[E0616]: field `items` of struct `Menu` is private
| ^^^^^ private field
help: a method `items` also exists, call it with parentheses</code></pre>
<p>Note what <code>items()</code> hands back: <code>&[String]</code>, a borrowed <em>view</em>, not the <code>Vec</code> itself. Callers may read every item and cannot push, clear, or reorder. You decided what outsiders can do, and the compiler enforces it with no runtime check.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch05-01-defining-structs.html">5.1 Defining Structs</a> · <a href="https://doc.rust-lang.org/stable/book/ch05-03-method-syntax.html">5.3 Method Syntax</a> · <a href="https://doc.rust-lang.org/stable/book/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html">7.3 Paths and privacy</a></p>
<h2>Part 2 — Enums</h2>
<p>An enum lists <strong>every value this type is allowed to be</strong>. A value is exactly one of them at a time.</p>
<pre><code>enum Size {
Small,
Medium,
Large,
}</code></pre>
<p>A <code>Size</code> is small, medium, or large. Not <code>"smal"</code>, not <code>"venti"</code>, not empty, not null. Where a <code>String</code> has billions of possible values and three that you meant, <code>Size</code> has three. <strong>The illegal states no longer exist</strong>, so you never write code to check for them.</p>
<h3>Matching on yourself</h3>
<p>The most common thing an enum does is answer a question about which variant it is:</p>
<pre><code>impl Size {
fn ml(&self) -&gt; u32 {
match self {
Size::Small =&gt; 240,
Size::Medium =&gt; 350,
Size::Large =&gt; 470,
}
}
}</code></pre>
<p><code>match</code> compares a value against patterns top to bottom and runs the first arm that fits. It is an <em>expression</em> — it produces a value, which is why there is no <code>return</code> above.</p>
<pre><code>1. Large is 470 ml
2. is it large? true</code></pre>
<h3>Exhaustiveness — the reason enums are worth it</h3>
<p>Delete one arm:</p>
<pre><code>error[E0004]: non-exhaustive patterns: `&Size::Large` not covered
| ^^^^ pattern `&Size::Large` not covered
note: `Size` defined here
= note: the matched value is of type `&Size`
help: ensure that all possible cases are being handled by adding a match arm
with a wildcard pattern or an explicit pattern as shown</code></pre>
<p>Not a warning. The program does not build. Now the version that actually pays you back — add a fourth variant and change <strong>nothing else</strong>:</p>
<pre><code>enum Size { Small, Medium, Large, ExtraLarge }</code></pre>
<pre><code>error[E0004]: non-exhaustive patterns: `&Size::ExtraLarge` not covered</code></pre>
<p>The compiler now walks you to every single place in the codebase that has to think about the new case. In a language with string constants or integer flags, adding a case is silent, and you find the places you forgot in production.</p>
<p>This is why <code>_ =&gt; {}</code> as a catch-all arm should make you pause. It silences that help forever. Use it when you genuinely mean "everything else", not to shut the compiler up.</p>
<h3>Variants that carry data</h3>
<p>This is the part with no equivalent in most languages, and the part worth slowing down for. <strong>Each variant can carry different data of a different shape.</strong></p>
<pre><code>enum Payment {
Cash { received: u32 }, // named fields, like a struct
Card(String), // one unnamed field, like a tuple
Voucher { code: String, off: u32 }, // several named fields
OnTheHouse, // nothing at all
}</code></pre>
<p>A <code>Payment</code> is one of four things, and the data it carries depends on which. A cash payment has an amount received. A voucher has a code and a discount. A free drink has nothing. There is no <code>Payment</code> that has a voucher code but is cash — that state cannot be constructed.</p>
<p>Building them:</p>
<pre><code>Payment::Cash { received: 500 }
Payment::Card(String::from("4242"))
Payment::Voucher { code: String::from("FREE10"), off: 100 }
Payment::OnTheHouse</code></pre>
<p>And matching pulls the data back out, binding it to names you can use in that arm:</p>
<pre><code>match payment {
Payment::Cash { received } =&gt; format!("cash, {received} received"),
Payment::Card(last4) =&gt; format!("card ending {last4}"),
Payment::Voucher { code, off } =&gt; format!("voucher {code}, {off} off"),
Payment::OnTheHouse =&gt; String::from("free"),
}</code></pre>
<pre><code>5. Cash { received: 500 } -&gt; cash, 500 received
5. Card("4242") -&gt; card ending 4242
5. Voucher { code: "FREE10", off: 100 } -&gt; voucher FREE10, 100 off
5. OnTheHouse -&gt; free</code></pre>
<p>Inside <code>Payment::Cash { received }</code>, the name <code>received</code> becomes a variable holding that variant's value. You cannot reach it any other way — the data is sealed inside the variant, and <code>match</code> is the key. That sealing is exactly why the compiler can promise you never read a voucher code off a cash payment.</p>
<h3>You have been using enums the whole time</h3>
<pre><code>enum Option&lt;T&gt; { Some(T), None }
enum Result&lt;T, E&gt; { Ok(T), Err(E) }</code></pre>
<p>That is their real definition — ordinary enums with data-carrying variants, no special compiler magic. Everything you learned about <code>Result</code> in <a href="../reference/rust-syntax.html">the Result pattern</a> is just this:</p>
<pre><code>match found {
Some(s) =&gt; println!("Option::Some carried a {:?}", s),
None =&gt; println!("Option::None carried nothing"),
}</code></pre>
<pre><code>6. Option::Some carried a Small
7. if let pulled out Large</code></pre>
<p>And <code>if let</code> is the shortcut for when you care about one variant and want to ignore the rest:</p>
<pre><code>if let Some(s) = Size::parse("large") {
println!("if let pulled out {:?}", s);
}</code></pre>
<h3>Option vs Result, when you write your own function</h3>
<pre><code>fn parse(text: &str) -&gt; Option&lt;Size&gt; {
match text {
"small" =&gt; Some(Size::Small),
"medium" =&gt; Some(Size::Medium),
"large" =&gt; Some(Size::Large),
_ =&gt; None,
}
}</code></pre>
<pre><code>3. parse("medium") = Some(Medium)
4. parse("venti") = None</code></pre>
<p>Why <code>Option</code> and not <code>Result</code> here? Because there is nothing useful to say about the failure. "That is not a size" is the whole story, and the absence itself carries it. Reach for <code>Result</code> when the caller needs to know <em>why</em> — a file that was missing versus one you lacked permission to read.</p>
<h3>Struct or enum?</h3>
<table>
<tr><th align="left">Question</th><th align="left">Use</th></tr>
<tr><td>Is it <strong>this AND this AND this</strong>?</td><td><code>struct</code></td></tr>
<tr><td>Is it <strong>this OR this OR this</strong>?</td><td><code>enum</code></td></tr>
</table>
<p>A drink has a name <em>and</em> a shot count <em>and</em> a size → struct. A size is small <em>or</em> medium <em>or</em> large → enum. They nest freely: the struct holds a field whose type is the enum, which is precisely what <code>Drink { size: Size }</code> means and what you will build in 0003.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch06-01-defining-an-enum.html">6.1 Defining an Enum</a> · <a href="https://doc.rust-lang.org/stable/book/ch06-02-match.html">6.2 The match Control Flow Construct</a> · <a href="https://doc.rust-lang.org/stable/book/ch06-03-if-let.html">6.3 Concise Control Flow with if let</a></p>
<h2>Part 3 — Packages, crates, modules</h2>
<p>Four words that get used interchangeably and should not be. From outside in:</p>
<table>
<tr><th align="left">Word</th><th align="left">What it is</th><th align="left">Where you see it</th></tr>
<tr><td><strong>Package</strong></td><td>What Cargo manages. One <code>Cargo.toml</code>. Can hold up to one library crate and any number of binary crates.</td><td><code>cargo new cafe</code></td></tr>
<tr><td><strong>Crate</strong></td><td>What the compiler compiles, in one go. A tree of modules with a single root file.</td><td><code>src/lib.rs</code>, <code>src/main.rs</code></td></tr>
<tr><td><strong>Module</strong></td><td>A namespace inside a crate. Controls what is visible to whom.</td><td><code>pub mod menu;</code></td></tr>
<tr><td><strong>Path</strong></td><td>How you name an item: <code>cafe::menu::Menu</code>.</td><td><code>use</code> lines</td></tr>
</table>
<p>The one that matters for your project: <strong>a package can contain two crates, and they are as separate as if a stranger wrote one of them.</strong></p>
<h3>The layout</h3>
<pre><code>cafe/
├── Cargo.toml
├── src/
│ ├── lib.rs ← root of the LIBRARY crate, named `cafe`
│ ├── menu.rs ← a module in that crate
│ ├── order.rs ← another module in that crate
│ └── main.rs ← root of the BINARY crate
└── tests/
└── spec.rs ← integration tests: a separate crate again</code></pre>
<p>Cargo finds all of this by filename. Here is the entire <code>Cargo.toml</code> for the working demo, unchanged from what <code>cargo new</code> produced:</p>
<pre><code>[package]
name = "cafe"
version = "0.1.0"
edition = "2024"
[dependencies]</code></pre>
<p>No <code>[lib]</code>. No <code>[[bin]]</code>. Convention over configuration: <code>src/lib.rs</code> means "library crate", <code>src/main.rs</code> means "binary crate", and both are picked up automatically.</p>
<h3>Wiring it, one error at a time</h3>
<p>This is the sequence I actually ran, and it is the sequence you will hit.</p>
<p><strong>Step 1.</strong> <code>src/menu.rs</code> exists and contains <code>pub struct Menu</code>. <code>src/lib.rs</code> is empty.</p>
<pre><code>error[E0433]: cannot find `menu` in `cafe`</code></pre>
<p>The file existing is not enough. <strong>A module does not exist until its parent declares it.</strong> <code>src/menu.rs</code> is an unread file on disk until something says <code>mod menu;</code>.</p>
<p><strong>Step 2.</strong> Put <code>mod menu;</code> in <code>lib.rs</code>.</p>
<pre><code>error[E0603]: module `menu` is private
| private module
note: the module `menu` is defined here</code></pre>
<p>Now it exists, and it is invisible from outside. <strong>Everything in Rust is private by default</strong>, including modules. <code>mod menu;</code> means "this module is part of my crate". <code>pub mod menu;</code> means "and outsiders may use it".</p>
<p><strong>Step 3.</strong> <code>pub mod menu;</code></p>
<pre><code>["latte"]</code></pre>
<p>Working. Three states, two error messages, and each message named exactly what was wrong.</p>
<h3><code>crate::</code> versus the package name</h3>
<p>The single most common stumble, and the one to memorise:</p>
<pre><code>// src/order.rs — INSIDE the library crate, reaching a sibling module
use crate::menu::Menu;
// src/main.rs — a DIFFERENT crate, so use the library's name
use cafe::menu::Menu;
// tests/spec.rs — also a different crate, same as main.rs
use cafe::menu::Menu;</code></pre>
<p><code>crate</code> means "the root of the crate I am compiling right now". In <code>main.rs</code>, that root is <code>main.rs</code> — which has no <code>menu</code> module, so:</p>
<pre><code>error[E0432]: unresolved import `crate::menu`</code></pre>
<p>The question to ask whenever a path will not resolve: <strong>which crate am I in right now?</strong> If the file is <code>main.rs</code> or anything under <code>tests/</code>, you are outside the library and must use its name.</p>
<p>The working version, run for real:</p>
<pre><code>all items: ["latte", "mocha"]
first item: Some("latte")</code></pre>
<h3>Why bother with a library crate at all?</h3>
<p>You could put everything in <code>main.rs</code>. Two concrete reasons not to:</p>
<ol>
<li><strong>Integration tests can only reach a library.</strong> Files in <code>tests/</code> compile as separate crates and can only <code>use</code> public items from the library. They cannot see inside <code>main.rs</code> at all. If your logic lives in <code>main.rs</code>, it is untestable from <code>tests/</code>.</li>
<li><strong>It forces you to design a real boundary.</strong> Your <code>main.rs</code> becomes just another consumer, so anything awkward about your API you feel immediately — the same as an outside user would.</li>
</ol>
<pre><code>test cannot_touch_private_field ... ok
test menu_starts_empty ... ok
test result: ok. 2 passed; 0 failed</code></pre>
<h3>The privacy rules, complete</h3>
<ul>
<li>Everything is <strong>private by default</strong>: modules, structs, fields, functions, enum variants' containing type.</li>
<li><code>pub</code> is needed at <strong>every level of the path</strong>. A <code>pub fn</code> inside a private <code>mod</code> is unreachable from outside.</li>
<li><code>pub struct</code> does <strong>not</strong> make fields public. Each field needs its own <code>pub</code>.</li>
<li><code>pub enum</code> <strong>does</strong> make all its variants public. Enums are the exception — a variant you cannot name is useless.</li>
<li>Child modules can always see their ancestors' private items. Privacy points outward, not inward.</li>
</ul>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch07-01-packages-and-crates.html">7.1 Packages and Crates</a> · <a href="https://doc.rust-lang.org/stable/book/ch07-02-defining-modules-to-control-scope-and-privacy.html">7.2 Defining Modules</a> · <a href="https://doc.rust-lang.org/stable/book/ch07-05-separating-modules-into-different-files.html">7.5 Separating Modules into Files</a> · <a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 Test Organization</a></p>
<h2>Derives, briefly</h2>
<p>You will need these in 0003 and they look like magic, so: <code>#[derive(..)]</code> asks the compiler to write an obvious implementation for you.</p>
<table>
<tr><th align="left">Derive</th><th align="left">Gives you</th><th align="left">Needed when</th></tr>
<tr><td><code>Debug</code></td><td><code>{:?}</code> printing</td><td>Any test that prints your type on failure</td></tr>
<tr><td><code>PartialEq</code></td><td><code>==</code> and <code>!=</code></td><td><code>assert_eq!</code> on your type</td></tr>
<tr><td><code>Clone</code></td><td><code>.clone()</code></td><td>You need a second copy explicitly</td></tr>
<tr><td><code>Copy</code></td><td>Assignment copies instead of moving</td><td>Small types with no heap data</td></tr>
</table>
<pre><code>#[derive(Debug, Clone, Copy, PartialEq)]
enum Size { Small, Medium, Large }</code></pre>
<p><code>Copy</code> has a hard limit: a type containing a <code>String</code> or <code>Vec</code> <strong>cannot</strong> be <code>Copy</code>, because those own heap memory and copying the pointer twice would mean freeing it twice. That is the move-versus-copy split from chapter 4, now constraining your own types. A three-variant enum with no data is a single byte and copies happily; a struct with a <code>String</code> title does not.</p>
<p>If you forget one, the compiler names the exact trait and the exact type. Read that message rather than guessing from this table.</p>
<h2>The five sentences worth keeping</h2>
<ol>
<li>A <strong>struct</strong> is this AND this AND this. An <strong>enum</strong> is this OR this OR this.</li>
<li><code>impl</code> holds the behaviour; the first parameter (<code>&self</code>, <code>&mut self</code>, <code>self</code>, or nothing) decides what the caller keeps.</li>
<li><code>match</code> on an enum must cover every variant — which is why adding a variant produces a to-do list instead of a bug.</li>
<li>A <strong>package</strong> holds <strong>crates</strong>; <code>src/lib.rs</code> and <code>src/main.rs</code> are two separate crates, so <code>main.rs</code> says <code>use tasks::..</code>, never <code>use crate::..</code>.</li>
<li>Everything is private until you write <code>pub</code>, and a file is not a module until a parent declares <code>mod</code>.</li>
</ol>
<footer>
<p><strong>Primary source:</strong> <a href="https://doc.rust-lang.org/stable/book/ch05-00-structs.html">The Rust Book, ch. 5 (structs)</a>, <a href="https://doc.rust-lang.org/stable/book/ch06-00-enums.html">ch. 6 (enums)</a>, <a href="https://doc.rust-lang.org/stable/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html">ch. 7 (packages and modules)</a>. Chapter 6 is the highest-value read of the three — enums with data are the idea most worth having properly.</p>
<p><strong>Next:</strong> <a href="0003-build-a-task-cli.html">0003 — Build a task CLI</a> · <strong>Reference:</strong> <a href="../reference/rust-syntax.html">Rust syntax reference</a></p>
<p>If any single paragraph here did not land, say which one. Vague explanations are my fault, not yours, and it is much cheaper to fix one now than to hit it as a compiler error in the middle of the project.</p>
</footer>
</body>
</html>
+542
View File
@@ -0,0 +1,542 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Traits — the promise, and the two your CLI needs</title>
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body>
<h1>Traits, <code>Display</code>, and a real error type</h1>
<p class="subtitle">Lesson 0005 · after <a href="0003-build-a-task-cli.html">0003</a> · reading, then a 20-minute drill on your own code · ~30 minutes</p>
<p>Every code block, every output line, and every error message on this page was produced by running it. Nothing here is written from memory.</p>
<p>The demo domain is a temperature sensor, deliberately — your project is a task CLI, so nothing below can be pasted into it. You have to translate, and translating is where the learning happens.</p>
<h2>First: what 0003 actually showed</h2>
<p>You passed all 17 tests. The three modules — <code>task</code>, <code>command</code>, <code>store</code> — do what the spec asked, you used <code>?</code> with <code>ok_or</code>, and <code>find(|task| task.id == id)</code> is the idiomatic answer, not a loop. Structs, enums and the two-crate package are no longer the weak spot.</p>
<p>But the tests only reach the library. <code>main.rs</code> is the one file no test could see, and it is the one file that misses the contract. Run your own crate:</p>
<pre><code>$ cargo run -- fly
no valid commands
$ echo $?
0
$ cargo run -- done 9
thread 'main' (146760) panicked at src/main.rs:25:33:
called `Result::unwrap()` on an `Err` value: "id not found"
$ echo $?
101</code></pre>
<p>The spec said: <em>errors print to stderr and exit with status 1</em>. What happens instead is three separate faults:</p>
<ol>
<li><code>println!</code> sends errors to <strong>stdout</strong>, so a pipe or a redirect mixes them into real output.</li>
<li>Exit status <strong>0</strong> means "success", so any script calling your CLI believes the failure worked.</li>
<li><code>.unwrap()</code> on <code>complete()</code> and <code>remove()</code> <strong>panics</strong> — status 101 and a backtrace hint — on the ordinary, expected case of a wrong id.</li>
</ol>
<p>That is not a syntax gap. It is the exact thing your mission names: <em>errors with <code>Result</code>, not <code>panic!</code></em>. And the tidy fix needs one thing you have not met yet: traits. So — traits first, then you fix those three faults yourself in the drill at the bottom.</p>
<h2>Part 1 — A trait is a promise</h2>
<p>A trait is a <strong>list of method signatures that a type can promise to provide</strong>. Nothing more. If you have used an interface, you have the shape already; the differences come later.</p>
<pre><code>trait Reading {
fn celsius(&amp;self) -&gt; f64;
fn label(&amp;self) -&gt; String {
format!("{:.1}C", self.celsius())
}
}</code></pre>
<p>Two kinds of method are in there, and the difference is the whole of Part 1:</p>
<ul>
<li><code>celsius</code> ends in a <strong>semicolon</strong> — a required method. Every implementor must write it.</li>
<li><code>label</code> has a <strong>body</strong> — a default method. Implementors get it for free and may override it. Note that the default calls <code>self.celsius()</code>, a method the trait does not yet have an implementation for. That is allowed: the trait can build on its own promises.</li>
</ul>
<p>Now two unrelated types keep that promise. <code>impl</code> <em>Trait</em> <code>for</code> <em>Type</em>:</p>
<pre><code>struct Thermometer { room: String, celsius: f64 }
struct Kettle { fahrenheit: f64 }
impl Reading for Thermometer {
fn celsius(&amp;self) -&gt; f64 { self.celsius }
fn label(&amp;self) -&gt; String { // overrides the default
format!("{} is {:.1}C", self.room, self.celsius)
}
}
impl Reading for Kettle {
fn celsius(&amp;self) -&gt; f64 { // takes the default `label`
(self.fahrenheit - 32.0) * 5.0 / 9.0
}
}</code></pre>
<pre><code>1. kitchen is 21.5C
2. 100.0C</code></pre>
<p>Line 1 is the override, line 2 is the default method doing the work for a type whose numbers were never even in Celsius. Two types, one vocabulary.</p>
<div class="callout">
<strong>The mental model.</strong> An <code>impl Type</code> block is what a type can do <em>for itself</em>. An <code>impl Trait for Type</code> block is a type <em>keeping a promise someone else defined</em>. Same keyword, two different jobs — that is why 0004's <code>impl Task { .. }</code> and this page's <code>impl Reading for Kettle { .. }</code> look so similar.
</div>
<p>Break the promise — drop <code>celsius</code> from the <code>Kettle</code> impl — and the compiler names exactly what is missing:</p>
<pre><code>error[E0046]: not all trait items implemented, missing: `celsius`
--&gt; src/main.rs:31:1
|
5 | fn celsius(&amp;self) -&gt; f64;
| ------------------------- `celsius` from trait
...
31 | impl Reading for Kettle {
| ^^^^^^^^^^^^^^^^^^^^^^^ missing `celsius` in implementation</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch10-02-traits.html#defining-a-trait">10.2 Defining a Trait</a> · <a href="https://doc.rust-lang.org/stable/book/ch10-02-traits.html#default-implementations">Default Implementations</a></p>
<h2>Part 2 — <code>Display</code>: the trait behind <code>{}</code></h2>
<p>Here is the part that pays off immediately. <code>println!("{}", x)</code> is not magic and it is not built into the language for "printable things". It calls <strong>one trait method</strong>, <code>Display::fmt</code>. A type prints with <code>{}</code> if — and only if — someone implemented that trait for it.</p>
<pre><code>use std::fmt;
impl fmt::Display for Thermometer {
fn fmt(&amp;self, f: &amp;mut fmt::Formatter) -&gt; fmt::Result {
write!(f, "{} [{:.1}C]", self.room, self.celsius)
}
}</code></pre>
<p>Three things in that signature to notice, then never think about again:</p>
<ul>
<li>You do not return a string. You <strong>write into</strong> the formatter <code>f</code> that the caller supplied — no allocation happens for a <code>println!</code>.</li>
<li><code>write!</code> is <code>format!</code> aimed at a destination. It has the same syntax and returns the <code>fmt::Result</code> you need, which is why the body is one line with no semicolon.</li>
<li><code>fmt::Result</code> is just <code>Result&lt;(), fmt::Error&gt;</code> under a different name.</li>
</ul>
<pre><code>3. kitchen [21.5C]
4. to_string gave: kitchen [21.5C]</code></pre>
<p>Line 4 is the bonus and it is worth understanding: <code>.to_string()</code> was never written for <code>Thermometer</code>. The standard library says <em>every</em> type implementing <code>Display</code> gets <code>ToString</code> automatically. One trait implemented, a second one granted. <span class="cite">(<a href="https://doc.rust-lang.org/std/string/trait.ToString.html">std: <code>ToString</code></a> — "implemented automatically for any type which implements <code>Display</code>")</span></p>
<p>And the type <em>without</em> the impl — <code>Kettle</code> — cannot use <code>{}</code> at all:</p>
<pre><code>error[E0277]: `Kettle` doesn't implement `std::fmt::Display`
--&gt; src/main.rs:63:23
|
63 | println!("5. {}", k);
| -- ^ `Kettle` cannot be formatted with the default formatter
|
help: the trait `std::fmt::Display` is not implemented for `Kettle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead</code></pre>
<p>You have met this error already, in 0003 — that is what <code>#[derive(Debug)]</code> and <code>{:?}</code> were for. Now the split is clear:</p>
<table>
<tr><th>&nbsp;</th><th><code>Debug</code> — <code>{:?}</code></th><th><code>Display</code> — <code>{}</code></th></tr>
<tr><td>Audience</td><td>You, debugging</td><td>The user of the program</td></tr>
<tr><td>How you get it</td><td><code>#[derive(Debug)]</code></td><td>Hand-written; no derive exists</td></tr>
<tr><td>Shape</td><td>Structure: <code>Task { id: 1, .. }</code></td><td>Whatever you decide it reads like</td></tr>
</table>
<p>There is no <code>#[derive(Display)]</code> on purpose: the compiler can print your fields mechanically, but only you know how the sentence should read.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch10-02-traits.html#implementing-a-trait-on-a-type">10.2 Implementing a Trait on a Type</a> · std: <a href="https://doc.rust-lang.org/std/fmt/trait.Display.html"><code>fmt::Display</code></a></p>
<h3>The one rule that will bite you</h3>
<p>You may write <code>impl SomeTrait for SomeType</code> only if <strong>the trait or the type is yours</strong>. <code>Display</code> for your <code>Task</code>: fine, the type is yours. Your own trait for <code>Vec&lt;T&gt;</code>: fine, the trait is yours. <code>Display</code> for <code>Vec&lt;String&gt;</code>: rejected — both belong to the standard library. This is the <em>orphan rule</em>, and it exists so no other crate can change what your code already does. <span class="cite">(<a href="https://doc.rust-lang.org/stable/book/ch10-02-traits.html#implementing-a-trait-on-a-type">Book 10.2, "coherence"</a>)</span></p>
<h2>Part 3 — Trait bounds: generics that are allowed to do something</h2>
<p>A generic <code>&lt;T&gt;</code> on its own means "any type at all" — and a function that accepts any type may do almost nothing with it, because the compiler has no idea what it can do. Watch it fail:</p>
<pre><code>fn hottest&lt;T&gt;(items: &amp;[T]) -&gt; Option&lt;&amp;T&gt; {
items.iter().max_by(|a, b| a.celsius().total_cmp(&amp;b.celsius()))
}</code></pre>
<pre><code>error[E0599]: no method named `celsius` found for reference `&amp;&amp;T` in the current scope
--&gt; src/main.rs:46:34
|
46 | items.iter().max_by(|a, b| a.celsius().total_cmp(&amp;b.celsius()))
| ^^^^^^^ method not found in `&amp;&amp;T`
|
= help: items from traits can only be used if the trait is implemented and in scope
note: `Reading` defines an item `celsius`, perhaps you need to implement it</code></pre>
<p>The fix is a <strong>bound</strong> — a promise demanded of the caller's type:</p>
<pre><code>fn hottest&lt;T: Reading&gt;(items: &amp;[T]) -&gt; Option&lt;&amp;T&gt; {
items.iter().max_by(|a, b| a.celsius().total_cmp(&amp;b.celsius()))
}</code></pre>
<pre><code>5. hottest: attic [28.0C]</code></pre>
<p>Read <code>&lt;T: Reading&gt;</code> as: <em>T can be any type, as long as it implements <code>Reading</code></em>. Inside the function you may now use every method the trait promises, and nothing else. Both sides get a guarantee, both checked at compile time, and no lookup happens at runtime.</p>
<p>Three spellings of the same idea, so you recognise all of them in other people's code:</p>
<pre><code>fn show&lt;T: Reading&gt;(r: &amp;T) // bound in the angle brackets
fn show(r: &amp;impl Reading) // same thing, shorter
fn show&lt;T&gt;(r: &amp;T) where T: Reading // same thing, for long bound lists</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch10-02-traits.html#traits-as-parameters">10.2 Traits as Parameters</a> · <a href="https://doc.rust-lang.org/stable/book/ch10-01-syntax.html">10.1 Generic Data Types</a></p>
<h2>Part 4 — An error type is a type with two traits on it</h2>
<p>Your <code>tasks</code> crate reports failures as <code>String</code>. That works and 0003 asked for it deliberately, because the real answer needs Part 1 to Part 3. Here it is.</p>
<p>Start with what you already know how to write — an enum, one variant per way of failing, carrying whatever the caller needs:</p>
<pre><code>#[derive(Debug)]
enum SensorError {
Empty,
NotANumber(ParseFloatError),
OutOfRange(f64),
}</code></pre>
<p>Compare that with <code>String</code>. A caller can <code>match</code> on this and react differently per case; it cannot match on prose. The bad reading is still <em>in</em> the value, so the message can be built later, at the edge of the program. And you cannot typo a variant — <code>"out of rnage"</code> compiles, <code>OutOfRnage</code> does not.</p>
<p>Then two traits turn it from "an enum" into "an error":</p>
<pre><code>impl fmt::Display for SensorError {
fn fmt(&amp;self, f: &amp;mut fmt::Formatter) -&gt; fmt::Result {
match self {
SensorError::Empty =&gt; write!(f, "no reading given"),
SensorError::NotANumber(e) =&gt; write!(f, "not a number: {}", e),
SensorError::OutOfRange(v) =&gt; write!(f, "{} is outside -90..60", v),
}
}
}
impl Error for SensorError {} // std::error::Error</code></pre>
<p><code>Display</code> is the human sentence — Part 2, applied. <code>Error</code> is an empty impl: it adds no code, it only <em>marks</em> the type as an error so it fits everywhere the ecosystem expects one. But it does demand something. Delete the <code>Display</code> impl and keep <code>impl Error</code>:</p>
<pre><code>error[E0277]: `SensorError` doesn't implement `std::fmt::Display`
--&gt; src/main.rs:13:16
|
13 | impl Error for SensorError {}
| ^^^^^^^^^^^ unsatisfied trait bound
|
help: the trait `std::fmt::Display` is not implemented for `SensorError`</code></pre>
<p><code>Error</code> requires <code>Display</code> and <code>Debug</code> — a trait can demand other traits, the same way a function demands bounds. That is why <code>#[derive(Debug)]</code> sits on the enum. <span class="cite">(<a href="https://doc.rust-lang.org/std/error/trait.Error.html">std: <code>Error</code></a> — "Errors must describe themselves through the <code>Display</code> and <code>Debug</code> traits")</span></p>
<h3><code>From</code>: a trait you have been using since chapter 1</h3>
<p>Yes — <code>From</code> is another trait, and it lives in the standard library. Its whole definition is one required method:</p>
<pre><code>trait From&lt;T&gt; {
fn from(value: T) -&gt; Self; // build a Self out of a T
}</code></pre>
<p>You have called it in every lesson so far without knowing it had a name:</p>
<pre><code>let s = String::from("hi"); // this IS From: impl From&lt;&amp;str&gt; for String, in std</code></pre>
<p>So read the impl below as an English sentence — <em>"here is how to build a <code>SensorError</code> out of a <code>ParseFloatError</code>"</em>:</p>
<pre><code>impl From&lt;ParseFloatError&gt; for SensorError {
fn from(e: ParseFloatError) -&gt; SensorError {
SensorError::NotANumber(e)
}
}</code></pre>
<p>It is an ordinary function with a wrapper around it. You can call it by hand, and nothing magic happens:</p>
<pre><code>let e: ParseFloatError = "nope".parse::&lt;f64&gt;().unwrap_err();
let wrapped: SensorError = SensorError::from(e); // just a function call</code></pre>
<h4>So why bother writing it?</h4>
<p>Because <code>?</code> calls it for you. This is the whole point. <code>?</code> does not simply hand the error to your caller — it converts it first:</p>
<pre><code>let value = thing()?;
// what the compiler writes for you:
let value = match thing() {
Ok(v) =&gt; v,
Err(e) =&gt; return Err(From::from(e)), // &lt;- YOUR impl runs here
};</code></pre>
<p>These three functions are therefore the same function. Same output, three spellings:</p>
<pre><code>fn by_hand(text: &amp;str) -&gt; Result&lt;f64, SensorError&gt; {
match text.parse::&lt;f64&gt;() {
Ok(v) =&gt; Ok(v),
Err(e) =&gt; Err(SensorError::from(e)), // call it yourself
}
}
fn with_into(text: &amp;str) -&gt; Result&lt;f64, SensorError&gt; {
match text.parse::&lt;f64&gt;() {
Ok(v) =&gt; Ok(v),
Err(e) =&gt; Err(e.into()), // `.into()` is From from the other side
}
}
fn with_question(text: &amp;str) -&gt; Result&lt;f64, SensorError&gt; {
Ok(text.parse::&lt;f64&gt;()?) // `?` calls it for you
}</code></pre>
<pre><code>1. Err(NotANumber(ParseFloatError { kind: Invalid }))
2. Err(NotANumber(ParseFloatError { kind: Invalid }))
3. Err(NotANumber(ParseFloatError { kind: Invalid }))</code></pre>
<p><code>e.into()</code> and <code>SensorError::from(e)</code> are the same trait read in opposite directions: <code>from</code> starts from the destination type, <code>into</code> starts from the value you hold. Implement <code>From</code> and you get <code>into</code> for free — you never write an <code>Into</code> impl.</p>
<h4>What it looks like when the impl is missing</h4>
<p>Delete the <code>impl From</code> and the compiler names precisely what is absent:</p>
<pre><code>error[E0277]: `?` couldn't convert the error to `SensorError`
--&gt; src/main.rs:28:27
|
27 | fn with_question(text: &amp;str) -&gt; Result&lt;f64, SensorError&gt; {
| ------------------------ expected `SensorError` because of this
28 | Ok(text.parse::&lt;f64&gt;()?)
| --------------^ the trait `From&lt;ParseFloatError&gt;` is not implemented for `SensorError`
| |
| this can't be annotated with `?` because it has type `Result&lt;_, ParseFloatError&gt;`</code></pre>
<p>"the trait <code>From&lt;X&gt;</code> is not implemented for <code>YourError</code>" always means the same thing: <em>write the recipe from X to YourError</em>. (Write the same code with an annotated <code>let value: f64 = text.parse()?;</code> and the report arrives as <code>E0271</code> instead, pointing at the same missing impl.)</p>
<h4>You already depend on this — in <code>command.rs</code>, line 13</h4>
<pre><code>fn parse(args: &amp;[String]) -&gt; Result&lt;Command, String&gt; {
let first = args.first().ok_or("No Arguments Found")?;
// ^^^^^^^^^^^^^^^^^^ this is a &amp;str, not a String
}</code></pre>
<p><code>ok_or("No Arguments Found")</code> produces <code>Result&lt;_, &amp;str&gt;</code>, and your function promises <code>Result&lt;_, String&gt;</code>. Two different types — the same mismatch as above. It compiled because the standard library already ships <code>impl From&lt;&amp;str&gt; for String</code>, and <code>?</code> found it. Verified:</p>
<pre><code>$ cargo run
Err("No Arguments Found") // a String, converted on the way out</code></pre>
<p>That is why the mechanism was invisible in 0003: std had written the impl you needed. The moment your own error type appears, you write it.</p>
<p>And that is what keeps a deep call stack readable — every layer writes a bare <code>?</code>, and each error type carries its own recipe for becoming the layer above.</p>
<p>The finished function, with all three failure paths and one bare <code>?</code> doing the conversion:</p>
<pre><code>fn parse_reading(text: &amp;str) -&gt; Result&lt;f64, SensorError&gt; {
if text.is_empty() {
return Err(SensorError::Empty);
}
let value: f64 = text.parse()?; // ParseFloatError becomes SensorError here
if value &lt; -90.0 || value &gt; 60.0 {
return Err(SensorError::OutOfRange(value));
}
Ok(value)
}</code></pre>
<pre><code>1. Ok(21.5)
2. not a number: invalid float literal
3. 900 is outside -90..60
4. no reading given</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator">9.2 The <code>?</code> operator</a> · std: <a href="https://doc.rust-lang.org/std/convert/trait.From.html"><code>From</code></a></p>
<h3>Where the error meets the user</h3>
<p>One trait object is worth knowing before you touch your CLI. <code>Box&lt;dyn Error&gt;</code> means "some value on the heap that implements <code>Error</code>, decided at runtime" — the escape hatch when a function can fail in unrelated ways and you do not want an enum listing them all. <code>main</code> may return it:</p>
<pre><code>fn main() -&gt; Result&lt;(), Box&lt;dyn Error&gt;&gt; {
let ok = parse_reading("18.25")?;
println!("5. ? gave us {}", ok);
let boom = parse_reading("nope")?; // fails here
println!("never printed {}", boom);
Ok(())
}</code></pre>
<pre><code>5. ? gave us 18.25
Error: NotANumber(ParseFloatError { kind: Invalid })
$ echo $?
1</code></pre>
<p>The exit status is right, and <code>?</code> in <code>main</code> is genuinely useful in a script or a test binary. But look at the message: <code>NotANumber(ParseFloatError { kind: Invalid })</code>. That is <strong>Debug</strong>, not your carefully written <code>Display</code> — <code>main</code>'s reporting uses <code>{:?}</code>. For a CLI a human runs, you want your own sentence, so you handle it yourself at the top:</p>
<pre><code>match parse_reading(text) {
Ok(v) =&gt; println!("reading {:.1}C", v),
Err(e) =&gt; {
eprintln!("error: {}", e); // stderr, and Display
process::exit(1);
}
}</code></pre>
<pre><code>$ cargo run -- 21.5
reading 21.5C
$ echo $?
0
$ cargo run -- warm
error: not a number: warm
$ echo $?
1</code></pre>
<p><code>eprintln!</code> is <code>println!</code> aimed at stderr; <code>process::exit(1)</code> sets the status a caller reads. Those two lines and the missing <code>?</code> are the entirety of what your <code>main.rs</code> is short of.</p>
<h2>Retrieval — before the drill</h2>
<p>Answer from memory. Scrolling up to check first is the one way to waste these. Some questions are about older topics on purpose — mixing them is what makes any of it stick.</p>
<div class="q" data-type="recall" data-topic="Traits">
<p class="topic">Traits</p>
<p class="prompt">In a trait definition, what is the difference between a method that ends with a semicolon and one that ends with a block?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Semicolon = <strong>required</strong>: every implementor must write that method. Block = <strong>default</strong>: implementors get that body for free and may override it. A default may call the trait's required methods.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="mcq" data-topic="Display">
<p class="topic">Display</p>
<p class="prompt">You wrote <code>impl fmt::Display for Task</code>. Which of these does that also give you, with no extra code?</p>
<div class="options">
<button class="opt" data-correct="false"><code>task.clone()</code> returning an owned copy of it</button>
<button class="opt" data-correct="true"><code>task.to_string()</code> returning an owned String of it</button>
<button class="opt" data-correct="false"><code>task.debug()</code> returning an owned dump of it</button>
</div>
<div class="explain hidden"><code>ToString</code> is implemented by the standard library for every type that implements <code>Display</code>. <code>Clone</code> and <code>Debug</code> are separate traits, both obtained by <code>#[derive]</code>.</div>
</div>
<div class="q" data-type="recall" data-topic="Error handling">
<p class="topic">Error handling</p>
<p class="prompt">Your function returns <code>Result&lt;T, MyError&gt;</code> and calls something that fails with <code>io::Error</code>. You want a bare <code>?</code> to work. What must you write?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden"><code>impl From&lt;io::Error&gt; for MyError</code>. The <code>?</code> operator calls <code>From::from</code> on the error as it returns, so any error type with a <code>From</code> impl into yours converts automatically.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="recall" data-topic="Generics">
<p class="topic">Generics</p>
<p class="prompt">Why does <code>fn longest&lt;T&gt;(a: &amp;T, b: &amp;T)</code> refuse to compare <code>a</code> and <code>b</code> with <code>&gt;</code>, and what is the smallest fix?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Unbounded <code>T</code> promises nothing, so no methods or operators are available on it. Add the bound that provides comparison: <code>fn longest&lt;T: PartialOrd&gt;(..)</code>. The error you would see is <code>E0369</code>/<code>E0599</code>, naming the missing trait.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="mcq" data-topic="Ownership">
<p class="topic">Ownership</p>
<p class="prompt">In <code>fn fmt(&amp;self, f: &amp;mut fmt::Formatter) -&gt; fmt::Result</code>, why is the first parameter <code>&amp;self</code> rather than <code>self</code>?</p>
<div class="options">
<button class="opt" data-correct="false">Printing a value has to consume the value being printed</button>
<button class="opt" data-correct="true">Printing a value must not consume the value being printed</button>
<button class="opt" data-correct="false">Printing a value should always copy the value being printed</button>
</div>
<div class="explain hidden">A consuming <code>self</code> would move the value into <code>println!</code>, making <code>println!("{}", t)</code> the last thing you could ever do with <code>t</code>. Chapter 4's rules, deciding your API shape — the same point 0004 made about <code>into_name</code>.</div>
</div>
<div class="q" data-type="recall" data-topic="Enums">
<p class="topic">Enums</p>
<p class="prompt">Name two concrete advantages an error <code>enum</code> has over a <code>String</code> error, as your <code>tasks</code> crate uses today.</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Any two of: the caller can <code>match</code> per failure case instead of parsing prose; the data that failed is carried in the variant, so the message is built at the edge; a misspelled variant will not compile whereas a misspelled message will; adding a variant makes the compiler list every place that must handle it.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="recall" data-topic="Modules">
<p class="topic">Modules &amp; paths</p>
<p class="prompt">You add <code>impl fmt::Display for Task</code> in <code>src/task.rs</code>. What does <code>main.rs</code> have to import to print a task with <code>{}</code>?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Only <code>Task</code> itself. <code>Display</code> is already in scope everywhere <code>println!</code> is usable, because the macro refers to it by full path. The "bring the trait into scope" rule applies when <em>you</em> call a trait method directly — e.g. <code>use std::io::Write;</code> before calling <code>.write_all()</code>.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div id="summary"><div id="summary-body"></div><p id="summary-total"></p>
<button id="report-btn">Copy report</button><pre id="report-output" class="hidden"></pre></div>
<h2>The drill — 20 minutes, your own crate</h2>
<div class="callout">
<strong>Type it, do not paste it.</strong> The code above is a sensor in a different project; none of it fits <code>tasks</code> unchanged. Keep <a href="../reference/rust-syntax.html">the syntax reference</a> open — looking syntax up is free, copying answers is not.
</div>
<p>Work in <code>~/learn-rust/tasks</code>. Three steps, each with its own check. Run <code>cargo test</code> at the end: all 17 must still pass, because you are not changing the library's contract.</p>
<h3>Step 1 — <code>Display for Task</code></h3>
<p>Your <code>main.rs</code> builds the list line by hand inside a closure. Move that decision to the type: implement <code>fmt::Display</code> for <code>Task</code> in <code>src/task.rs</code>, producing exactly the format the spec prints — <code>1 [todo] buy milk (medium)</code> — then reduce the <code>list</code> arm to printing each task with <code>{}</code>.</p>
<p>Check: <code>cargo run -- add x</code> still works, and <code>list</code>'s output format has not changed.</p>
<details>
<summary>Stuck for ten minutes on the signature?</summary>
<p>The file needs <code>use std::fmt;</code> at the top. The impl block goes anywhere in <code>task.rs</code>, and the one method is <code>fn fmt(&amp;self, f: &amp;mut fmt::Formatter) -&gt; fmt::Result</code>. The body is a single <code>write!(f, ..)</code> with no semicolon; inside it you can call <code>self.status.label()</code> just as <code>main.rs</code> does now.</p>
</details>
<h3>Step 2 — no <code>unwrap</code>, no panic</h3>
<p>Move the body of <code>main</code> into a second function that returns <code>Result&lt;(), String&gt;</code>, so <code>Command::parse</code>, <code>complete</code> and <code>remove</code> can all be reached with <code>?</code> instead of <code>unwrap</code> and nested matches. <code>main</code> keeps only: collect the args, call it, and deal with the error. While you are there, <code>remove</code> prints nothing today — make it say <code>removed &lt;id&gt;</code>.</p>
<p>Check: <code>grep unwrap src/main.rs</code> finds nothing.</p>
<details>
<summary>Stuck for ten minutes on the shape?</summary>
<p><code>fn run(args: &amp;[String], store: &amp;mut Store) -&gt; Result&lt;(), String&gt;</code>. Its first line can be <code>match Command::parse(args)? { .. }</code> — the <code>?</code> lands on <code>parse</code>, so the match arms deal with <code>Command</code> values, not <code>Result</code>s. Every arm ends in <code>()</code>, and the function's last line is <code>Ok(())</code>. This works with no <code>From</code> impl because every error in play is already <code>String</code>.</p>
</details>
<h3>Step 3 — the contract from 0003</h3>
<p>In <code>main</code>, report the failure on stderr with your own message and exit with status 1. Nothing else changes.</p>
<p>Check — all three must hold:</p>
<pre><code>$ cargo run --quiet -- fly ; echo $?
error: no valid commands
1
$ cargo run --quiet -- done 9 ; echo $?
error: id not found
1
$ cargo run --quiet -- add "buy milk" 2&gt;/dev/null ; echo $?
added task 1
0</code></pre>
<details>
<summary>Stuck for ten minutes on the last two lines?</summary>
<p><code>use std::process;</code> at the top. Then <code>if let Err(e) = run(&amp;args, &amp;mut store) { .. }</code> is enough — inside it, <code>eprintln!("error: {}", e);</code> followed by <code>process::exit(1);</code>. The third check passes automatically once errors leave stdout: redirecting stderr to <code>/dev/null</code> must not swallow real output.</p>
</details>
<h3>Then stop</h3>
<p>Converting <code>String</code> errors into a proper <code>TaskError</code> enum with <code>Display</code>, <code>Error</code> and <code>From</code> is the obvious next move, and it is deliberately <em>not</em> in this drill — it touches all four files and it is the next lesson. Get these three green first.</p>
<h2>The five sentences worth keeping</h2>
<ol>
<li>A <strong>trait</strong> is a list of promised methods; <code>impl Trait for Type</code> is a type keeping that promise.</li>
<li><code>{}</code> is <code>Display</code> and nothing else — you write it by hand, and <code>to_string()</code> comes free with it. <code>{:?}</code> is <code>Debug</code>, which you derive.</li>
<li>A bare <code>&lt;T&gt;</code> can do nothing; <code>&lt;T: Trait&gt;</code> can do exactly what the trait promises.</li>
<li>An <strong>error type</strong> is an enum plus <code>Display</code> plus the empty <code>impl Error</code>; <code>?</code> converts between error types by calling <code>From</code>.</li>
<li>Failures a program can expect belong in <code>Result</code>, on <strong>stderr</strong>, with <strong>exit 1</strong>. <code>unwrap</code> is for the cases you have proved impossible.</li>
</ol>
<footer>
<p><strong>Primary source:</strong> <a href="https://doc.rust-lang.org/stable/book/ch10-02-traits.html">The Rust Book, ch. 10.2 — Traits: Defining Shared Behavior</a>. Read it in full; it is the highest-value chapter left in the book for your mission, because every library you will touch in a backend job (serde, axum, tokio) is a pile of traits. Then skim <a href="https://doc.rust-lang.org/std/error/trait.Error.html">std::error::Error</a> for the two-sentence definition of what an error is.</p>
<p><strong>Previous:</strong> <a href="0004-structs-enums-packages.html">0004 — Structs, enums, packages</a> · <a href="0003-build-a-task-cli.html">0003 — Task CLI project</a> · <strong>Reference:</strong> <a href="../reference/rust-syntax.html">Rust syntax reference</a> (new sections: <a href="../reference/rust-syntax.html#traits">Traits &amp; generics</a>, <a href="../reference/rust-syntax.html#error-types">Error types</a>)</p>
<p><strong>Ask me things.</strong> If a paragraph did not land, say which one — vague explanation is my fault, not yours, and it is far cheaper to fix here than in the middle of the drill. Bring me your compiler errors verbatim; reading them together is the fastest way to make them stop being scary.</p>
</footer>
<script src="../assets/quiz.js"></script>
</body>
</html>
+104
View File
@@ -0,0 +1,104 @@
// The 0006 specification, as executable tests. Do not edit this file — make it pass.
// Copy to: tasks/tests/errors.rs
// Run with: cargo test
use std::error::Error;
use tasks::command::Command;
use tasks::error::TaskError;
use tasks::store::Store;
use tasks::task::Priority;
fn args(list: &[&str]) -> Vec<String> {
list.iter().map(|s| s.to_string()).collect()
}
// ---------- one variant per way of failing ----------
#[test]
fn parse_errors_name_the_exact_failure() {
assert_eq!(Command::parse(&args(&[])).unwrap_err(), TaskError::NoCommand);
assert_eq!(
Command::parse(&args(&["fly"])).unwrap_err(),
TaskError::UnknownCommand("fly".to_string())
);
assert_eq!(
Command::parse(&args(&["add"])).unwrap_err(),
TaskError::MissingTitle
);
assert_eq!(
Command::parse(&args(&["add", "x", "urgent"])).unwrap_err(),
TaskError::BadPriority("urgent".to_string())
);
assert_eq!(
Command::parse(&args(&["done"])).unwrap_err(),
TaskError::MissingId
);
}
#[test]
fn store_reports_which_id_was_missing() {
let mut store = Store::new();
store.add("only task", Priority::Low);
assert_eq!(store.complete(99).unwrap_err(), TaskError::NotFound(99));
assert_eq!(store.remove(7).unwrap_err(), TaskError::NotFound(7));
}
// ---------- Display: the sentence the user reads ----------
#[test]
fn messages_are_lowercase_and_unpunctuated() {
assert_eq!(TaskError::NoCommand.to_string(), "no command given");
assert_eq!(
TaskError::UnknownCommand("fly".to_string()).to_string(),
"unknown command: fly"
);
assert_eq!(TaskError::MissingTitle.to_string(), "add needs a title");
assert_eq!(
TaskError::MissingId.to_string(),
"this command needs a task id"
);
assert_eq!(
TaskError::BadPriority("urgent".to_string()).to_string(),
"unknown priority: urgent"
);
assert_eq!(TaskError::NotFound(9).to_string(), "no task with id 9");
}
// ---------- From + ? : the parse error is wrapped, not thrown away ----------
#[test]
fn a_bad_id_wraps_the_parse_error() {
let err = Command::parse(&args(&["done", "abc"])).unwrap_err();
assert!(matches!(err, TaskError::BadId(_)), "expected BadId");
assert!(
Command::parse(&args(&["remove", "-1"])).is_err(),
"a negative id is not a u32"
);
}
#[test]
fn a_wrapped_error_is_reported_once_and_reachable() {
let err = Command::parse(&args(&["done", "abc"])).unwrap_err();
// Display says your sentence and does not repeat std's.
assert_eq!(err.to_string(), "task id must be a number");
// std's sentence is still reachable, through Error::source().
let inner = err.source().expect("BadId must expose its source");
assert_eq!(inner.to_string(), "invalid digit found in string");
}
#[test]
fn errors_that_carry_nothing_have_no_source() {
assert!(TaskError::NoCommand.source().is_none());
assert!(TaskError::NotFound(1).source().is_none());
}
// ---------- the Error trait, and the bounds the ecosystem expects ----------
#[test]
fn task_error_is_a_real_error() {
fn assert_usable_as_error<E: Error + Send + Sync + 'static>() {}
assert_usable_as_error::<TaskError>();
let boxed: Box<dyn Error> = Box::new(TaskError::NotFound(1));
assert_eq!(boxed.to_string(), "no task with id 1");
}
+512
View File
@@ -0,0 +1,512 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>0006 — Your own error type</title>
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body>
<h1>Your own error type</h1>
<p class="subtitle">Lesson 0006 · after <a href="0005-traits-display-and-errors.html">0005</a> · reading, then a 25-minute drill against a shipped test file · ~40 minutes</p>
<div class="callout">
Every code block and every compiler message on this page was produced by running it, in a scratch project, today.
Nothing is written from memory. The demo domain is a config loader — your project is a task CLI, so nothing here
pastes in. Translating is the work.
</div>
<h2>Where 0005 left you</h2>
<p>The drill landed: <code>Display for Task</code>, no <code>unwrap</code>, <code>Command::parse(args)?</code>,
<code>eprintln!</code> + <code>process::exit(1)</code>, all 17 tests still green. The contract 0003 asked for and never
got is now real code.</p>
<p>So look at what is left. Two lines in two different files:</p>
<pre><code>// src/command.rs — the user typed `done` with no id after it
let id = args.get(1).ok_or("id not found")?;
// src/store.rs — the user typed `done 9`, and task 9 does not exist
Err("id not found".to_string())</code></pre>
<p>Two unrelated failures, one identical sentence. A caller cannot tell them apart, and neither can you at 3am.
There is a third: <code>Err(String::from("no valid commands"))</code> throws away the word the user actually typed,
so your CLI can never say <em>which</em> command it did not recognise.</p>
<p>That is the last thing standing between <code>tasks</code> and a crate you would show an interviewer. Today it
becomes one enum and three traits.</p>
<h2>Part 1 — What a <code>String</code> error costs</h2>
<p>Three costs, and they are not stylistic.</p>
<table>
<tr><th>With <code>String</code></th><th>With an enum</th></tr>
<tr>
<td>The caller gets prose. To react differently per failure it must <em>match on text</em> — <code>if msg == "id not found"</code> — and that breaks the day you fix a typo.</td>
<td>The caller matches on a variant. The compiler checks the arms.</td>
</tr>
<tr>
<td>The data is gone. <code>format!("no task with id {id}")</code> flattens the id into text; nothing downstream can use it.</td>
<td>The value rides along — <code>NotFound(9)</code> — and the sentence is built at the edge, where the human is.</td>
</tr>
<tr>
<td>A misspelled message compiles.</td>
<td>A misspelled variant does not.</td>
</tr>
</table>
<p>This is not just taste; it is the published guideline for the language:</p>
<blockquote>
<p>"Error types should always implement the <code>std::error::Error</code> trait… Never use <code>()</code> as an error
type, even where there is no useful additional information for the error to carry… The error message given by the
<code>Display</code> representation of an error type should be lowercase without trailing punctuation, and typically
concise."</p>
</blockquote>
<p class="cite">Rust API Guidelines: <a href="https://rust-lang.github.io/api-guidelines/interoperability.html#error-types-are-meaningful-and-well-behaved-c-good-err">C-GOOD-ERR</a></p>
<p>Note the lowercase rule — that is why <code>"invalid digit found in string"</code>, straight from
<code>std</code>, has no capital and no full stop. Your messages will match that style, and the shipped tests check
it.</p>
<h2>Part 2 — The one new trait method: <code>source()</code></h2>
<p>0005 gave you the recipe: <code>#[derive(Debug)]</code>, then <code>Display</code>, then the empty
<code>impl Error</code>, plus <code>From</code> so a bare <code>?</code> converts. That is 90% of today's drill and
you already have it.</p>
<p>Here is the 10% that is new, and it is the part that makes wrapped errors behave. When one of your variants
carries another error — <code>BadPort(ParseIntError)</code> — you now have two sentences for one failure: yours and
<code>std</code>'s. The <code>Error</code> trait has a slot for the inner one:</p>
<pre><code>pub trait Error: Debug + Display {
fn source(&self) -&gt; Option&lt;&amp;(dyn Error + 'static)&gt; { ... }
// …plus deprecated description() / cause(), which you never implement
}</code></pre>
<p class="cite">std: <a href="https://doc.rust-lang.org/std/error/trait.Error.html">std::error::Error</a> — note the
supertraits: <code>Debug + Display</code> is a <em>requirement of the trait itself</em>, the same bound idea as
0005's <code>&lt;T: Reading&gt;</code>, applied to a trait instead of a function.</p>
<p>The rule that goes with it is one sentence, and it is easy to get wrong:</p>
<blockquote>
<p>"In error types that wrap an underlying error, the underlying error should be either returned by the outer error's
<code>Error::source()</code>, or rendered by the outer error's <code>Display</code> implementation, but not both."</p>
</blockquote>
<p class="cite">std: <a href="https://doc.rust-lang.org/std/error/trait.Error.html#error-source">Error source</a></p>
<p>So: say your sentence in <code>Display</code>, hand the cause to <code>source()</code>, and never print both.
Here is the whole pattern in a config loader — <code>Option</code> in, <code>u16</code> out, two ways to fail:</p>
<pre><code>#[derive(Debug)]
enum ConfigError {
Missing(String),
BadPort(ParseIntError),
}
impl fmt::Display for ConfigError {
fn fmt(&amp;self, f: &amp;mut fmt::Formatter&lt;'_&gt;) -&gt; fmt::Result {
match self {
ConfigError::Missing(key) =&gt; write!(f, "missing setting: {}", key),
// no `{e}` on the next arm - the port text is not worth echoing
ConfigError::BadPort(_) =&gt; write!(f, "port must be a number"),
}
}
}
impl Error for ConfigError {
fn source(&amp;self) -&gt; Option&lt;&amp;(dyn Error + 'static)&gt; {
match self {
ConfigError::BadPort(e) =&gt; Some(e), // the cause lives here instead
ConfigError::Missing(_) =&gt; None, // nothing underneath this one
}
}
}
impl From&lt;ParseIntError&gt; for ConfigError {
fn from(e: ParseIntError) -&gt; ConfigError { ConfigError::BadPort(e) }
}
fn port(text: Option&lt;&amp;str&gt;) -&gt; Result&lt;u16, ConfigError&gt; {
let text = text.ok_or(ConfigError::Missing("port".to_string()))?;
Ok(text.parse()?) // ParseIntError -&gt; ConfigError, via From
}</code></pre>
<pre><code>1. Ok(8080)
2. port must be a number
3. Some("invalid digit found in string")
4. missing setting: port
5. None</code></pre>
<p>Line 2 is your sentence. Line 3 is <code>std</code>'s, reached through <code>source()</code> — still available for
a log or a <code>--verbose</code> flag, not shoved in the user's face. Lines 4–5: a variant with nothing underneath
it returns <code>None</code>, and that is not a gap, it is the answer.</p>
<p>Read <code>Option&lt;&amp;(dyn Error + 'static)&gt;</code> as "maybe a reference to some error, whatever type it
is" — the <code>dyn</code> from 0005's <code>Box&lt;dyn Error&gt;</code>, borrowed instead of boxed. Copy the
signature; it is not worth memorising.</p>
<h3>What the enum buys the caller</h3>
<p>Now the payoff that a <code>String</code> can never give you — recovering from <em>one</em> failure and staying
fatal on the rest:</p>
<pre><code>fn port_or_default(text: Option&lt;&amp;str&gt;) -&gt; Result&lt;u16, ConfigError&gt; {
match port(text) {
Err(ConfigError::Missing(_)) =&gt; Ok(8080), // recover from ONE variant
other =&gt; other, // every other failure stays fatal
}
}</code></pre>
<pre><code>6. Ok(8080)
7. Err("port must be a number")</code></pre>
<p>A missing setting falls back to a default; a typo'd one still fails. Try writing that against
<code>Result&lt;u16, String&gt;</code> — you would be comparing prose. This is the shape of every real config loader,
retry policy, and HTTP status decision you will write in a backend job.</p>
<h2>Part 3 — What the compiler starts doing for you</h2>
<p>An enum is a <em>closed</em> set, and the compiler knows all of it. Add a variant to a shipped error type:</p>
<pre><code>enum TaskError {
…
NotFound(u32),
StoreFull, // new today
}</code></pre>
<pre><code>error[E0004]: non-exhaustive patterns: `&amp;TaskError::StoreFull` not covered
--&gt; src/error.rs:19:15
|
19 | match self {
| ^^^^ pattern `&amp;TaskError::StoreFull` not covered
|
note: `TaskError` defined here
--&gt; src/error.rs:6:10
|
6 | pub enum TaskError {
| ^^^^^^^^^
...
14 | StoreFull,
| --------- not covered
= note: the matched value is of type `&amp;TaskError`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern
or an explicit pattern as shown</code></pre>
<p>That is the answer to the question you missed in 0005's quiz, delivered by the compiler: <strong>adding a
failure mode makes the build fail everywhere the new case is unhandled.</strong> With a <code>String</code>, adding
a failure mode is silent — you find out in production. This is also the argument for <em>not</em> reaching for
<code>_ =&gt; …</code> in a <code>match</code> on your own error type: the wildcard throws the guarantee away.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch06-02-match.html">6.2 <code>match</code></a>
(exhaustiveness) · <a href="https://doc.rust-lang.org/stable/book/ch09-02-recoverable-errors-with-result.html">9.2 <code>?</code> and <code>From</code></a></p>
<h3>The three errors you will meet during the migration</h3>
<p>Not hypotheticals — I ran your crate with each mistake in place. Recognise them and each costs you ten seconds
instead of ten minutes.</p>
<p><strong>1. You wrote the new module but never declared it.</strong></p>
<pre><code>error[E0432]: unresolved import `crate::error`
--&gt; src/command.rs:1:12
|
1 | use crate::error::TaskError;
| ^^^^^ unresolved import</code></pre>
<p>A file in <code>src/</code> is not a module until a <code>mod</code> declaration names it. Add
<code>pub mod error;</code> to <code>src/lib.rs</code>. (Chapter 7, still true.)</p>
<p><strong>2. You changed the signature but left an old <code>String</code> behind.</strong></p>
<pre><code>error[E0308]: mismatched types
--&gt; src/store.rs:37:13
|
37 | Err("id not found".to_string())
| --- ^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `TaskError`, found `String`
| |
| arguments to this enum variant are incorrect</code></pre>
<p>This is the migration working as intended: the compiler is listing your remaining <code>String</code> errors one
at a time. Follow it until it stops.</p>
<p><strong>3. You used <code>?</code> on <code>parse()</code> with no <code>From</code> impl.</strong></p>
<pre><code>error[E0271]: type mismatch resolving `&lt;u32 as FromStr&gt;::Err == TaskError`
--&gt; src/command.rs:31:43
|
31 | Ok(Command::Done { id: id.parse()? })
| ^^^^^ expected `TaskError`, found `ParseIntError`</code></pre>
<p>0005 predicted exactly this: when the target type comes from context rather than a turbofish, the missing
<code>From</code> is reported as <code>E0271</code> instead of <code>E0277</code>. Same meaning, same fix — write
<code>impl From&lt;ParseIntError&gt; for TaskError</code>.</p>
<h2>Retrieval — before the drill</h2>
<p>From memory. Scrolling up first is the one way to waste these. Two questions are about older material on
purpose — interleaving is what makes any of it stick.</p>
<div class="q" data-type="recall" data-topic="Enums">
<p class="topic">Enums</p>
<p class="prompt">You add a variant to an error enum that is matched in four places. What does the compiler do, and what does the equivalent change to a <code>String</code> error do?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">The build fails at every <code>match</code> that does not cover the new variant — <code>error[E0004]: non-exhaustive patterns</code> — so the compiler hands you the exact list of places to update. Adding a new failure message to a <code>String</code> error changes nothing at compile time; every caller keeps compiling and silently mishandles the new case. This is why <code>_ =&gt; …</code> on your own error type is a mistake: it opts out of the guarantee.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="mcq" data-topic="Error handling">
<p class="topic">Error handling</p>
<p class="prompt">Your variant <code>BadId(ParseIntError)</code> wraps another error. Where should <code>std</code>'s message appear?</p>
<div class="options">
<button class="opt" data-correct="false">In your Display output, and also from source()</button>
<button class="opt" data-correct="true">From source() only, not in your Display output</button>
<button class="opt" data-correct="false">In neither, since the outer message replaces it</button>
</div>
<div class="explain hidden">std's own words: the underlying error "should be either returned by the outer error's <code>Error::source()</code>, or rendered by the outer error's <code>Display</code> implementation, but not both." Choose <code>source()</code>: the report stays one clean sentence, and a log or a <code>--verbose</code> flag can still dig out the cause.</div>
</div>
<div class="q" data-type="recall" data-topic="Traits">
<p class="topic">Traits</p>
<p class="prompt">The trait is declared <code>pub trait Error: Debug + Display</code>. What are those two names doing there, and what happens if your type has neither?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">They are supertraits — bounds on the trait itself: you may only implement <code>Error</code> for a type that already implements <code>Debug</code> and <code>Display</code>. Without them, <code>impl Error for MyError {}</code> fails with <code>E0277: unsatisfied trait bound</code> (0005 showed the real output). It is the same mechanism as <code>&lt;T: Reading&gt;</code> on a function, pointed at a trait.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="recall" data-topic="Generics">
<p class="topic">Generics</p>
<p class="prompt">What does this test actually assert, given that its body is empty? <code>fn assert_usable_as_error&lt;E: Error + Send + Sync + 'static&gt;() {}</code> then <code>assert_usable_as_error::&lt;TaskError&gt;();</code></p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Nothing at runtime — it is a compile-time assertion. Naming <code>TaskError</code> as the type argument forces the compiler to check every bound, so the test fails to build if <code>TaskError</code> stops implementing <code>Error</code>, or stops being <code>Send</code>/<code>Sync</code>. The API guidelines ask for exactly those bounds, because an error that is not <code>Send</code> cannot cross a thread boundary — which every web framework does.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="mcq" data-topic="Ownership">
<p class="topic">Ownership</p>
<p class="prompt">Why does <code>UnknownCommand(String)</code> hold an owned <code>String</code> rather than a borrowed <code>&amp;str</code> taken from the argument list?</p>
<div class="options">
<button class="opt" data-correct="false">Because a borrowed str cannot be printed by Display</button>
<button class="opt" data-correct="true">Because the error outlives the args it was built from</button>
<button class="opt" data-correct="false">Because String is faster to match against than str</button>
</div>
<div class="explain hidden">The error is returned upward, out of the function that borrowed the argument slice. A <code>&amp;str</code> in the variant would need a lifetime parameter — <code>TaskError&lt;'a&gt;</code> — infecting every signature that mentions it. Owning one short string at the moment of failure is the cheap, boring answer. This is chapter 4 deciding your API shape again, and it is why you have not needed chapter 10.3 yet.</div>
</div>
<div class="q" data-type="recall" data-topic="Modules &amp; paths">
<p class="topic">Modules &amp; paths</p>
<p class="prompt">You create <code>src/error.rs</code> and <code>store.rs</code> says <code>use crate::error::TaskError;</code>. It fails with <code>E0432: unresolved import</code>. Why, and what is the fix?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">A file in <code>src/</code> is not part of the crate until something declares it. Add <code>pub mod error;</code> to <code>src/lib.rs</code> (<code>pub</code> because <code>main.rs</code> is a separate crate and needs to name the type too). <code>use</code> only creates a shortcut to a path that already exists — it never brings a file into the module tree.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div id="summary"><div id="summary-body"></div><p id="summary-total"></p>
<button id="report-btn">Copy report</button><pre id="report-output" class="hidden"></pre></div>
<h2>The drill — 25 minutes, your own crate</h2>
<p>Type it, do not paste it. The config loader above is a different program; none of it fits
<code>tasks</code> unchanged. Keep the <a href="../reference/rust-syntax.html#error-types">syntax reference</a> open —
looking syntax up is free, copying answers is not.</p>
<p>This time the feedback loop is a test file, like 0003. Install it first:</p>
<pre><code>cd ~/learn-rust/tasks
cp ../lessons/0006-errors-spec.rs tests/errors.rs
cargo test # 7 new tests fail to compile — that is the starting line</code></pre>
<p>Do not edit <code>tests/errors.rs</code>. Do not edit <code>tests/spec.rs</code> either: all 17 must still pass,
because the library's <em>behaviour</em> is not changing today — only the type it reports failures with. Target at
the end: <strong>24 passing</strong>.</p>
<h3>Step 1 — <code>src/error.rs</code></h3>
<p>New file, new module. The tests name the variants, so this shape is fixed:</p>
<pre><code>pub enum TaskError {
NoCommand, // no arguments at all
UnknownCommand(String), // the word the user actually typed
MissingTitle, // `add` with nothing after it
MissingId, // `done` / `remove` with nothing after it
BadPriority(String), // the priority word that was not low/medium/high
BadId(ParseIntError), // `done abc` — wraps std's parse failure
NotFound(u32), // the id that was not in the store
}</code></pre>
<p>Write the four things it needs: the derive, <code>Display</code>, <code>impl Error</code> with
<code>source()</code>, and <code>From&lt;ParseIntError&gt;</code>. Two derives are required —
<code>Debug</code> because <code>Error</code> demands it, and <code>PartialEq</code> because the tests compare
variants with <code>assert_eq!</code>. Miss the second and you get
<code>error[E0369]: binary operation == cannot be applied to type TaskError</code>.</p>
<p>The messages are part of the contract — the tests compare them exactly. Lowercase, no trailing punctuation,
per C-GOOD-ERR:</p>
<table>
<tr><th>Variant</th><th><code>to_string()</code> must be</th></tr>
<tr><td><code>NoCommand</code></td><td><code>no command given</code></td></tr>
<tr><td><code>UnknownCommand("fly")</code></td><td><code>unknown command: fly</code></td></tr>
<tr><td><code>MissingTitle</code></td><td><code>add needs a title</code></td></tr>
<tr><td><code>MissingId</code></td><td><code>this command needs a task id</code></td></tr>
<tr><td><code>BadPriority("urgent")</code></td><td><code>unknown priority: urgent</code></td></tr>
<tr><td><code>BadId(..)</code></td><td><code>task id must be a number</code></td></tr>
<tr><td><code>NotFound(9)</code></td><td><code>no task with id 9</code></td></tr>
</table>
<p><strong>Check:</strong> <code>cargo build</code> compiles the library, with <code>pub mod error;</code> added to
<code>src/lib.rs</code>.</p>
<details>
<summary>Stuck for ten minutes on <code>source()</code>?</summary>
<p>Imports: <code>use std::error::Error;</code>, <code>use std::fmt;</code>,
<code>use std::num::ParseIntError;</code>. The method signature is
<code>fn source(&amp;self) -&gt; Option&lt;&amp;(dyn Error + 'static)&gt;</code> — copy it from Part 2, it is not worth
deriving. Exactly one variant returns
<code>Some(e)</code>; a <code>_ =&gt; None</code> arm is acceptable here because you are matching to find one case,
not to handle every case.</p>
</details>
<h3>Step 2 — <code>command.rs</code>: <code>Result&lt;Command, TaskError&gt;</code></h3>
<p>Change the signature, then let the compiler walk you through the five <code>ok_or</code> / <code>Err</code> sites.
Two of them get better: the <code>_ =&gt;</code> arm can now name the word it rejected, and both
<code>match id.parse() { Ok(n) =&gt; n, Err(_) =&gt; return Err(..) }</code> blocks collapse to
<code>id.parse()?</code> — that is what the <code>From</code> impl was for.</p>
<p><strong>Check:</strong> <code>cargo test --test errors parse_errors_name_the_exact_failure</code> passes, and
<code>grep -c "match id.parse" src/command.rs</code> prints <code>0</code>.</p>
<details>
<summary>Stuck on the unknown-command arm?</summary>
<p>The match arm <code>_ =&gt; …</code> discards the value it matched. Bind it instead:
<code>other =&gt; Err(TaskError::UnknownCommand(other.to_string()))</code>. <code>other</code> is a
<code>&amp;str</code> (you matched on <code>.as_str()</code>), and the variant holds a <code>String</code> — see the
Ownership question above for why.</p>
</details>
<h3>Step 3 — <code>store.rs</code>: report <em>which</em> id</h3>
<p>Both error returns become <code>TaskError::NotFound(id)</code>. Nothing else in the file changes.</p>
<p><strong>Check:</strong> <code>cargo test --test errors store_reports_which_id_was_missing</code> passes.</p>
<h3>Step 4 — <code>main.rs</code>: the edge</h3>
<p><code>run</code> now returns <code>Result&lt;(), TaskError&gt;</code>. Everything else you already wrote in 0005
stays exactly as it is — <code>eprintln!("error: {}", e)</code> keeps working because <code>Display</code> is
implemented, and that is the whole point of the trait.</p>
<p><strong>Check — the observable behaviour of your CLI:</strong></p>
<pre><code>$ cargo run --quiet -- fly ; echo $?
error: unknown command: fly
1
$ cargo run --quiet -- done abc ; echo $?
error: task id must be a number
1
$ cargo run --quiet -- done 9 ; echo $?
error: no task with id 9
1
$ cargo run --quiet ; echo $?
error: no command given
1
$ cargo run --quiet -- add "buy milk" 2&gt;/dev/null ; echo $?
added task 1
0
$ cargo test
… 17 passed … 7 passed …</code></pre>
<p>Compare the first three lines with what your CLI said an hour ago — <code>no valid commands</code>,
<code>id not found</code>, <code>id not found</code>. Same code paths, same exit codes; the errors now name the
thing that went wrong. That is the whole return on one enum.</p>
<h3>Then stop</h3>
<p>Persistence is the obvious next move and it is deliberately not here: reading and writing a file brings
<code>fs</code>, <code>io::Error</code>, a second <code>From</code> impl, and turning a line of text back into a
<code>Task</code>. That is lesson 0007, and it is much easier once <code>TaskError</code> exists to convert
<em>into</em>.</p>
<h2>What you are missing, measured</h2>
<p>You asked not to miss anything important, so I mapped this workspace against the book's real table of contents
rather than my memory of it: <strong><a href="../reference/book-coverage.html">the coverage map</a></strong>. Read it
once — it is the shortest honest answer to "where am I?".</p>
<p>The summary: chapters 1–7, 9, and 10.2 are <em>produced</em>, not just read. Three genuine gaps stand between you
and a job-ready floor, in the order I intend to teach them:</p>
<ol>
<li><strong>ch 12 — files and <code>io::Error</code></strong>: your CLI forgets everything on exit (lesson 0007).</li>
<li><strong>ch 8 + 13 — <code>HashMap</code>, <code>map</code>/<code>filter</code>/<code>collect</code></strong>: your
weakest measured area, and the most common shape in real Rust code.</li>
<li><strong>ch 11 — writing tests</strong>: you have consumed 24 of my tests and written none. A take-home will ask
you to produce them.</li>
</ol>
<p>Lifetimes (ch 10.3) are untouched, and that is fine for now — you have dodged them by owning your data, which is
the right call in a CLI. They become unavoidable when you read other people's code.</p>
<h2>Take it outside</h2>
<p>Once the 24 tests are green, <code>tasks</code> is a small, complete, idiomatic crate — the first thing in this
workspace worth showing to strangers. The highest-value thing you can do with it is ask people who write Rust daily
whether your error type is idiomatic:</p>
<ul>
<li><a href="https://users.rust-lang.org">users.rust-lang.org</a> — the official forum, "Code Review" category.
Paste <code>error.rs</code> and ask specifically: is one enum for both parse and store failures right, or should
those be two types with a wrapping variant? That is a genuine design question with a real answer, and it is the kind
of thing a reviewer will teach you in one reply.</li>
<li><a href="https://reddit.com/r/rust">r/rust</a> — faster, noisier; good for "is this idiomatic?" checks.</li>
</ul>
<p>Two things you will likely hear back, and both are worth knowing in advance: real crates often reach for
<a href="https://docs.rs/thiserror"><code>thiserror</code></a> to derive exactly the <code>Display</code>/<code>From</code>
code you just wrote by hand, and applications often use
<a href="https://docs.rs/anyhow"><code>anyhow</code></a> instead of an enum. Both are correct advice, and both are
the wrong place to start — you cannot judge a macro that writes an <code>Error</code> impl until you have written
one yourself. Today's version is the one that teaches; reach for the crates on your second real project.</p>
<h2>The five sentences worth keeping</h2>
<ol>
<li>An error type is an enum with one variant per way of failing, carrying the data that failed.</li>
<li><code>Display</code> is the sentence a human reads: lowercase, no trailing punctuation, no repeat of the cause.</li>
<li><code>source()</code> is where a wrapped error goes — either <code>source()</code> or <code>Display</code>, never both.</li>
<li><code>From&lt;Cause&gt; for MyError</code> is what makes a bare <code>?</code> convert; missing, it reports as
<code>E0277</code> or <code>E0271</code> depending on how the target type was named.</li>
<li>The compiler's exhaustiveness check is the real reason for an enum: adding a failure mode breaks the build,
not production.</li>
</ol>
<footer>
<p><strong>Primary source:</strong> re-read
<a href="https://doc.rust-lang.org/stable/book/ch09-02-recoverable-errors-with-result.html">The Rust Book 9.2 —
Recoverable Errors with <code>Result</code></a>, now that you have written the type it describes; then the two-screen
<a href="https://doc.rust-lang.org/std/error/trait.Error.html">std::error::Error</a> page, which is the definition of
what an error <em>is</em>. If you read one third thing, read
<a href="https://rust-lang.github.io/api-guidelines/interoperability.html#error-types-are-meaningful-and-well-behaved-c-good-err">C-GOOD-ERR</a>
— it is the checklist an experienced reviewer applies to your error type.</p>
<p>Previous: <a href="0005-traits-display-and-errors.html">0005 — Traits, Display, errors</a> ·
<a href="0004-structs-enums-packages.html">0004 — Structs, enums, packages</a> ·
<a href="0003-build-a-task-cli.html">0003 — Task CLI project</a><br />
Reference: <a href="../reference/rust-syntax.html">Rust syntax reference</a> ·
<a href="../reference/book-coverage.html">Coverage map</a></p>
<p><strong>Ask me things.</strong> Bring me the compiler output verbatim — especially in step 2, where the error type
changes under five call sites at once. If a paragraph did not land, name it; vague explanation is my fault, not
yours, and it is far cheaper to fix here than in the middle of the drill.</p>
</footer>
<script src="../assets/quiz.js"></script>
</body>
</html>
+588
View File
@@ -0,0 +1,588 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>0007 — Files, io::Error, and FromStr</title>
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body>
<h1>Files, <code>io::Error</code>, and <code>FromStr</code></h1>
<p class="subtitle">Lesson 0007 · after <a href="0006-your-own-error-type.html">0006</a> · reading, then a 30-minute drill against a shipped test file · ~45 minutes</p>
<div class="callout">
Every code block and every compiler message on this page was produced by running it today, in a scratch project.
Nothing is written from memory. The demo domain is a weather log — your project is a task CLI, so nothing here
pastes in. Translating is the work.
</div>
<h2>Where 0006 left you, and the one loose end</h2>
<p>Twenty-four tests are green, and the error type behind them is real work: <code>TaskError</code> has seven
variants, each with its own <code>Display</code> sentence, a <code>source()</code> that hands back the one wrapped
cause, and an <code>impl From&lt;ParseIntError&gt;</code>. That is the full set of obligations for a std-compatible
error type, and you wrote all of it from the signatures up.</p>
<p>There is one loose end, and it is worth looking at closely before adding anything new. The <code>From</code>
impl you wrote is never actually called. Over in <code>command.rs</code>, the conversion is still done by hand —
once in the <code>done</code> arm, once in <code>remove</code>:</p>
<pre><code>let id: u32 = match id.parse() {
Ok(n) =&gt; n,
Err(e) =&gt; return Err(TaskError::BadId(e)), // this IS From::from, typed out
};</code></pre>
<p>Compare that with what <code>From</code> exists to do. Your impl says "given a <code>ParseIntError</code>,
build a <code>TaskError::BadId</code>" — which is exactly what the <code>Err</code> arm above says, in five lines
instead of zero. The impl is correct; it simply never gets reached, because <code>match</code> handles the error
before <code>?</code> would have had a chance to convert it. So the mechanism was learned and the reflex was not,
which is the most common way a Rust concept half-lands.</p>
<p>Step 0 of today's drill deletes both blocks. Then today adds a <em>second</em> <code>From</code> impl, and this
one you will not be able to route around by hand: the shape the code needs makes <code>?</code> the only
reasonable option, and the compiler stops the build until the impl exists.</p>
<h2>Part 1 — A file is a <code>String</code> that can fail</h2>
<p>A save file is, from Rust's point of view, nothing more exotic than a <code>String</code> that might not arrive.
Two functions in <a href="https://doc.rust-lang.org/std/fs/">std::fs</a> cover everything a CLI of this size needs.
Each one opens the file, does the work, and closes it again, all inside the single call — there is no handle to keep
track of and nothing to remember to close:</p>
<pre><code>fn read_to_string&lt;P: AsRef&lt;Path&gt;&gt;(path: P) -&gt; io::Result&lt;String&gt;
fn write&lt;P: AsRef&lt;Path&gt;, C: AsRef&lt;[u8]&gt;&gt;(path: P, contents: C) -&gt; io::Result&lt;()&gt;</code></pre>
<p class="cite">std: <a href="https://doc.rust-lang.org/std/fs/fn.read_to_string.html">fs::read_to_string</a> ·
<a href="https://doc.rust-lang.org/std/fs/fn.write.html">fs::write</a></p>
<p>The return types look unfamiliar, so take them apart before going further.
<code>io::Result&lt;T&gt;</code> is not a new kind of <code>Result</code>; it is
<a href="https://doc.rust-lang.org/std/io/type.Result.html">a type alias</a>, declared in std as
<code>type Result&lt;T&gt; = std::result::Result&lt;T, io::Error&gt;</code>. In other words the error half has already been
filled in for you, because every function in that module fails the same way. Whenever you meet
<code>io::Result&lt;String&gt;</code> in a signature, read it silently as <code>Result&lt;String, io::Error&gt;</code> and
carry on — it is the same <code>Result</code> you have been matching on since chapter 9, and <code>?</code> works
on it exactly as you would expect.</p>
<p>(The <code>P: AsRef&lt;Path&gt;</code> in the signature is a convenience bound, and you can read past it for now.
All it means is that you may pass a <code>&amp;str</code>, a <code>String</code>, a <code>&amp;Path</code>, or a
<code>PathBuf</code>, and std will accept any of them. It is the same idea as the trait bounds from 0005, used to
widen what a function will take.)</p>
<p>The interesting half is <code>io::Error</code>. Unlike <code>ParseIntError</code>, which really only means "that
was not a number", an <code>io::Error</code> stands for dozens of distinct situations: the file does not exist, the
path is a directory rather than a file, the process lacks permission to read it, the disk is full, the name is too
long. All of those arrive as the same type, so the type alone cannot tell you what went wrong. You separate them by
asking the value, using <a href="https://doc.rust-lang.org/std/io/enum.ErrorKind.html"><code>e.kind()</code></a>,
which returns a variant of the <code>ErrorKind</code> enum:</p>
<pre><code>let missing = fs::read_to_string("/tmp/definitely-not-here.txt");
println!("{:?}", missing.map_err(|e| e.kind()));</code></pre>
<pre><code>missing -&gt; Err(NotFound)</code></pre>
<p>Keep that <code>NotFound</code> in mind. It looks like a small detail, but it turns into the most important
design decision of the whole lesson, and Part 4 comes back to it: on the very first run of your CLI, the save file
legitimately does not exist yet, and how you treat that one kind decides whether a fresh install works or looks
broken.</p>
<h2>Part 2 — The second <code>From</code>, and why it is legal</h2>
<p>You asked, after the last lesson, whether a type can have two <code>From</code> impls. The answer decides how
today's code is shaped, so here is the rule stated precisely: <strong>for any given source type <code>T</code>,
there may be exactly one <code>impl From&lt;T&gt; for YourType</code> in the whole program.</strong> Write a second
one for the same <code>T</code> and the compiler stops you with
<code>error[E0119]: conflicting implementations</code>.</p>
<p>The reason is worth holding on to, because it explains a lot of Rust's trait rules. At the moment you write
<code>value?</code>, the only information the compiler has is the pair of types involved: it is converting a
<code>ParseIntError</code> into a <code>TaskError</code>. Nothing at that call site records what you <em>meant</em>
by the failure. If two impls existed for that pair, there would be two possible answers and no way to choose
between them, so the language forbids the ambiguity up front rather than picking one for you.</p>
<p>Nothing stops you from adding an impl for a <em>different</em> source type, though, and that is what today
needs — one conversion for parse failures, and a new one for file failures:</p>
<pre><code>impl From&lt;ParseIntError&gt; for TaskError { .. } // T = ParseIntError (0006)
impl From&lt;io::Error&gt; for TaskError { .. } // T = io::Error (0007)</code></pre>
<p>Write <code>fs::write(path, out)?</code> before the impl exists and the compiler tells you exactly what is
missing. Real output:</p>
<pre><code>error[E0277]: `?` couldn't convert the error to `TaskError`
--&gt; src/store.rs:82:29
|
82 | fs::write(path, out)?;
| --------------------^ the trait `From&lt;std::io::Error&gt;` is not implemented for `TaskError`
| |
| this can't be annotated with `?` because it has type `Result&lt;_, std::io::Error&gt;`
|
note: `TaskError` needs to implement `From&lt;std::io::Error&gt;`
= note: the question mark operation (`?`) implicitly performs a conversion
on the error value using the `From` trait</code></pre>
<p>The final note in that message is the part worth reading twice. <code>?</code> has no special knowledge of
<code>io</code>, and it is not a built-in shortcut for file handling: it simply calls <code>From::from</code> on
whatever error it is given. It is the identical mechanism that has been quietly converting your
<code>ParseIntError</code> into a <code>BadId</code> since 0006. Once you see <code>?</code> as "return early, and
run the error through <code>From</code> on the way out", every one of these messages becomes predictable rather
than mysterious.</p>
<h2>Part 3 — <code>FromStr</code>: the trait behind <code>.parse()</code></h2>
<p>You have been calling <code>.parse()</code> since the guessing game, and it has probably felt like a built-in
piece of string handling. It is not. It is a trait method, and once you see the two declarations behind it, the
whole thing stops being magic:</p>
<pre><code>impl str {
pub fn parse&lt;F: FromStr&gt;(&amp;self) -&gt; Result&lt;F, F::Err&gt; { .. }
}
pub trait FromStr: Sized {
type Err; // an ASSOCIATED TYPE
fn from_str(s: &amp;str) -&gt; Result&lt;Self, Self::Err&gt;;
}</code></pre>
<p class="cite">std: <a href="https://doc.rust-lang.org/std/str/trait.FromStr.html">FromStr</a> ·
Book: <a href="https://doc.rust-lang.org/stable/book/ch20-02-advanced-traits.html">20.2 — associated types</a></p>
<p>Read the two together. <code>"42".parse::&lt;u32&gt;()</code> works for exactly one reason: somewhere in std there
is an <code>impl FromStr for u32</code>, and it declares <code>type Err = ParseIntError</code>. There is no
special case for integers in the language. That means the door is open to you — implement the same trait for your
own type and <code>.parse()</code> begins working on it immediately. This is a genuinely different move from
writing a <code>Task::from_line</code> helper of your own. A helper is a function only your code knows about;
implementing the trait means your type joins an interface that std and every other crate already speak, so any
generic function taking <code>F: FromStr</code> will now accept a <code>Task</code> as well.</p>
<p>The unfamiliar line is <code>type Err;</code>, and it deserves its own paragraph because it is your first
<strong>associated type</strong>. Think of it as a slot in the trait that the <em>implementor</em> fills in, once,
and permanently — as opposed to a generic parameter, which the <em>caller</em> chooses at each call site. That
distinction is exactly why <code>ParseIntError</code> appears nowhere in <code>parse()</code>'s signature: the
signature says <code>F::Err</code>, meaning "whatever error type <code>F</code> declared when it implemented the
trait". When you write <code>type Err = TaskError</code> in your impl, you are filling that slot for
<code>Task</code>, and from then on <code>line.parse::&lt;Task&gt;()</code> is known to return
<code>Result&lt;Task, TaskError&gt;</code> without anyone having to say so again.</p>
<p>Leave the slot out and the compiler is explicit about the missing piece:</p>
<pre><code>error[E0046]: not all trait items implemented, missing: `Err`
--&gt; src/task.rs:103:1
|
103 | impl FromStr for Task {
| ^^^^^^^^^^^^^^^^^^^^^ missing `Err` in implementation
|
= help: implement the missing item: `type Err = /* Type */;`</code></pre>
<p>And call <code>.parse::&lt;Task&gt;()</code> before implementing it at all:</p>
<pre><code>error[E0277]: the trait bound `Task: FromStr` is not satisfied
--&gt; src/store.rs:98:29
|
98 | tasks.push(line.parse::&lt;Task&gt;()?);
| ^^^^^ unsatisfied trait bound
|
help: the trait `FromStr` is not implemented for `Task`</code></pre>
<p>The whole thing, in the weather-log demo — run today, output below:</p>
<pre><code>#[derive(Debug, PartialEq)]
struct Reading { station: String, celsius: f64 }
impl FromStr for Reading {
type Err = String; // your error type goes in the slot
fn from_str(line: &amp;str) -&gt; Result&lt;Reading, String&gt; {
let (station, temp) =
line.split_once('=').ok_or_else(|| line.to_string())?;
Ok(Reading {
station: station.to_string(),
celsius: temp.parse().map_err(|_| line.to_string())?,
})
}
}</code></pre>
<pre><code>Ok(Reading { station: "oslo", celsius: -3.5 })
Ok(Reading { station: "lagos", celsius: 31.0 })
Err("broken")</code></pre>
<p>One line in there is doing something you have not seen before, so look at the inner
<code>temp.parse().map_err(|_| ..)?</code> closely. It parses a <code>f64</code>, and the failure it can produce is
a <code>ParseFloatError</code> — but notice what that failure <em>means</em> in this context. It does not mean "the
user typed a bad number at the keyboard"; it means "the line stored in this file is corrupt". Those are two
different problems, they deserve two different variants, and only one of them can be the one that <code>From</code>
produces automatically.</p>
<p>So the shape to remember is this: <strong><code>?</code> on its own handles the single canonical conversion,
and <code>map_err</code> is how you name a different variant for any other meaning of the same error type.</strong>
In the drill you will write both, a few lines apart, on the very same <code>ParseIntError</code> — the CLI path
keeps <code>?</code> and produces <code>BadId</code>, while the file path uses <code>map_err</code> and produces
<code>BadLine</code>.</p>
<h2>Part 4 — Three decisions the tests will hold you to</h2>
<h3>A missing file is not an error</h3>
<p>Picture the very first time anyone runs your CLI. There is no <code>tasks.txt</code> yet, because nothing has
ever created one. If <code>load</code> simply propagates whatever <code>fs::read_to_string</code> returns, that
first run prints an error to stderr and exits 1 — the program looks broken before the user has done anything wrong.
A missing file here is not a failure at all; it is the normal starting state, and it means "you have no tasks yet".</p>
<p>So this is one of the rare places where you deliberately catch a single kind of error and turn it into a
successful result, while letting every other kind through untouched:</p>
<pre><code>let text = match fs::read_to_string(path) {
Ok(text) =&gt; text,
Err(e) if e.kind() == io::ErrorKind::NotFound =&gt; return Ok(Store::new()),
Err(e) =&gt; return Err(TaskError::Io(e)), // permissions etc. still fail
};</code></pre>
<p>The new syntax is <code>Err(e) if ..</code>, which is called a <strong>match guard</strong>: an extra condition
attached to an arm, so the arm only matches when the pattern fits <em>and</em> the condition holds. Here the first
<code>Err</code> arm catches only <code>NotFound</code>, and anything else falls through to the arm below it.</p>
<p>It is worth being clear about what this is not, because the lazy version is tempting. This is not
<code>unwrap_or_default()</code> and it is not <code>.ok()</code>. Both of those would treat <em>every</em> io
failure as "no tasks" — so a permissions problem, or a disk that has gone read-only, would silently present the
user with an empty list, and the next <code>save</code> would overwrite their real file with nothing. One specific
kind of failure is expected; the rest genuinely are failures and must still be reported.</p>
<h3>The saved format is not the <code>Display</code> format</h3>
<p>You already have a <code>Display for Task</code> from lesson 0005, and it prints
<code>1 [done] buy milk (high)</code>. That is a good sentence for a person reading a terminal, and it is a poor
format to read back in: to reconstruct the task you would have to find the brackets and parentheses, while allowing
for a title that might itself contain either. The format fights you because it was never designed to be parsed.</p>
<p>Storage has different requirements from presentation, so give it its own format — one with a separator that
splits cleanly and a fixed field order:</p>
<pre><code>1|done|high|buy milk</code></pre>
<p>Two audiences, two formats: <code>Display</code> stays exactly as it is for the <code>list</code> command, and
<code>to_line</code> is added beside it for the file. Do not be tempted to make one serve both.</p>
<p>Notice also that the title is placed <em>last</em>. That is deliberate, and it lets the title contain anything at
all, including the separator itself. The reason is how <code>splitn</code> works:
<code>"a|b|c|d|e".splitn(4, '|')</code> stops splitting after it has produced four pieces, so the fourth piece is
the entire remainder, <code>"d|e"</code>, with its <code>|</code> intact. Reach for <code>split</code> instead and a
title containing a pipe silently loses everything after it. One of the shipped tests covers exactly this case.</p>
<h3><code>#[derive(PartialEq)]</code> will break, and that is informative</h3>
<p>Add <code>Io(io::Error)</code> to the enum and the derive on line 3 fails:</p>
<pre><code>error[E0369]: binary operation `==` cannot be applied to type `&amp;std::io::Error`
--&gt; src/error.rs:12:8
|
3 | #[derive(Debug, PartialEq)]
| --------- in this derive macro expansion
...
12 | Io(std::io::Error),
| ^^^^^^^^^^^^^^
|
note: `std::io::Error` does not implement `PartialEq`</code></pre>
<p>The note at the bottom is the interesting part: <code>io::Error</code> deliberately does not implement
<code>PartialEq</code>. That is a considered decision by the std authors, not an oversight — two io failures can
carry the same message and still come from entirely different OS state, so "are these two errors equal?" has no
honest answer. Your enum now contains one, and equality for the whole enum is therefore no longer derivable.</p>
<p>This is also a useful moment to see what <code>derive</code> actually is. It is not a language feature attached
to the type; it is a code generator that writes an ordinary <code>impl</code> for you, comparing every field with
<code>==</code>. When one field cannot be compared, the generated line does not compile, and you get the error
above pointing at the derive itself.</p>
<p>Dropping <code>PartialEq</code> is not an option, because the 0006 tests compare <code>TaskError</code> values
with <code>assert_eq!</code> and you may not edit them. So write the impl by hand instead. This part is mechanical
rather than conceptual — type it, understand the three notes underneath, and move on:</p>
<pre><code>impl PartialEq for TaskError {
fn eq(&amp;self, other: &amp;Self) -&gt; bool {
use TaskError::*;
match (self, other) {
(UnknownCommand(a), UnknownCommand(b))
| (BadPriority(a), BadPriority(b))
| (BadLine(a), BadLine(b)) =&gt; a == b,
(BadId(a), BadId(b)) =&gt; a == b,
(NotFound(a), NotFound(b)) =&gt; a == b,
(Io(a), Io(b)) =&gt; a.kind() == b.kind(), // the kind, not the error
_ =&gt; std::mem::discriminant(self) == std::mem::discriminant(other),
}
}
}</code></pre>
<p>Three pieces of that impl are new, and each is useful well beyond this one function.</p>
<p>First, <code>match (self, other)</code> matches on a <strong>tuple of two values at once</strong>. You build a
temporary pair and pattern-match both halves together, which is how you ask "are these the same variant, and if so,
are their payloads equal?" in a single expression.</p>
<p>Second, <code>(A(a), A(b)) | (B(a), B(b)) =&gt;</code> is an <strong>or-pattern</strong>: several patterns
sharing one arm. Rust allows it here because every alternative binds the same names, <code>a</code> and
<code>b</code>, at the same types, so the arm's body is valid whichever alternative matched. That is what lets three
<code>String</code>-carrying variants share a single line instead of taking three.</p>
<p>Third, <a href="https://doc.rust-lang.org/std/mem/fn.discriminant.html"><code>mem::discriminant</code></a>
returns an opaque value identifying <em>which</em> variant a value is, ignoring any payload. Comparing two of them
answers "same variant?" without your having to name the variants at all, which handles the four payload-free cases
in one line.</p>
<p>That last convenience has a real cost, and it is the kind of thing to notice now rather than discover later. The
<code>_</code> arm means the compiler will never again force you to update this impl when you add a variant — and a
new variant carrying data would then be compared by variant alone, treating two different payloads as equal. It is
an acceptable trade for a small error type, but it is a trade, not a free win.</p>
<h2>Check yourself before the drill</h2>
<p>Six questions before you touch the keyboard. Try to answer each one out loud, in full sentences, before you
reveal or click — an answer you can say is an answer you have understood, and one you can only recognise on a page
usually is not. Getting one wrong here costs you nothing; getting the same thing wrong twenty minutes into the
drill costs you the drill.</p>
<div class="q" data-type="mcq" data-topic="Traits">
<p class="topic">Traits</p>
<p class="prompt">You have <code>impl From&lt;ParseIntError&gt; for TaskError</code>. Which second impl is rejected by the compiler?</p>
<div class="options">
<button class="opt" data-correct="true">A second <code>From&lt;ParseIntError&gt; for TaskError</code></button>
<button class="opt" data-correct="false">An added <code>From&lt;io::Error&gt; for TaskError</code></button>
<button class="opt" data-correct="false">An added <code>From&lt;ParseIntError&gt; for LineError</code></button>
<button class="opt" data-correct="false">An added <code>From&lt;ParseFloatError&gt; for TaskError</code></button>
</div>
<div class="explain hidden">One impl per (trait, source type, target type). Two impls for the same <code>T</code> give <code>error[E0119]: conflicting implementations</code>, because <code>?</code> would have no single answer for what to convert into. Changing either the source type or the target type makes it a different impl, so the other three are all legal.</div>
</div>
<div class="q" data-type="recall" data-topic="Traits">
<p class="topic">Traits</p>
<p class="prompt">What is <code>type Err</code> in <code>impl FromStr</code>, and why is it not written <code>impl FromStr&lt;Err&gt;</code>?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">It is an <strong>associated type</strong>: a slot in the trait that the implementor fills in exactly once. A generic parameter is chosen by the <em>caller</em> and lets one type implement the trait many times; an associated type is chosen by the <em>implementor</em>, so <code>Task</code> implements <code>FromStr</code> once and <code>"…".parse::&lt;Task&gt;()</code> is never ambiguous about which error comes back. Omit it and you get <code>error[E0046]: missing Err in implementation</code>.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="mcq" data-topic="Error handling">
<p class="topic">Error handling</p>
<p class="prompt">Your <code>load</code> calls <code>fs::read_to_string(path)?</code> and the file does not exist yet. What does the user see on their first ever run?</p>
<div class="options">
<button class="opt" data-correct="false">An empty task list, printed normally</button>
<button class="opt" data-correct="true">An error on stderr, and exit code 1</button>
<button class="opt" data-correct="false">A new empty file, created silently</button>
<button class="opt" data-correct="false">A panic with a full stack backtrace</button>
</div>
<div class="explain hidden">A bare <code>?</code> propagates every <code>io::Error</code>, including <code>ErrorKind::NotFound</code>. Your <code>main</code> prints it to stderr and exits 1 — so a brand-new install looks broken. That is why <code>load</code> needs the match guard <code>Err(e) if e.kind() == ErrorKind::NotFound =&gt; Ok(Store::new())</code>, and why the shipped test <code>a_missing_file_is_an_empty_store_not_an_error</code> exists.</div>
</div>
<div class="q" data-type="recall" data-topic="Error handling">
<p class="topic">Error handling</p>
<p class="prompt">Both <code>done abc</code> (a CLI argument) and a corrupt saved line produce a <code>ParseIntError</code>. You want two different variants. How, given only one <code>From</code> impl is allowed?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden"><code>?</code> for the canonical one, <code>map_err</code> for the other. The CLI path keeps <code>id.parse()?</code>, which calls <code>From</code> and yields <code>BadId</code>. The file path writes <code>part.parse().map_err(|_| TaskError::BadLine(line.to_string()))?</code>, choosing the variant explicitly. Same error type, two meanings, and the meaning is a property of the call site, not of the type — which is exactly what <code>From</code> cannot express.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="recall" data-topic="Ownership">
<p class="topic">Ownership</p>
<p class="prompt"><code>fn load(path: &amp;Path) -&gt; Result&lt;Store, TaskError&gt;</code> — no <code>&amp;self</code>, and it returns a <code>Store</code> by value. Why is that not a copy, and where does the returned value live?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">It is an <strong>associated function</strong>, not a method — no receiver, called as <code>Store::load(&amp;path)</code>, the same shape as <code>Store::new()</code>. Returning by value <em>moves</em> ownership to the caller; the <code>Vec</code>'s heap buffer is never copied, only the three-word handle. <code>&amp;Path</code> is borrowed because <code>load</code> only reads the path and the caller keeps it for the later <code>save</code>.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="recall" data-topic="Enums">
<p class="topic">Enums</p>
<p class="prompt">After loading two tasks from a file, why must <code>Store</code> recompute its next id instead of starting at 1?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Because the counter is state that lives in memory only, and the file does not save it. Load without recomputing and the next <code>add</code> hands out id 1 again — two tasks share an id, and <code>done 1</code> silently completes the wrong one. The fix is one line: <code>tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1</code>. The test <code>ids_do_not_restart_after_a_reload</code> is exactly this bug, caught.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div id="summary"><div id="summary-body"></div><p id="summary-total"></p>
<button id="report-btn">Copy report</button><pre id="report-output" class="hidden"></pre></div>
<h2>The drill — 30 minutes, your own crate</h2>
<p>Type it, do not paste it. The weather log above is a different program. Keep the
<a href="../reference/rust-syntax.html#files">files &amp; FromStr reference</a> open — looking syntax up is free.</p>
<pre><code>cd ~/learn-rust/tasks
cp ../lessons/0007-persist-spec.rs tests/persist.rs
cargo test # 8 new tests fail to compile — that is the starting line</code></pre>
<p>Do not edit anything in <code>tests/</code>. All 24 existing tests must still pass. Target at the end:
<strong>32 passing</strong>.</p>
<h3>Step 0 — pay off 0006 (2 minutes)</h3>
<p>Delete both <code>match id.parse()</code> blocks in <code>command.rs</code>. Each becomes one line, and the
<code>From</code> impl you wrote last lesson finally does its job:</p>
<pre><code>let id: u32 = args.get(1).ok_or(TaskError::MissingId)?.parse()?;</code></pre>
<p><strong>Check:</strong> <code>grep -c "match id.parse" src/command.rs</code> prints <code>0</code>, and
<code>cargo test --test errors</code> still passes 7.</p>
<h3>Step 1 — two new variants, and a hand-written <code>PartialEq</code></h3>
<p>Add to <code>TaskError</code>:</p>
<pre><code>BadLine(String), // a saved line that cannot be read back — carries the line
Io(io::Error), // the file could not be read or written — wraps std's error</code></pre>
<p>Then: remove <code>PartialEq</code> from the derive and write the impl from Part 4; add both
<code>Display</code> arms; add <code>Io(e) =&gt; Some(e)</code> to <code>source()</code>; add
<code>From&lt;io::Error&gt;</code>. Exact messages, checked by the tests:</p>
<table>
<tr><th>Variant</th><th><code>to_string()</code> must be</th></tr>
<tr><td><code>BadLine("rubbish")</code></td><td><code>cannot read saved line: rubbish</code></td></tr>
<tr><td><code>Io(..)</code></td><td><code>cannot read or write the task file</code></td></tr>
</table>
<p><strong>Check:</strong> <code>cargo build</code> passes, and <code>cargo test --test errors</code> is still 7/7
— the old variants must behave exactly as before.</p>
<h3>Step 2 — <code>task.rs</code>: one line out, one line in</h3>
<p>Three additions:</p>
<ul>
<li><code>Status::parse(&amp;str) -&gt; Option&lt;Status&gt;</code> — the mirror of <code>label()</code>, same shape as
<code>Priority::parse</code>, which you already have.</li>
<li><code>Task::to_line(&amp;self) -&gt; String</code> — <code>id|status|priority|title</code>.</li>
<li><code>impl FromStr for Task</code> with <code>type Err = TaskError</code>, using
<code>splitn(4, '|')</code>. Every failure is <code>BadLine(line.to_string())</code> — including the id, which is
where <code>map_err</code> earns its keep.</li>
</ul>
<p><strong>Check:</strong> <code>cargo test --test persist a_task_becomes</code> and
<code>cargo test --test persist a_title_may</code> both pass.</p>
<details>
<summary>Stuck on repeating <code>BadLine(line.to_string())</code> five times?</summary>
<p>Bind it once as a closure and hand it to <code>ok_or_else</code>: <code>let bad = || TaskError::BadLine(line.to_string());</code>
then <code>parts.next().ok_or_else(bad)?</code>. Note <code>ok_or_else</code>, not <code>ok_or</code> — the first
takes a closure and only builds the error when there is one, the second builds it every time. With a
<code>String</code> allocation inside, that difference is real.</p>
</details>
<h3>Step 3 — <code>store.rs</code>: <code>save</code> and <code>load</code></h3>
<pre><code>pub fn save(&amp;self, path: &amp;Path) -&gt; Result&lt;(), TaskError&gt;
pub fn load(path: &amp;Path) -&gt; Result&lt;Store, TaskError&gt; // associated fn</code></pre>
<p><code>save</code> builds one <code>String</code> — one line per task, each ending in <code>\n</code> — and calls
<code>fs::write</code> once. <code>load</code> reads, skips empty lines, parses each into a <code>Task</code>, and
recomputes the next id. Both the missing-file guard and the id recomputation are in Part 4.</p>
<p><strong>Check:</strong> <code>cargo test --test persist</code> passes all 8.</p>
<h3>Step 4 — <code>main.rs</code>: load, act, save</h3>
<p>Where the file lives should not be hard-coded into the logic. One line of ch12 gets you an override for free:</p>
<pre><code>let path = PathBuf::from(
env::var("TASKS_FILE").unwrap_or_else(|_| "tasks.txt".to_string()));</code></pre>
<p>Then <code>run</code> takes the path, loads the store itself, and saves at the end. Note the ordering that falls
out of <code>?</code>: an error anywhere means <code>save</code> is never reached, so a failed command cannot
corrupt the file.</p>
<p><strong>Check — your CLI now remembers things:</strong></p>
<pre><code>$ cd $(mktemp -d) # empty dir: proves a first run works with no file
$ run(){ TASKS_FILE=t.txt cargo run -q --manifest-path ~/learn-rust/tasks/Cargo.toml -- "$@"; }
$ run add "buy milk" high
added task 1
$ run add "call bank"
added task 2
$ run done 1
completed 1
$ run list
1 [done] buy milk (high)
2 [todo] call bank (medium)
$ cat t.txt
1|done|high|buy milk
2|todo|medium|call bank
$ echo "rubbish" &gt;&gt; t.txt ; run list ; echo $?
error: cannot read saved line: rubbish
1</code></pre>
<p>That last one is the payoff for <code>BadLine(String)</code> carrying the line: the message names the exact
text to go and fix. A <code>String</code> error, or a bare <code>Io</code>, could not.</p>
<p><strong>Final check:</strong> <code>cargo test</code> → 17 + 7 + 8 = <strong>32 passed</strong>. And add
<code>tasks.txt</code> to <code>.gitignore</code> — it is user data, not source.</p>
<h3>Then stop</h3>
<p>Not today: <code>serde</code> and JSON (the real answer for storage, but it teaches a crate rather than a
concept), file locking, and <code>BufReader</code> for files too big to hold in memory. Your task file is a few
kilobytes; <code>read_to_string</code> is the correct tool, and reaching for a buffered reader here would be
copying a pattern you do not need.</p>
<h2>What this closed</h2>
<p>Chapter 12 moves to <em>produced</em> on the <a href="../reference/book-coverage.html">coverage map</a>:
<code>env::args</code>, <code>env::var</code>, <code>fs</code>, stderr, and exit codes are now all in your own
code. Two gaps left before the job-ready floor, and they are next:</p>
<ol>
<li><strong>ch 8 + 13 — <code>HashMap</code>, <code>map</code>/<code>filter</code>/<code>collect</code></strong>,
plus your first hand-written generic function (lesson 0008). Your <code>load</code> loop is a
<code>collect::&lt;Result&lt;Vec&lt;_&gt;, _&gt;&gt;()</code> waiting to happen — I left it as a <code>for</code> loop
deliberately so 0008 has something of yours to rewrite.</li>
<li><strong>ch 11 — writing your own tests</strong>: you have now consumed 32 of mine and written zero
(lesson 0009).</li>
</ol>
<h2>Take it outside</h2>
<p>The forum ask from 0006 still stands and is now stronger: <code>error.rs</code> holds both
<em>user mistakes</em> (<code>UnknownCommand</code>, <code>BadPriority</code>) and <em>system failures</em>
(<code>Io</code>, <code>BadLine</code>) in one enum. Many Rust developers would split those into two types. Post it
on <a href="https://users.rust-lang.org">users.rust-lang.org</a> (Code Review category) and ask which they would
do and why. That is a genuine design question with real disagreement behind it — the answer you get back is wisdom
you cannot derive from the book.</p>
<h2>The five sentences worth keeping</h2>
<ol>
<li><code>io::Result&lt;T&gt;</code> is just <code>Result&lt;T, io::Error&gt;</code>; <code>e.kind()</code> is how you
tell one io failure from another.</li>
<li>One <code>impl From&lt;T&gt; for YourError</code> per <code>T</code> — a different <code>T</code> is a new impl,
a repeat <code>T</code> is <code>E0119</code>.</li>
<li><code>?</code> for the canonical conversion, <code>map_err</code> for every other meaning of the same error.</li>
<li><code>FromStr</code> is what <code>.parse()</code> calls; <code>type Err</code> is an associated type — a slot
the implementor fills, not a parameter the caller passes.</li>
<li>A missing file on first run is expected, not exceptional: match <code>ErrorKind::NotFound</code>, and let every
other kind fail loudly.</li>
</ol>
<footer>
<p><strong>Primary source:</strong> The Rust Book
<a href="https://doc.rust-lang.org/stable/book/ch12-00-an-io-project.html">chapter 12 — An I/O Project</a>, and
specifically <a href="https://doc.rust-lang.org/stable/book/ch12-02-reading-a-file.html">12.2 reading a file</a>
and <a href="https://doc.rust-lang.org/stable/book/ch12-05-working-with-environment-variables.html">12.5
environment variables</a>. Then the one-screen
<a href="https://doc.rust-lang.org/std/str/trait.FromStr.html">std::str::FromStr</a> page — read the
<code>Point</code> example there, it is the same shape as your <code>Task</code>.</p>
<p>Previous: <a href="0006-your-own-error-type.html">0006 — Your own error type</a> ·
<a href="0005-traits-display-and-errors.html">0005 — Traits, Display, errors</a> ·
<a href="0004-structs-enums-packages.html">0004 — Structs, enums, packages</a><br />
Reference: <a href="../reference/rust-syntax.html#files">Files &amp; FromStr</a> ·
<a href="../reference/rust-syntax.html#error-types">Custom error types</a> ·
<a href="../reference/book-coverage.html">Coverage map</a></p>
<p><strong>Ask me things.</strong> Bring the compiler output verbatim — step 1 breaks the derive and step 2 is the
first trait you have implemented with an associated type. If a paragraph did not land, name it; that is my fault to
fix, and cheaper to fix now than mid-drill.</p>
</footer>
<script src="../assets/quiz.js"></script>
</body>
</html>
+106
View File
@@ -0,0 +1,106 @@
// The 0007 specification, as executable tests. Do not edit this file — make it pass.
// Copy to: tasks/tests/persist.rs
// Run with: cargo test
use std::error::Error;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};
use tasks::error::TaskError;
use tasks::store::Store;
use tasks::task::{Priority, Status, Task};
// a fresh path per test, inside the OS temp dir — no file is ever left in your crate
fn temp_path() -> PathBuf {
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("tasks-test-{}-{}.txt", std::process::id(), n))
}
// ---------- a Task survives the trip to text and back ----------
#[test]
fn a_task_becomes_a_line_and_a_line_becomes_a_task() {
let task = Task {
id: 3,
title: String::from("buy milk"),
priority: Priority::High,
status: Status::Done,
};
assert_eq!(task.to_line(), "3|done|high|buy milk");
assert_eq!("3|done|high|buy milk".parse::<Task>().unwrap(), task);
}
#[test]
fn a_title_may_contain_the_separator() {
let line = "1|todo|low|read a|b testing";
let task: Task = line.parse().unwrap();
assert_eq!(task.title, "read a|b testing");
assert_eq!(task.to_line(), line);
}
#[test]
fn a_corrupt_line_names_itself() {
for bad in ["", "nonsense", "1|todo|low", "x|todo|low|t", "1|sleeping|low|t", "1|todo|urgent|t"] {
assert_eq!(
bad.parse::<Task>().unwrap_err(),
TaskError::BadLine(bad.to_string()),
"line {:?} should be reported as a bad line",
bad
);
}
}
// ---------- the store round trip ----------
#[test]
fn every_task_survives_save_then_load() {
let path = temp_path();
let mut store = Store::new();
store.add("buy milk", Priority::High);
let second = store.add("call bank", Priority::Medium);
store.complete(second).unwrap();
store.save(&path).unwrap();
let loaded = Store::load(&path).unwrap();
assert_eq!(loaded.tasks(), store.tasks());
}
#[test]
fn ids_do_not_restart_after_a_reload() {
let path = temp_path();
let mut store = Store::new();
store.add("first", Priority::Low);
store.add("second", Priority::Low);
store.save(&path).unwrap();
let mut loaded = Store::load(&path).unwrap();
assert_eq!(loaded.add("third", Priority::Low), 3);
}
#[test]
fn a_missing_file_is_an_empty_store_not_an_error() {
let path = temp_path(); // never created
let store = Store::load(&path).expect("first run must not fail");
assert!(store.tasks().is_empty());
}
#[test]
fn a_corrupt_file_fails_loudly() {
let path = temp_path();
std::fs::write(&path, "1|todo|low|fine\nrubbish\n").unwrap();
assert_eq!(
Store::load(&path).unwrap_err(),
TaskError::BadLine(String::from("rubbish"))
);
}
// ---------- io::Error becomes TaskError, and keeps its cause ----------
#[test]
fn a_write_that_cannot_happen_is_an_io_error() {
let path = temp_path().join("no-such-dir").join("t.txt");
let err = Store::new().save(&path).unwrap_err();
assert!(matches!(err, TaskError::Io(_)), "got {:?}", err);
assert_eq!(err.to_string(), "cannot read or write the task file");
assert!(err.source().is_some(), "the io::Error must stay reachable");
}
+162
View File
@@ -0,0 +1,162 @@
// The 0008 specification, as executable tests. Do not edit this file — make it pass.
// Copy to: tasks/tests/collections.rs
// Run with: cargo test
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};
use tasks::stats::tally;
use tasks::store::Store;
use tasks::task::Priority;
// a fresh path per test, inside the OS temp dir — no file is ever left in your crate
fn temp_path() -> PathBuf {
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("tasks-coll-{}-{}.txt", std::process::id(), n))
}
// ---------- tally: one generic function, any element type, any key type ----------
#[test]
fn tally_counts_how_often_each_key_appears() {
let words = ["red", "blue", "red", "green", "red"];
let counts = tally(&words, |word| *word);
assert_eq!(counts[&"red"], 3);
assert_eq!(counts[&"blue"], 1);
assert_eq!(counts.len(), 3);
}
#[test]
fn tally_accepts_a_key_type_that_is_not_the_element_type() {
let words = ["a", "bb", "cc", "ddd"];
let by_length = tally(&words, |word| word.len());
assert_eq!(by_length, HashMap::from([(1, 1), (2, 2), (3, 1)]));
}
#[test]
fn tally_of_nothing_is_an_empty_map() {
let nothing: [u32; 0] = [];
assert!(tally(&nothing, |n| *n).is_empty());
}
// ---------- counting the store, built on that same function ----------
#[test]
fn count_by_priority_counts_every_priority_present() {
let mut store = Store::new();
store.add("buy milk", Priority::High);
store.add("call bank", Priority::High);
store.add("water plants", Priority::Low);
assert_eq!(
store.count_by_priority(),
HashMap::from([(Priority::High, 2), (Priority::Low, 1)])
);
}
#[test]
fn count_by_priority_has_no_entry_for_an_absent_priority() {
let mut store = Store::new();
store.add("buy milk", Priority::High);
let counts = store.count_by_priority();
assert_eq!(counts.len(), 1, "absent priorities must not appear as zero");
assert_eq!(counts.get(&Priority::Low), None);
}
#[test]
fn count_by_priority_of_an_empty_store_is_empty() {
assert!(Store::new().count_by_priority().is_empty());
}
// ---------- selecting titles: filter, map, collect ----------
#[test]
fn titles_with_returns_matching_titles_in_order() {
let mut store = Store::new();
store.add("buy milk", Priority::High);
store.add("water plants", Priority::Low);
store.add("call bank", Priority::High);
assert_eq!(store.titles_with(Priority::High), ["buy milk", "call bank"]);
}
#[test]
fn titles_with_returns_nothing_when_no_task_matches() {
let mut store = Store::new();
store.add("buy milk", Priority::High);
assert!(store.titles_with(Priority::Medium).is_empty());
}
// ---------- clearing out finished work: retain ----------
#[test]
fn remove_completed_drops_done_tasks_and_reports_how_many() {
let mut store = Store::new();
let first = store.add("buy milk", Priority::High);
store.add("water plants", Priority::Low);
let third = store.add("call bank", Priority::Medium);
store.complete(first).unwrap();
store.complete(third).unwrap();
assert_eq!(store.remove_completed(), 2);
assert_eq!(store.titles_with(Priority::Low), ["water plants"]);
assert_eq!(store.tasks().len(), 1);
}
#[test]
fn remove_completed_removes_nothing_when_nothing_is_done() {
let mut store = Store::new();
store.add("buy milk", Priority::High);
store.add("call bank", Priority::Low);
assert_eq!(store.remove_completed(), 0);
assert_eq!(store.tasks().len(), 2);
}
#[test]
fn remove_completed_keeps_the_remaining_ids_unchanged() {
let mut store = Store::new();
let first = store.add("buy milk", Priority::High);
store.add("call bank", Priority::Low);
store.complete(first).unwrap();
store.remove_completed();
assert_eq!(store.tasks()[0].id, 2, "surviving tasks keep their own id");
assert_eq!(store.add("new one", Priority::Low), 3, "and the counter is untouched");
}
// ---------- load: one collect, and the line rules that come with it ----------
#[test]
fn a_file_without_a_trailing_newline_still_loads() {
let path = temp_path();
std::fs::write(&path, "1|todo|low|fine").unwrap();
let store = Store::load(&path).unwrap();
assert_eq!(store.tasks().len(), 1);
}
#[test]
fn a_blank_line_inside_the_file_is_a_bad_line() {
let path = temp_path();
std::fs::write(&path, "1|todo|low|fine\n\n2|todo|low|also fine\n").unwrap();
let err = Store::load(&path).unwrap_err();
assert_eq!(err.to_string(), "cannot read saved line: ");
}
#[test]
fn one_bad_line_loses_the_whole_load_not_part_of_it() {
let path = temp_path();
std::fs::write(&path, "1|todo|low|fine\nrubbish\n3|todo|low|also fine\n").unwrap();
assert!(Store::load(&path).is_err());
// the file itself is untouched by a failed load — nothing was half-written
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
"1|todo|low|fine\nrubbish\n3|todo|low|also fine\n"
);
}
+784
View File
@@ -0,0 +1,784 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>0008 — Iterators, HashMap, and your first generic function</title>
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body>
<h1>Iterators, <code>HashMap</code>, and your first generic function</h1>
<p class="subtitle">Lesson 0008 · after <a href="0007-files-and-fromstr.html">0007</a> · reading, then a 35-minute drill against a shipped test file · ~50 minutes</p>
<div class="callout">
Every code block, every compiler message, and every terminal session on this page was produced by running it
today. Nothing is written from memory. The demo domain is a library shelf, defined in full in the next section —
your project is a task CLI, so nothing here pastes in. Translating is the work.
</div>
<h2>The demo domain, in full</h2>
<p>Every example on this page runs against the same four books, so it is worth reading the data model once
before the examples start. Then any snippet below can be read without guessing what a field is called or what
type it holds. This is the whole thing — two type definitions, two helpers, and one <code>Vec</code>:</p>
<pre><code>use Shelf::*; // so the examples can say Fiction instead of Shelf::Fiction
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
enum Shelf { Fiction, History, Poetry } // fieldless, so Copy is free
#[derive(Debug)]
struct Book {
title: String, // owned text
shelf: Shelf, // which shelf it belongs on
borrowed: bool, // is it out on loan right now
}
fn book(title: &amp;str, shelf: Shelf, borrowed: bool) -&gt; Book {
Book { title: title.to_string(), shelf, borrowed }
}
// "fiction" -&gt; Some(Fiction), anything unknown -&gt; None
fn shelf_of(word: &amp;str) -&gt; Option&lt;Shelf&gt; {
match word {
"fiction" =&gt; Some(Fiction),
"history" =&gt; Some(History),
"poetry" =&gt; Some(Poetry),
_ =&gt; None,
}
}
// The data. Four books, three shelves, one of them out on loan.
let mut shelf: Vec&lt;Book&gt; = vec![
book("Dubliners", Fiction, true),
book("SPQR", History, false),
book("Ariel", Poetry, false),
book("Beloved", Fiction, false),
];
// A second value, used only where an element must be a plain string:
let titles: Vec&lt;&amp;str&gt; = vec!["Dubliners", "SPQR", "Ariel"];</code></pre>
<p>Two naming conventions to hold on to, because they are the only way to know an element's type at a glance.
<code>shelf</code> (lowercase) is the <code>Vec&lt;Book&gt;</code>, so <code>shelf.iter()</code> hands you
<code>&amp;Book</code> and the closure parameter is written <code>|b|</code>. <code>Shelf</code> (capitalised) is
the enum, so <code>b.shelf</code> is a field holding one of its three variants. And <code>titles</code> is a
<code>Vec&lt;&amp;str&gt;</code>, so <code>titles.iter()</code> hands you <code>&amp;&amp;str</code> and the closure
parameter is written <code>|t|</code>.</p>
<p>The mapping onto your own crate is exact, which is what makes the translation mechanical rather than
creative: <code>Book</code> is <code>Task</code>, <code>title</code> is <code>title</code>,
<code>Shelf</code> is <code>Priority</code>, and <code>borrowed</code> is <code>Status</code>. So when a
snippet below counts books per shelf, you are reading the <code>count_by_priority</code> you are about to
write.</p>
<h2>Where 0007 left you, and one prediction I got wrong</h2>
<p>Thirty-two tests are green and the persistence layer behind them is real: <code>fs::read_to_string</code> and
<code>fs::write</code>, a <code>NotFound</code> match guard so a first run does not look broken, an
<code>impl FromStr for Task</code> with its own associated <code>type Err</code>, and a hand-written
<code>PartialEq</code> because <code>io::Error</code> refuses to have one. The best signal is in
<code>command.rs</code>: both hand-rolled <code>match id.parse()</code> blocks are gone, replaced by
<code>.parse()?</code>. That was the whole point of step 0, and it landed — <code>?</code> plus <code>From</code>
is now a reflex rather than a fact.</p>
<p>But I predicted something in 0007 that turned out to be false, and it is worth a paragraph because the lesson
generalises. I claimed the compiler would <em>force</em> you to write <code>impl From&lt;io::Error&gt; for
TaskError</code>, because <code>?</code> on <code>fs::write</code> would be the only reasonable shape. You wrote
this instead:</p>
<pre><code>fs::write(path, contents).map_err(TaskError::Io)</code></pre>
<p>That is perfectly good Rust. <code>TaskError::Io</code> is a tuple-variant constructor, which means it is also
a function of type <code>fn(io::Error) -&gt; TaskError</code>, so handing it straight to <code>map_err</code> is
idiomatic and allocation-free. No <code>From</code> impl needed, no error, and the test still passes. My claim
was simply wrong: <strong>a compiler error can only force a design when no legal alternative exists</strong>, and
here a legal alternative existed.</p>
<p>So which one should you write? Both are correct, and the difference is leverage rather than style.
<code>map_err</code> converts at one call site; <code>From</code> converts at <em>every</em> call site,
including ones you have not written yet, and it is what makes bare <code>?</code> work on any function in
<code>std::fs</code>, <code>std::io</code>, or a future crate that returns an <code>io::Error</code>. Today's
<code>save</code> gets rewritten anyway, and the rewrite is shorter when <code>?</code> just works — so step 0 of
the drill writes the impl and deletes the <code>map_err</code>. One line each way.</p>
<h2>Part 1 — An iterator is a lazy machine with one button</h2>
<p>You have written iterator code already, in bursts: <code>env::args().skip(1).collect()</code> in
<code>main</code>, <code>self.tasks.iter().find(|t| t.id == id)</code> in <code>find</code>, and
<code>.map(|t| t.id).max().unwrap_or(0)</code> in <code>load</code>. What you have not had yet is the model
underneath them, so each one was memorised separately. The model is unusually small — one trait, one method:</p>
<pre><code>pub trait Iterator {
type Item;
fn next(&amp;mut self) -&gt; Option&lt;Self::Item&gt;;
// ~75 more methods, all with default bodies built on next()
}</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch13-02-iterators.html">13.2 — Processing a
series of items with iterators</a> · std:
<a href="https://doc.rust-lang.org/std/iter/trait.Iterator.html">Iterator</a></p>
<p>That is the entire interface. <code>next</code> hands back <code>Some(item)</code> until the sequence runs
out, then <code>None</code> forever. Notice <code>type Item</code>: it is an associated type, the same
mechanism you filled in as <code>type Err</code> when you implemented <code>FromStr</code> last lesson. Every
other method — <code>map</code>, <code>filter</code>, <code>find</code>, <code>collect</code>, <code>sum</code>
— is a default method written in terms of <code>next</code>. Which is why learning the vocabulary is cheap:
there is no new machinery behind any of them, only different ways of pressing the same button.</p>
<p>The one property that trips everybody up is laziness. The book states it flatly:</p>
<blockquote>
<p>In Rust, iterators are <em>lazy</em>, meaning they have no effect until you call methods that consume the
iterator to use it up.</p>
</blockquote>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch13-02-iterators.html">13.2</a></p>
<p>Building a chain of adapters does no work and touches no elements. It only describes work. Write a chain and
forget to finish it, and the closure never runs even once — the compiler warns, because the warning is the only
thing standing between you and a silently dead line of code:</p>
<pre><code>titles.iter().map(|t| t.to_uppercase());</code></pre>
<pre><code>warning: unused `Map` that must be used
--&gt; examples/e1.rs:3:5
|
3 | titles.iter().map(|t| t.to_uppercase());
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default</code></pre>
<p>So every chain has exactly two parts, and it is worth naming them because the names tell you where a chain
must end. <strong>Adapters</strong> take an iterator and return another iterator: <code>map</code>,
<code>filter</code>, <code>enumerate</code>, <code>skip</code>, <code>take</code>, <code>rev</code>. They are
lazy, and they compose. <strong>Consumers</strong> take an iterator and return something that is not an
iterator: <code>collect</code>, <code>find</code>, <code>position</code>, <code>count</code>, <code>sum</code>,
<code>max</code>, <code>any</code>, <code>for_each</code>. They do the work, and a chain that does not end in
one has not run.</p>
<p>The performance question answers itself once you see the structure, and it matters for the job you are aiming
at. A chain of adapters is not a chain of temporary vectors — each adapter is a small struct wrapping the previous
one, and the whole tower compiles down to a single pass. That is why rewriting a <code>for</code> loop as an
iterator chain costs nothing at runtime, and why nobody in Rust treats the choice as a speed trade-off.</p>
<h2>Part 2 — Three ways to iterate, and the ownership behind each</h2>
<p>Before any adapter runs you have to say <em>how</em> you want the elements, and this is the one place where
iterators meet the borrow rules. There are three methods, they differ only in the ownership they hand out, and
picking the wrong one is the most common way an iterator chain fails to compile:</p>
<table>
<tr><th>Call</th><th>Item type</th><th>Use it when</th></tr>
<tr><td><code>v.iter()</code></td><td><code>&amp;T</code></td><td>You are reading. The collection survives.</td></tr>
<tr><td><code>v.iter_mut()</code></td><td><code>&amp;mut T</code></td><td>You are editing in place. It survives.</td></tr>
<tr><td><code>v.into_iter()</code></td><td><code>T</code></td><td>You want the elements out. It is consumed.</td></tr>
</table>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch13-02-iterators.html">13.2 — “if we want
to create an iterator that takes ownership … we can call <code>into_iter</code>”</a></p>
<p>This is the same three-way choice you already make with <code>&amp;self</code>, <code>&amp;mut self</code>,
and <code>self</code> in a method signature, applied one element at a time — so nothing new is being introduced,
only a new place for a rule you already know. It also explains a piece of your own code you may have written
without reading: your <code>complete</code> uses <code>iter_mut</code> because it assigns to
<code>task.status</code>, while <code>find</code> uses <code>iter</code> because it only looks. Swap them and
neither compiles.</p>
<p>One trap deserves seeing before you hit it in the drill, because the error message is about a borrow and the
cause is an iterator. When you keep the result of an <code>iter_mut</code> chain in a variable, the mutable
borrow of the whole collection stays alive for as long as that variable does:</p>
<pre><code>struct Library { books: Vec&lt;String&gt; } // titles only, to keep the error bare
impl Library {
fn rename(&amp;mut self, from: &amp;str, to: &amp;str) {
let book = self.books.iter_mut().find(|b| *b == from).unwrap();
println!("{} books", self.books.len()); // asks for a second borrow
*book = to.to_string();
}
}</code></pre>
<pre><code>error[E0502]: cannot borrow `self.books` as immutable because it is also borrowed as mutable
--&gt; examples/e4.rs:5:44
|
4 | let book = self.books.iter_mut().find(|b| *b == from).unwrap();
| ---------- mutable borrow occurs here
5 | println!("renaming 1 of {} books", self.books.len());
| ^^^^^^^^^^ immutable borrow occurs here
6 | *book = to.to_string();
| ----- mutable borrow later used here</code></pre>
<p>Read the three annotations as a timeline and the rule falls out: the borrow begins at
<code>iter_mut()</code>, and it ends after the <em>last use</em> of <code>book</code>, not at the end of the
statement that created it. Anything else touching <code>self.books</code> in between is a second borrow, and
that is exactly the rule from chapter 4. The fix is to reorder — read the length first, or finish with
<code>book</code> before asking. Nothing about iterators is special here; they just make the overlap easy to
write by accident.</p>
<h2>Part 3 — The verbs you will use every day</h2>
<p>Here is the whole working vocabulary, run against a shelf of books. Read the calls beside their real output
rather than trying to memorise signatures — the shapes are what you want in your fingers:</p>
<pre><code>// the same four books from the top of the page
let mut shelf: Vec&lt;Book&gt; = vec![
book("Dubliners", Fiction, true), book("SPQR", History, false),
book("Ariel", Poetry, false), book("Beloved", Fiction, false),
];
let all_titles: Vec&lt;&amp;str&gt; = shelf.iter().map(|b| b.title.as_str()).collect();
shelf.iter_mut().for_each(|b| b.borrowed = false);
let fiction: Vec&lt;&amp;str&gt; = shelf.iter()
.filter(|b| b.shelf == Shelf::Fiction)
.map(|b| b.title.as_str())
.collect();
shelf.iter().find(|b| b.title == "Ariel").map(|b| b.shelf);
shelf.iter().position(|b| b.title == "Ariel");
shelf.iter().any(|b| b.borrowed); // bool
shelf.iter().filter(|b| !b.borrowed).count(); // usize
shelf.retain(|b| b.shelf != Shelf::Poetry); // delete in place</code></pre>
<pre><code>all_titles -&gt; ["Dubliners", "SPQR", "Ariel", "Beloved"]
iter_mut -&gt; every book returned, borrowed set to false on each
fiction -&gt; ["Dubliners", "Beloved"]
find -&gt; Some(Poetry) // the ITEM, mapped: Option&lt;Shelf&gt;
position -&gt; Some(2) // the INDEX: Option&lt;usize&gt;, Ariel is 3rd
any / count -&gt; false / 4 // nothing is borrowed now, so all 4 are in
retain -&gt; removed 1, 3 left // Ariel was the only poetry book</code></pre>
<p>Two of those are worth a second look, because they are the ones your own <code>store.rs</code> currently
writes out longhand as loops.</p>
<p><strong><code>find</code> versus <code>position</code></strong> is a question about what you need next.
<code>find</code> gives you the element, which is what <code>complete</code> wants — it has to assign to
<code>status</code>. <code>position</code> gives you the index, which is what <code>remove</code> wants —
<code>Vec::remove</code> takes an index, not an element. Both return an <code>Option</code>, and both compose
straight into the error handling you already have: <code>.ok_or(TaskError::NotFound(id))?</code> turns a
<code>None</code> into your own error and unwraps the rest, collapsing a nine-line loop into one expression.</p>
<p><strong><code>retain</code></strong> is the one that saves you from a genuine bug. Deleting several elements
from a <code>Vec</code> by index in a loop is a classic error: each removal shifts everything after it down one,
so the loop skips elements. <code>retain</code> takes a predicate meaning “keep this one” and does a single
compacting pass. It is a method on <code>Vec</code> rather than on <code>Iterator</code>, because it mutates
the collection in place — one of several useful methods that live on the collection rather than on the trait.</p>
<p class="cite">std: <a href="https://doc.rust-lang.org/std/vec/struct.Vec.html#method.retain">Vec::retain</a>
· <a href="https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.position">Iterator::position</a></p>
<h2>Part 4 — <code>collect</code> is the interesting one</h2>
<p><code>collect</code> looks like “make a <code>Vec</code>”, and that undersells it enough to hide the single
most useful trick in this lesson. Its real signature says something much stronger:</p>
<pre><code>fn collect&lt;B: FromIterator&lt;Self::Item&gt;&gt;(self) -&gt; B</code></pre>
<p class="cite">std: <a href="https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect">Iterator::collect</a>
· <a href="https://doc.rust-lang.org/std/iter/trait.FromIterator.html">FromIterator</a></p>
<p>Read that as: <em>collect will build any type that knows how to be built from this kind of item</em>. The
target is chosen by <code>B</code>, and <code>B</code> is decided by you, at the call site — which is why
<code>collect</code> is the one method where you routinely have to state a type. Leave it out and the compiler
has nothing to go on:</p>
<pre><code>let shouted = titles.iter().map(|t| t.to_uppercase()).collect();</code></pre>
<pre><code>error[E0283]: type annotations needed
--&gt; examples/e2.rs:3:9
|
3 | let shouted = titles.iter().map(|t| t.to_uppercase()).collect();
| ^^^^^^^ ------- type must be known at this point
|
= note: multiple `impl`s satisfying `_: FromIterator&lt;String&gt;` found in the `alloc` crate:
- impl FromIterator&lt;String&gt; for Box&lt;str&gt;;
- impl FromIterator&lt;String&gt; for String;</code></pre>
<p>The note is the teaching. This is not the compiler being fussy about vectors — it is telling you that several
types can be built from a stream of <code>String</code>s and it will not guess which one you meant. You answer
either on the left, <code>let shouted: Vec&lt;String&gt; = ...</code>, or on the right with a turbofish,
<code>.collect::&lt;Vec&lt;String&gt;&gt;()</code>. Both are common; pick whichever reads better in the line.</p>
<p>Now the trick. <code>Result</code> and <code>Option</code> both implement <code>FromIterator</code>, so
<strong>an iterator of <code>Result</code>s can collect into a single <code>Result</code> holding a
<code>Vec</code></strong>. The same chain, with only the target type changed, gives two entirely different
answers:</p>
<pre><code>let words = ["fiction", "history", "rubbish", "poetry"];
let each: Vec&lt;Option&lt;Shelf&gt;&gt; = words.iter().map(|w| shelf_of(w)).collect();
let all: Option&lt;Vec&lt;Shelf&gt;&gt; = words.iter().map(|w| shelf_of(w)).collect();
let numbers: Result&lt;Vec&lt;u32&gt;, _&gt; =
"1 2 x 4".split(' ').map(str::parse::&lt;u32&gt;).collect();</code></pre>
<pre><code>Vec&lt;Option&gt; -&gt; [Some(Fiction), Some(History), None, Some(Poetry)]
Option&lt;Vec&gt; -&gt; None
Result&lt;Vec&gt; -&gt; Err("invalid digit found in string")</code></pre>
<p class="cite">std: <a href="https://doc.rust-lang.org/std/result/enum.Result.html#impl-FromIterator%3CResult%3CA,+E%3E%3E-for-Result%3CV,+E%3E">impl
FromIterator&lt;Result&lt;A, E&gt;&gt; for Result&lt;V, E&gt;</a></p>
<p><code>Vec&lt;Option&lt;Shelf&gt;&gt;</code> keeps every outcome, hole included. <code>Option&lt;Vec&lt;Shelf&gt;&gt;</code>
means all-or-nothing: the first <code>None</code> ends the iteration and the whole result is <code>None</code>.
That short-circuit is not a detail — it is the reason this is the right tool for reading a file. Your
<code>load</code> currently loops, parses each line, and pushes into a <code>Vec</code>, with
<code>?</code> inside the loop. One <code>collect</code> replaces all of it:</p>
<pre><code>let tasks: Vec&lt;Task&gt; = contents.lines()
.map(str::parse)
.collect::&lt;Result&lt;Vec&lt;Task&gt;, TaskError&gt;&gt;()?;</code></pre>
<p>The behaviour is exactly what a save file wants. Every line parses and you get the tasks; one line is corrupt
and you get that line's error and nothing else — no half-loaded store to accidentally save back over the good
file. And notice <code>map(str::parse)</code>: you can pass a function <em>path</em> where a closure is expected,
because <code>|line| line.parse()</code> and <code>str::parse</code> are the same function. Which
<code>parse</code>, of the many possible, is settled by the collect target — the <code>Vec&lt;Task&gt;</code> tells
the compiler to look for <code>Task</code>'s <code>FromStr</code> impl, the one you wrote last lesson.</p>
<p>One line-splitting detail comes with this rewrite, and it is a real trap rather than trivia:</p>
<pre><code>let file = "1|fiction|Dubliners\n2|poetry|Ariel\n";
file.split('\n') // -&gt; ["1|fiction|Dubliners", "2|poetry|Ariel", ""]
file.lines() // -&gt; ["1|fiction|Dubliners", "2|poetry|Ariel"]</code></pre>
<p class="cite">std: <a href="https://doc.rust-lang.org/std/primitive.str.html#method.lines">str::lines</a></p>
<p><code>split('\n')</code> yields an empty final piece for a file that ends in a newline, because the text
after the last separator is the empty string. That is why your current <code>load</code> needs an
<code>if !items.is_empty()</code> guard — the guard exists to paper over the wrong splitter.
<a href="https://doc.rust-lang.org/std/primitive.str.html#method.lines"><code>lines()</code></a> is built for
this job: it treats the trailing newline as a terminator rather than a separator, and it strips a
<code>\r\n</code> too, which is free Windows compatibility. Switch splitters and the guard disappears — after
which a blank line in the <em>middle</em> of a file is no longer silently skipped but reported as a bad line,
which is the honest answer for a corrupt file. A shipped test pins that behaviour.</p>
<h2>Part 5 — <code>HashMap</code>, and the two traits a key must have</h2>
<p><code>HashMap&lt;K, V&gt;</code> is the last of the three common collections, and it is the one you have not
used at all. It is a lookup by key rather than by position, it lives on the heap like <code>Vec</code>, and it
is not in the prelude, so it needs an import:</p>
<pre><code>use std::collections::HashMap;
let mut counts: HashMap&lt;Shelf, usize&gt; = HashMap::new();
counts.insert(Shelf::Fiction, 2);
counts.get(&amp;Shelf::Fiction); // Option&lt;&amp;usize&gt; — may be absent
counts.get(&amp;Shelf::Poetry).copied().unwrap_or(0); // absent counts as 0
for (shelf, n) in &amp;counts { } // ARBITRARY order — never trust it</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html">8.3 — Storing keys
with associated values in hash maps</a></p>
<p>Two things there are easy to skim past and expensive to learn later. <code>get</code> returns an
<code>Option&lt;&amp;V&gt;</code>, so “missing key” is a value you handle rather than a crash — the same shape as
<code>Vec::get</code>. And iteration order is arbitrary and not stable between runs. If a user is going to read
your output, you must impose an order yourself; the <code>stats</code> command in today's drill prints high,
medium, low in a fixed sequence for exactly that reason.</p>
<p>The idiom that makes hash maps worth their weight is <code>entry</code>. Counting things is the standard
example, and the book's version is four lines:</p>
<pre><code>for b in &amp;shelf {
*counts.entry(b.shelf).or_insert(0) += 1;
}</code></pre>
<pre><code>entry() -&gt; {Fiction: 2, History: 1}</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html">8.3 — Listing
8-25, counting occurrences of words</a></p>
<p>Take that line apart slowly, because it is dense and it is everywhere in real Rust.
<code>entry(key)</code> returns an <code>Entry</code>, an enum standing for a slot that may or may not be
filled. <code>or_insert(0)</code> fills it with <code>0</code> if it was empty, and either way hands back a
<code>&amp;mut usize</code> pointing into the map. <code>*</code> follows that reference so
<code>+= 1</code> lands on the number itself. The whole thing is one hash lookup — the version you would write
by hand, <code>if !map.contains_key(k) { map.insert(k, 0) }</code> followed by a <code>get_mut</code>, costs
two or three and reads worse.</p>
<p>Now the part that is specific to Rust. A key type must implement <code>Eq</code> and <code>Hash</code>, and
you will meet that rule at a call site from today's drill — so here is the line the next two errors point at,
before they point at it. <code>Store::count_by_priority</code> is one line long, and it hands the work to a
small generic function called <code>tally</code>. You write both in the drill; Part 6 builds
<code>tally</code> from this signature:</p>
<pre><code>// src/stats.rs — Part 6 explains it and the drill writes the body
pub fn tally&lt;T, K, F&gt;(items: &amp;[T], key: F) -&gt; HashMap&lt;K, usize&gt;
// src/store.rs:68 — count_by_priority, in full
tally(self.tasks(), |task| task.priority)</code></pre>
<p>Read that as “count the items, grouped by whatever the closure pulls out of each one”. It is all you need
for the errors below; the three type parameters and the body are Part 6's job. Your <code>Priority</code>
implements neither <code>Eq</code> nor <code>Hash</code>, so the first attempt at using it as a key fails twice
over:</p>
<pre><code>error[E0277]: the trait bound `Priority: Eq` is not satisfied
--&gt; src/store.rs:68:9
|
68 | tally(self.tasks(), |task| task.priority)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Eq` is not implemented for `Priority`
|
help: consider annotating `Priority` with `#[derive(Eq)]`
error[E0277]: the trait bound `Priority: Hash` is not satisfied
help: consider annotating `Priority` with `#[derive(Hash)]`</code></pre>
<p>The requirement is not bureaucracy; it is the data structure stating its contract. To find a key the map
hashes it to pick a bucket, then compares for equality inside that bucket — so a key it cannot hash or cannot
compare is a key it cannot store. <code>Hash</code> gives it the first, <code>Eq</code> the second.</p>
<p><code>Eq</code> is worth understanding rather than just deriving, since you already have
<code>PartialEq</code> and this looks like a duplicate. It is not: <code>Eq</code> is a marker with no methods
of its own, and it promises one extra property that <code>PartialEq</code> does not — that every value equals
itself. The famous exception is <code>f64</code>, where <code>NAN != NAN</code>, which is precisely why
<code>f64</code> implements <code>PartialEq</code> but not <code>Eq</code>, and why a <code>f64</code> cannot
be a <code>HashMap</code> key. A three-variant enum has no such problem, so the derive is honest.</p>
<p class="cite">std: <a href="https://doc.rust-lang.org/std/cmp/trait.Eq.html">Eq</a> ·
<a href="https://doc.rust-lang.org/std/hash/trait.Hash.html">Hash</a></p>
<p>Fix those two and a third error appears, which is the most instructive of the set:</p>
<pre><code>error[E0507]: cannot move out of `task.priority` which is behind a shared reference
--&gt; src/store.rs:68:36
|
68 | tally(self.tasks(), |task| task.priority)
| ^^^^^^^^^^^^^ move occurs because `task.priority` has type `Priority`,
| which does not implement the `Copy` trait
|
note: if `Priority` implemented `Clone`, you could clone the value</code></pre>
<p>The closure receives <code>&amp;Task</code> — a borrow — and a map key has to be owned, since the map keeps
it. Reading <code>task.priority</code> out of a borrow is a move out of something you do not own, which is
E0507, one of the most common errors in real Rust. Three fixes exist and they are not equivalent:
<code>.clone()</code> works but is noise for three variants; <code>#[derive(Clone, Copy)]</code> makes
<code>Priority</code> behave like <code>u32</code>, copied implicitly wherever it is read; keying by
<code>task.priority.label()</code> sidesteps it by using a <code>&amp;str</code> instead. Derive
<code>Copy</code>. A fieldless enum is a single small integer at runtime, copying it is free, and it is what std
does for its own small enums such as <code>ErrorKind</code>.</p>
<h2>Part 6 — Your first generic function</h2>
<p>Counting tasks by priority is one specific job, and today you will write it once and never again — because the
function you write is generic over what it counts. This is your first hand-written generic, so here it is whole,
and then taken apart:</p>
<pre><code>use std::collections::HashMap;
use std::hash::Hash;
pub fn tally&lt;T, K, F&gt;(items: &amp;[T], key: F) -&gt; HashMap&lt;K, usize&gt;
where
K: Eq + Hash,
F: Fn(&amp;T) -&gt; K,
{
let mut counts = HashMap::new();
for item in items {
*counts.entry(key(item)).or_insert(0) += 1;
}
counts
}</code></pre>
<p>Three type parameters, and each one is there for a reason. <code>T</code> is the element type, and the
function never looks inside a <code>T</code>, which is exactly why it works on tasks and on strings alike.
<code>K</code> is the key type, and it carries the bound <code>Eq + Hash</code> — not because <code>tally</code>
cares, but because the <code>HashMap</code> it returns does. <code>F</code> is the closure type. Every closure in
Rust has its own anonymous type, so the only way to accept one is a type parameter bounded by
<code>Fn(&amp;T) -&gt; K</code>, which reads as “anything callable that takes a <code>&amp;T</code> and returns a
<code>K</code>”.</p>
<p>The <code>where</code> clause is worth seeing as the point of the exercise rather than syntax to tolerate. It
is a contract in both directions: callers must supply types that satisfy it, and inside the body you may use
exactly the operations it guarantees and nothing else. That is why generics in Rust do not blow up at the call
site the way C++ templates can — the bounds are checked once, against the definition. Try to call
<code>item.to_string()</code> in there and it will not compile, because nothing in the clause promised
<code>T: Display</code>.</p>
<p>The payoff is that one definition serves cases that have nothing to do with each other:</p>
<pre><code>tally(&amp;shelf, |b| b.shelf) // -&gt; {History: 1, Fiction: 2}
tally(&amp;["a", "bb", "cc"], |w| w.len()) // -&gt; {1: 1, 2: 2}</code></pre>
<p>Two calls, two different <code>T</code>, two different <code>K</code>, and — this is the part that matters
for the interviews you are aiming at — no runtime cost for the generality. Rust monomorphises: it compiles one
specialised copy of <code>tally</code> per combination of types actually used, so each call site gets code as
tight as if you had written that version by hand.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch10-01-syntax.html">10.1 — Generic data
types</a> and <a href="https://doc.rust-lang.org/stable/book/ch13-01-closures.html">13.1 — Closures</a></p>
<p>Last note before the drill, and it is a taste question rather than a rule. <code>tally</code>'s body keeps a
<code>for</code> loop, on purpose. Iterators replace loops that <em>search</em>, <em>transform</em>, or
<em>collect</em> — those have a named adapter and the chain reads better than the loop. A loop that folds many
items into one accumulator is the case where a loop is still the clearest thing to write; the iterator version
exists, <code>fold</code>, and here it would be harder to read for no gain. The drill's grep check is scoped to
<code>store.rs</code> for exactly this reason.</p>
<h2>Check yourself before the drill</h2>
<p>Six questions before you touch the keyboard. Answer each one out loud, in full sentences, before you reveal or
click. An answer you can say is an answer you have understood; one you can only recognise on the page usually is
not. Getting one wrong here costs nothing — getting it wrong twenty minutes into the drill costs you the drill.</p>
<div class="q" data-type="mcq" data-topic="Iterators">
<p class="topic">Iterators</p>
<p class="prompt">How many times does the closure run in <code>v.iter().map(|x| f(x));</code> — with no <code>collect</code>?</p>
<div class="options">
<button class="opt" data-correct="true">Not once, and it warns</button>
<button class="opt" data-correct="false">Once for each element</button>
<button class="opt" data-correct="false">Once, for the first item</button>
<button class="opt" data-correct="false">Once for each, then drops</button>
</div>
<div class="explain hidden">Adapters are lazy: <code>map</code> only builds a <code>Map</code> struct describing the work. Nothing iterates until a consumer calls <code>next</code>, so the closure never runs and you get <code>warning: unused `Map` that must be used — iterators are lazy and do nothing unless consumed</code>. A chain that does not end in a consumer has not run.</div>
</div>
<div class="q" data-type="recall" data-topic="Iterators">
<p class="topic">Iterators</p>
<p class="prompt"><code>complete</code> needs the task itself; <code>remove</code> needs its index. Which adapter does each want, and which iterator does each start from?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden"><code>complete</code> wants <code>self.tasks.iter_mut().find(|t| t.id == id)</code> — <code>find</code> returns the element, and <code>iter_mut</code> because it assigns to <code>status</code>. <code>remove</code> wants <code>self.tasks.iter().position(|t| t.id == id)</code> — <code>position</code> returns <code>Option&lt;usize&gt;</code>, which is what <code>Vec::remove</code> takes, and plain <code>iter</code> is enough because it only looks. Both then take <code>.ok_or(TaskError::NotFound(id))?</code>.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="mcq" data-topic="Collections">
<p class="topic">Collections</p>
<p class="prompt">Four lines are parsed, the third is corrupt, and you <code>collect::&lt;Result&lt;Vec&lt;Task&gt;, TaskError&gt;&gt;()</code>. What comes back?</p>
<div class="options">
<button class="opt" data-correct="true">One <code>Err</code>, holding that line</button>
<button class="opt" data-correct="false">One <code>Ok</code>, holding three tasks</button>
<button class="opt" data-correct="false">One <code>Ok</code>, holding four results</button>
<button class="opt" data-correct="false">One <code>Err</code>, holding four errors</button>
</div>
<div class="explain hidden"><code>Result</code> implements <code>FromIterator</code>, so collecting an iterator of <code>Result</code>s into a <code>Result&lt;Vec&lt;_&gt;, E&gt;</code> short-circuits: the first <code>Err</code> stops the iteration and becomes the whole answer, and the successful items are dropped. That is the behaviour a save file wants — no half-loaded store. Collect into <code>Vec&lt;Result&lt;Task, TaskError&gt;&gt;</code> instead and you keep all four outcomes.</div>
</div>
<div class="q" data-type="recall" data-topic="Collections">
<p class="topic">Collections</p>
<p class="prompt">Why must a <code>HashMap</code> key implement both <code>Eq</code> and <code>Hash</code>, and why is <code>PartialEq</code> not enough?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Lookup is two steps: hash the key to choose a bucket (<code>Hash</code>), then compare for equality inside it (<code>Eq</code>). <code>Eq</code> is a marker trait with no methods that adds one promise <code>PartialEq</code> does not make — every value equals itself. <code>f64</code> breaks that promise, since <code>NAN != NAN</code>, so it implements <code>PartialEq</code> only and cannot be a key. A fieldless enum can promise it, so <code>#[derive(PartialEq, Eq, Hash)]</code> is honest.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="recall" data-topic="Ownership">
<p class="topic">Ownership</p>
<p class="prompt"><code>|task| task.priority</code> in a closure over <code>&amp;Task</code> gives <code>error[E0507]: cannot move out of ... behind a shared reference</code>. What is the cause, and which of the three fixes wins?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">A map key must be <em>owned</em>, because the map keeps it — but the closure only has a borrow of the task, so reading the field out of it is a move from something you do not own. Fixes: <code>.clone()</code>, <code>#[derive(Clone, Copy)]</code>, or key by <code>label()</code> to get a <code>&amp;str</code>. Derive <code>Copy</code>: a fieldless enum is one small integer, so copying is free, and std does the same for its own small enums such as <code>ErrorKind</code>.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="recall" data-topic="Traits">
<p class="topic">Traits</p>
<p class="prompt">In <code>fn tally&lt;T, K, F&gt;(items: &amp;[T], key: F)</code> with <code>K: Eq + Hash, F: Fn(&amp;T) -&gt; K</code> — why does <code>F</code> have to be a type parameter at all, and what does the <code>where</code> clause buy you?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Every closure has its own unique anonymous type, so there is no concrete type to write down; a parameter bounded by the <code>Fn</code> trait is the only way to accept one. The bounds are a two-way contract: callers must supply types that satisfy them, and the body may use only the operations they guarantee — so <code>item.to_string()</code> would not compile without <code>T: Display</code>. That is checked once against the definition, and monomorphisation then compiles one specialised copy per set of types, so the generality costs nothing at runtime.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div id="summary"><div id="summary-body"></div><p id="summary-total"></p>
<button id="report-btn">Copy report</button><pre id="report-output" class="hidden"></pre></div>
<h2>The drill — 35 minutes, your own crate</h2>
<p>Type it, do not paste it. The bookshelf above is a different program. Keep the
<a href="../reference/rust-syntax.html#iterators">iterators reference</a> open — looking syntax up is free.</p>
<pre><code>cd ~/learn-rust/tasks
cp ../lessons/0008-collections-spec.rs tests/collections.rs
cargo test # 14 new tests fail to compile — that is the starting line</code></pre>
<p>Do not edit anything in <code>tests/</code>. All 32 existing tests must still pass. Target at the end:
<strong>46 passing</strong>.</p>
<h3>Step 0 — the second <code>From</code>, two minutes</h3>
<p>Write the impl that 0007 asked for and then delete the workaround, so <code>?</code> handles io errors
everywhere from here on:</p>
<pre><code>impl From&lt;io::Error&gt; for TaskError { .. } // in error.rs, beside the other
fs::write(path, contents)?; // in save — map_err goes away</code></pre>
<p><strong>Check:</strong> <code>grep -c "impl From&lt;io::Error&gt;" src/error.rs</code> prints <code>1</code>,
<code>grep -c "map_err(TaskError::Io)" src/store.rs</code> prints <code>0</code>, and
<code>cargo test --test persist</code> still passes 8.</p>
<h3>Step 1 — a new module and one generic function</h3>
<p>Create <code>src/stats.rs</code>, declare it in <code>lib.rs</code>, and write <code>tally</code> from
Part 6 — from the signature, not by copying the body. It is nine lines.</p>
<p><strong>Check:</strong> <code>cargo test --test collections tally</code> → 3 passed.</p>
<details>
<summary>Forgotten how a module is declared?</summary>
<p><code>pub mod stats;</code> in <code>src/lib.rs</code>, alphabetically beside the others. Without that line
the file is not compiled at all and you get <code>error[E0432]: unresolved import</code> from the test file —
the same error 0006 showed you.</p>
</details>
<h3>Step 2 — <code>Priority</code> as a key</h3>
<p>Add <code>count_by_priority(&amp;self) -&gt; HashMap&lt;Priority, usize&gt;</code> to <code>Store</code>, as one
line delegating to <code>tally</code>. Let it fail first, read all three errors, and fix them with the derives
they ask for. Seeing E0277 twice and E0507 once, in that order, is the point of the step.</p>
<p><strong>Check:</strong> <code>cargo test --test collections count_by</code> → 3 passed.</p>
<h3>Step 3 — two more methods on <code>Store</code></h3>
<pre><code>pub fn titles_with(&amp;self, priority: Priority) -&gt; Vec&lt;&amp;str&gt;
pub fn remove_completed(&amp;mut self) -&gt; usize</code></pre>
<p><code>titles_with</code> answers “what am I meant to be doing at this priority?”. Hand it a priority and it
gives back the title of every task that carries that priority, in the order the tasks were added. Nothing
matches, and you get an empty <code>Vec</code> rather than an error — an empty answer is a legitimate answer
here. Note the return type: <code>Vec&lt;&amp;str&gt;</code>, not <code>Vec&lt;String&gt;</code>. It hands back
borrows of titles the store still owns, so nothing is cloned, and <code>task.title.as_str()</code> is the
conversion you need.</p>
<p><code>remove_completed</code> is the tidy-up: it deletes every task whose status is <code>Done</code> and
returns how many it deleted. Three details the tests hold you to. The tasks that survive keep their own ids —
you are removing rows, not renumbering them. The id counter is untouched, so the next <code>add</code> carries
on from where it had got to rather than reusing a freed number. And removing nothing is a normal outcome that
returns <code>0</code>, not an error. One <code>Vec</code> method from Part 3 does the removal; the count is
the length before minus the length after.</p>
<p><strong>Check:</strong> <code>cargo test --test collections</code> → 11 of 14 passed.</p>
<h3>Step 4 — rewrite <code>store.rs</code> with what you learned</h3>
<p>Four functions, all currently loops, all one expression each: <code>complete</code> with
<code>iter_mut().find()</code>, <code>remove</code> with <code>iter().position()</code>, <code>save</code>
with <code>map(..).collect::&lt;String&gt;()</code>, and <code>load</code> with
<code>lines().map(str::parse).collect::&lt;Result&lt;Vec&lt;Task&gt;, TaskError&gt;&gt;()?</code>. In <code>load</code>
the <code>if !items.is_empty()</code> guard goes away with the splitter, and the trailing
<code>mut contents = String::new()</code> dance collapses into the <code>match</code> from 0007 returning
a value.</p>
<p><strong>Check:</strong> <code>grep -c "for " src/store.rs</code> prints <code>0</code>,
<code>grep -c "lines()" src/store.rs</code> prints <code>1</code>, and <code>cargo test</code> → 17 + 7 + 8 +
14 = <strong>46 passed</strong>.</p>
<details>
<summary>Stuck on <code>save</code> building a <code>String</code> from an iterator?</summary>
<p><code>String</code> implements <code>FromIterator&lt;String&gt;</code>, so a chain of owned lines collects
straight into one: <code>self.tasks.iter().map(|t| format!("{}\n", t.to_line())).collect()</code>. Annotate the
target — <code>let contents: String = ..</code> — or E0283 will ask you which of several possible types you
meant.</p>
</details>
<h3>Step 5 — two new commands, so the CLI shows it</h3>
<p>Add <code>Stats</code> and <code>Clear</code> to the <code>Command</code> enum and to
<code>Command::parse</code> (the words are <code>stats</code> and <code>clear</code>). The
<code>match</code> in <code>main</code> will refuse to compile until both are handled — that is
<code>E0004</code>, the same non-exhaustive-match error from 0006, doing its job again.</p>
<p><code>stats</code> must print the three priorities in a fixed order, because hash map iteration order is
arbitrary. Loop over <code>[Priority::High, Priority::Medium, Priority::Low]</code> and ask the map for each,
with <code>counts.get(&amp;p).copied().unwrap_or(0)</code> so an absent priority prints <code>0</code> rather
than vanishing.</p>
<p><strong>Check — a real session, run today against the reference implementation:</strong></p>
<pre><code>$ cd $(mktemp -d)
$ run add "buy milk" high
added task 1
$ run add "call bank"
added task 2
$ run add "water plants" low
added task 3
$ run done 1
completed 1
$ run stats
high 1
medium 1
low 1
$ run clear
cleared 1 completed
$ run list
2 [todo] call bank (medium)
3 [todo] water plants (low)
$ cat t.txt
2|todo|medium|call bank
3|todo|low|water plants
$ run stats ; echo $?
high 0
medium 1
low 1
0</code></pre>
<p>(<code>run</code> above is
<code>TASKS_FILE=t.txt cargo run -q --manifest-path ~/learn-rust/tasks/Cargo.toml --</code>.) That last
<code>high 0</code> is <code>.copied().unwrap_or(0)</code> earning its place: the completed high-priority task
is gone, so the map has no <code>High</code> entry at all, and the absence prints as a zero instead of a
missing line.</p>
<h3>Then stop</h3>
<p>Not today: <code>fold</code> and <code>zip</code>, <code>BTreeMap</code> (sorted keys — the right answer if
you ever want <code>stats</code> ordered without hard-coding), <code>impl Iterator for</code> your own type, and
<code>itertools</code>. Each is a small step from here, and none of them is on the path to the next gap.</p>
<h2>What this closed</h2>
<p>Chapters 8 and 13 move to <em>produced</em> on the <a href="../reference/book-coverage.html">coverage
map</a>, and 10.1 opens with a real generic function of your own rather than a book example. What is left before
the job-ready floor is short:</p>
<ol>
<li><strong>ch 11 — writing your own tests</strong> (lesson 0009). You have now consumed 46 of my tests and
written zero. Test-writing is a first-round interview question, and it is the last big gap in the book's core.</li>
<li><strong>ch 10.3 — lifetimes</strong>, as reading practice. You wrote one today without noticing:
<code>titles_with</code> returns <code>Vec&lt;&amp;str&gt;</code> borrowed from <code>&amp;self</code>, and
elision filled in the annotation for you.</li>
<li>Then <code>serde</code> → <code>axum</code>, where the trait work from 0005–0008 starts paying rent.</li>
</ol>
<h2>Take it outside</h2>
<p>Here is a question with genuine disagreement behind it, which makes it a good one to ask people rather than
docs. Your <code>tally</code> takes <code>&amp;[T]</code>. Most experienced Rust developers would write it to
take <code>impl IntoIterator&lt;Item = T&gt;</code> instead, so it accepts a <code>Vec</code>, an array, a
<code>HashSet</code>, or any chain of adapters — not only a slice. Post <code>tally</code> on
<a href="https://users.rust-lang.org">users.rust-lang.org</a> (Code Review category) and ask whether the
<code>IntoIterator</code> version is worth the extra signature complexity for a small crate, and where they
personally draw that line. The answers will teach you more about idiomatic API design than any chapter, because
it is a taste question and the book cannot have taste for you.</p>
<h2>The five sentences worth keeping</h2>
<ol>
<li>Adapters are lazy and return iterators; consumers do the work. A chain that does not end in a consumer never
ran.</li>
<li><code>iter</code> borrows, <code>iter_mut</code> borrows mutably, <code>into_iter</code> takes ownership —
the <code>&amp;self</code>/<code>&amp;mut self</code>/<code>self</code> choice, one element at a time.</li>
<li><code>collect</code> builds any <code>FromIterator</code> type, so an iterator of <code>Result</code>s
collects into one <code>Result&lt;Vec&lt;_&gt;, E&gt;</code> that short-circuits on the first error.</li>
<li>A <code>HashMap</code> key needs <code>Eq + Hash</code>; <code>*map.entry(k).or_insert(0) += 1</code> is the
counting idiom; iteration order is arbitrary, so impose your own before printing.</li>
<li>A generic function's <code>where</code> clause is a contract checked once against the definition, and
monomorphisation means the generality is free at runtime.</li>
</ol>
<footer>
<p><strong>Primary source:</strong> The Rust Book
<a href="https://doc.rust-lang.org/stable/book/ch13-02-iterators.html">13.2 — Processing a Series of Items with
Iterators</a>, then <a href="https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html">8.3 — Hash Maps</a>.
After the drill, skim the method list on
<a href="https://doc.rust-lang.org/std/iter/trait.Iterator.html">std::iter::Iterator</a> — not to memorise it, but
so you know what is there to look for. It is the single highest-value page in std.</p>
<p>Previous: <a href="0007-files-and-fromstr.html">0007 — Files, io::Error, FromStr</a> ·
<a href="0006-your-own-error-type.html">0006 — Your own error type</a> ·
<a href="0005-traits-display-and-errors.html">0005 — Traits, Display, errors</a><br />
Reference: <a href="../reference/rust-syntax.html#iterators">Iterators</a> ·
<a href="../reference/rust-syntax.html#collections">Collections</a> ·
<a href="../reference/rust-syntax.html#traits">Traits &amp; generics</a> ·
<a href="../reference/book-coverage.html">Coverage map</a></p>
<p><strong>Ask me things.</strong> Bring the compiler output verbatim — step 2 is meant to fail three times, and
step 4 is the first time you will rewrite working code purely for shape, which is a different kind of
uncomfortable. If a paragraph did not land, name it; that is my fault to fix, and cheaper to fix now than
mid-drill.</p>
</footer>
<script src="../assets/quiz.js"></script>
</body>
</html>
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# 0009 — the mutation check.
#
# Copies your crate to a temp dir, plants ONE deliberate bug in the copy, and
# runs YOUR tests against it. Your own files are never touched.
#
# killed = your tests noticed the bug. Good.
# SURVIVED = the bug is invisible to your suite. Write the missing test.
# SKIP = the pattern is not in your source, so nothing was planted.
#
# Usage: bash 0009-mutants.sh ~/learn-rust/tasks
set -u
crate=$(cd "${1:-.}" && pwd)
work=$(mktemp -d)
export CARGO_TARGET_DIR=$work/target
killed=0 survived=0 skipped=0
# name <TAB> file <TAB> sed expression
mutants=$(cat <<'EOF'
stats-order src/cli.rs s/Priority::High, Priority::Medium, Priority::Low/Priority::Low, Priority::Medium, Priority::High/
stats-zero src/cli.rs s/unwrap_or(0)/unwrap_or(1)/
clear-count src/cli.rs s/remove_completed()/remove_completed() + 1/
list-format src/task.rs s/({})"/{}"/
status-parse src/task.rs s/"in-progress" =>/"inprogress" =>/
command-case src/command.rs s/to_lowercase()/to_string()/
EOF
)
echo "crate: $crate"
while IFS=$'\t' read -r name file expr; do
d=$(mktemp -d)
cp -r "$crate/src" "$crate/tests" "$crate/Cargo.toml" "$crate/Cargo.lock" "$d/"
if [ ! -f "$d/$file" ]; then
printf ' SKIP %-13s %s does not exist yet\n' "$name" "$file"
skipped=$((skipped + 1))
continue
fi
before=$(md5sum <"$d/$file")
sed -i "$expr" "$d/$file"
if [ "$before" = "$(md5sum <"$d/$file")" ]; then
printf ' SKIP %-13s pattern not found in %s\n' "$name" "$file"
skipped=$((skipped + 1))
continue
fi
if ! (cd "$d" && cargo build --tests -q) >/dev/null 2>&1; then
printf ' SKIP %-13s mutant does not compile\n' "$name"
skipped=$((skipped + 1))
continue
fi
if (cd "$d" && cargo test -q) >/dev/null 2>&1; then
printf ' SURVIVED %-13s %s\n' "$name" "$file"
survived=$((survived + 1))
else
printf ' killed %-13s %s\n' "$name" "$file"
killed=$((killed + 1))
fi
done <<<"$mutants"
printf '\n%d killed, %d survived, %d skipped\n' "$killed" "$survived" "$skipped"
[ "$survived" -eq 0 ]
+934
View File
@@ -0,0 +1,934 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>0009 — Writing your own tests</title>
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body>
<h1>Writing your own tests</h1>
<p class="subtitle">Lesson 0009 · after <a href="0008-iterators-and-hashmap.html">0008</a> · reading, then a 45-minute drill graded by planted bugs · ~55 minutes</p>
<div class="callout">
Every code block, every compiler message, and every terminal session on this page was produced by running it
today. Nothing is written from memory. Where a <code>cargo test</code> block is quoted, the
<code>Compiling</code> / <code>Finished</code> lines and the empty <code>Doc-tests</code> section are cut and
nothing else. The demo domain is a thermostat, defined in full two sections down — your project is a task CLI,
so nothing here pastes in. Translating is the work.
</div>
<h2>Where 0008 left you, and the bug that 46 tests could not see</h2>
<p>The library half of 0008 landed cleanly. All 46 tests pass, <code>tally</code> is a real generic function
written from its signature, <code>load</code> is one <code>collect::&lt;Result&lt;Vec&lt;Task&gt;,
TaskError&gt;&gt;()?</code>, <code>remove_completed</code> uses <code>Vec::retain</code>, and
<code>count_by_priority</code> is a one-line delegate. Iterators and <code>HashMap</code> are produced, not
recognised.</p>
<p>Three of the drill's checks still failed, and the interesting thing is where they failed. Here is your
<code>stats</code> and <code>clear</code>, run today against your own crate:</p>
<pre><code>$ run stats
high 1
low 1
medium 1
$ run clear
$ run list
2 [todo] call bank (medium)
3 [todo] water plants (low)</code></pre>
<p>The priorities print in the wrong order — <code>high</code>, <code>low</code>, <code>medium</code>, because
<code>main.rs</code> loops over <code>[Priority::High, Low, Medium]</code> — and <code>clear</code> prints
nothing at all, throwing away the <code>usize</code> that <code>remove_completed</code> went to the trouble of
returning. Both are real defects a user would notice in the first minute. Both sat behind 46 green tests.</p>
<p>That is not bad luck, and it is not because you were careless. It is structural: <strong>every one of those
46 tests lives in <code>tests/</code>, every one of them talks to the library, and <code>run</code> lives in
<code>src/main.rs</code>, where no test in <code>tests/</code> can reach it.</strong> The untested thing is the
undone thing — this is the third lesson in a row where the one requirement no test could see is the one
requirement that was not met. Today you close that loop from both ends: you learn to write tests, and you move
the code that was unreachable into a place where a test can reach it.</p>
<h2>The demo domain, in full</h2>
<p>Every example on this page runs against one small crate, so it is worth reading the whole thing once before
the examples start. It is a thermostat that refuses illegal targets. Three things to notice as you read: the
field <code>target</code> is private, the helper <code>capped</code> has no <code>pub</code>, and
<code>new</code> panics while <code>set_from</code> returns a <code>Result</code> — the page needs both to show
you both ways of testing failure:</p>
<pre><code>// /tmp/heating/src/lib.rs — created with `cargo new --lib heating`
pub const MIN: i32 = 5;
pub const MAX: i32 = 30;
#[derive(Debug, PartialEq)]
pub struct Thermostat {
target: i32, // degrees celsius, always inside MIN..=MAX
}
impl Thermostat {
// panics if the target is outside the legal range
pub fn new(target: i32) -&gt; Thermostat {
if target &lt; MIN {
panic!("target must be at least {MIN}, got {target}");
} else if target &gt; MAX {
panic!("target must be at most {MAX}, got {target}");
}
Thermostat { target }
}
pub fn target(&amp;self) -&gt; i32 {
self.target
}
// never leaves the legal range, however big `by` is
pub fn warmer(&amp;mut self, by: i32) {
self.target = capped(self.target + by);
}
pub fn is_heating(&amp;self, room: i32) -&gt; bool {
room &lt; self.target
}
// "21" -&gt; Ok, "hot" or "99" -&gt; Err
pub fn set_from(&amp;mut self, text: &amp;str) -&gt; Result&lt;(), String&gt; {
let degrees: i32 = text
.trim()
.parse()
.map_err(|_| format!("not a number: {text}"))?;
if degrees &lt; MIN || degrees &gt; MAX {
return Err(format!("out of range: {degrees}"));
}
self.target = degrees;
Ok(())
}
}
// private helper: no `pub`, so only this file can call it
fn capped(degrees: i32) -&gt; i32 {
degrees.clamp(MIN, MAX)
}</code></pre>
<p>One naming convention, because it is the only way to read the examples without guessing:
<code>Thermostat</code> (capitalised) is the type, <code>t</code> is always a value of it, and
<code>room</code> is always the current room temperature rather than the target. The mapping onto your crate is
loose on purpose — this is a different program, not a template. What transfers is the shape of a test, not its
subject.</p>
<h2>Part 1 — A test is a function that fails by panicking</h2>
<p>The whole mechanism is one attribute. Put <code>#[test]</code> on a function that takes no arguments and
returns nothing, and <code>cargo test</code> builds a second binary out of your crate, runs every such
function, and reports on each one. The book's definition is worth reading slowly, because the second half of it
is the part people never internalise:</p>
<blockquote>
<p>Tests fail when something in the test function panics. Each test is run in a new thread, and when the main
thread sees that a test thread has died, the test is marked as failed.</p>
</blockquote>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html">11.1 — How to
write tests</a></p>
<p>So there is no assertion framework here and no special test runtime. <em>Panicking is the failure
protocol.</em> Every assertion macro you are about to meet is a thin wrapper that panics when its condition
does not hold, which is why <code>.unwrap()</code> in a test body is not a code smell the way it is in
<code>main</code> — an <code>unwrap</code> that fires is a test that fails, with the message you wanted
anyway.</p>
<p>Here are two tests against the thermostat, and the output they produce. Read the output as carefully as the
code, because the failure format is the thing you will actually spend your time reading:</p>
<pre><code>#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_new_thermostat_keeps_its_target() {
let t = Thermostat::new(20);
assert_eq!(t.target(), 20);
}
#[test]
fn warmer_never_passes_the_maximum() {
let mut t = Thermostat::new(28);
t.warmer(10);
assert_eq!(t.target(), MAX);
}
}</code></pre>
<pre><code> Running unittests src/lib.rs (target/debug/deps/heating-795830f7b1de3879)
running 2 tests
test tests::a_new_thermostat_keeps_its_target ... ok
test tests::warmer_never_passes_the_maximum ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</code></pre>
<p>You have read that summary line 46 times without needing it. Now it is yours, so take the five fields
apart once: <code>passed</code> and <code>failed</code> are self-explanatory; <code>ignored</code> counts tests
marked <code>#[ignore]</code>, which Part 5 covers; <code>measured</code> is for nightly-only benchmarks and
will always be <code>0</code> for you; and <code>filtered out</code> counts tests that exist but did not run
because you passed a name filter. Note also that the test name is <code>tests::a_new_thermostat…</code> —
<strong>the module path is part of the test's name</strong>, which is what makes filtering by module possible
later.</p>
<p>Now the same run with a bug planted in <code>capped</code>, which drops the upper bound
(<code>degrees.clamp(MIN, MAX)</code> becomes <code>degrees.max(MIN)</code>):</p>
<pre><code>running 2 tests
test tests::a_new_thermostat_keeps_its_target ... ok
test tests::warmer_never_passes_the_maximum ... FAILED
failures:
---- tests::warmer_never_passes_the_maximum stdout ----
thread 'tests::warmer_never_passes_the_maximum' (192610) panicked at src/lib.rs:66:9:
assertion `left == right` failed
left: 38
right: 30
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::warmer_never_passes_the_maximum
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`</code></pre>
<p>Three sections, and each answers a different question. The per-test lines say <em>which</em> tests ran. The
<code>failures:</code> block with the stdout capture says <em>why</em> each failure happened, and it is the
only place the panic message appears. The short <code>failures:</code> list at the end is just names, so that
with forty tests and six failures you can copy one name and re-run it alone. The final
<code>error: test failed, to rerun pass <code>--lib</code></code> is cargo telling you which target to narrow
to — <code>--lib</code> for unit tests, <code>--test &lt;name&gt;</code> for one integration file.</p>
<h2>Part 2 — Three macros, and what each failure tells you</h2>
<p>You only need three, and the choice between them is entirely about what you want printed when the test
fails. That is the whole design question: a passing test prints nothing interesting, so a macro earns its keep
only by how much it tells you on the day it goes red.</p>
<table>
<tr><th>Macro</th><th>Fails when</th><th>Prints</th></tr>
<tr><td><code>assert!(cond)</code></td><td><code>cond</code> is false</td><td>the source text of <code>cond</code></td></tr>
<tr><td><code>assert_eq!(a, b)</code></td><td><code>a != b</code></td><td>both values, as <code>left</code> and <code>right</code></td></tr>
<tr><td><code>assert_ne!(a, b)</code></td><td><code>a == b</code></td><td>both values, the same way</td></tr>
</table>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html">11.1 — Testing
equality with <code>assert_eq!</code> and <code>assert_ne!</code></a></p>
<p>Prefer <code>assert_eq!</code> whenever you have an expected value to name, because a bare
<code>assert!</code> throws away the numbers. Compare these two failures of the same bug — the room comparison
in <code>is_heating</code> flipped to <code>room &gt; self.target</code>. First, <code>assert!</code> on its
own:</p>
<pre><code>#[test]
fn a_cold_room_heats() {
let t = Thermostat::new(20);
assert!(t.is_heating(18));
}</code></pre>
<pre><code>thread 'tests::a_cold_room_heats' (196474) panicked at src/lib.rs:59:9:
assertion failed: t.is_heating(18)</code></pre>
<p>That tells you the expression was false, which you could have guessed from the test's name. When the
condition is a <code>bool</code> and there is nothing to compare, add the message yourself — every argument
after the condition is handed to <code>format!</code>, so you can print whatever would have helped:</p>
<pre><code>#[test]
fn a_cold_room_heats() {
let t = Thermostat::new(20);
assert!(
t.is_heating(18),
"a room at 18 must heat towards {}",
t.target()
);
}</code></pre>
<pre><code>thread 'tests::a_cold_room_heats' (196562) panicked at src/lib.rs:59:9:
a room at 18 must heat towards 20</code></pre>
<p>Your shipped tests use this in a place worth copying. In <code>tests/persist.rs</code> the loop over corrupt
lines ends with <code>"line {:?} should be reported as a bad line", bad</code>, because the assertion runs six
times and the failure would otherwise not say <em>which</em> line broke it. That is the rule: <strong>if an
assertion runs inside a loop, it needs a message naming the case</strong>, or a red test sends you back to
guessing.</p>
<p>One requirement comes attached to <code>assert_eq!</code>, and you have already satisfied it without
knowing. To print the two values, the macro needs <code>Debug</code>; to compare them, it needs
<code>PartialEq</code>:</p>
<blockquote>
<p>When the assertions fail, these macros print their arguments using debug formatting, which means the values
being compared must implement the <code>PartialEq</code> and <code>Debug</code> traits.</p>
</blockquote>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html">11.1</a></p>
<p>This is why <code>#[derive(Debug, PartialEq)]</code> sits on <code>Task</code>, <code>Status</code>,
<code>Priority</code>, <code>Command</code>, and <code>Store</code> — not decoration, a testing requirement.
And it is why <code>TaskError</code> has that hand-written <code>impl PartialEq</code> from 0007:
<code>io::Error</code> does not implement it, so the derive was impossible and you compared
<code>kind()</code> instead. Every one of those impls exists so that <code>assert_eq!</code> can print
something useful. Today you are finally the one calling it.</p>
<h2>Part 3 — Two ways to test a failure</h2>
<p>Code that works is the easy half. The interesting tests are the ones that pin down what happens when the
input is wrong, and Rust gives you two tools because your code has two ways to fail: it panics, or it returns
an <code>Err</code>.</p>
<p>For a panic, annotate the test with <code>#[should_panic]</code>, and the test passes if and only if the
body panics. Always give it <code>expected</code>, a substring of the panic message, or the test will happily
pass on a panic that came from somewhere else entirely:</p>
<pre><code>#[test]
#[should_panic(expected = "at most 30")]
fn refuses_a_high_target() {
Thermostat::new(99);
}</code></pre>
<pre><code>running 2 tests
test tests::a_text_target_is_read_or_reported ... ok
test tests::refuses_a_high_target - should panic ... ok</code></pre>
<p>Notice the <code>- should panic</code> marker in the result line: the runner tells you the test's polarity
is inverted, which matters when you are reading someone else's suite. And here is the same test against a
<code>new</code> whose upper-bound branch was given the lower bound's message by mistake — the code still
panics on 99, but says the wrong thing:</p>
<pre><code>thread 'tests::refuses_a_high_target' (197030) panicked at src/lib.rs:15:13:
target must be at least 5, got 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
panic message: "target must be at least 5, got 99"
expected substring: "at most 30"</code></pre>
<p>Without <code>expected</code> that run would have been green, and the test would have been worthless: it
would have proved only that <em>something</em> went wrong. The <code>expected</code> substring is what turns
"it panicked" into "it panicked for the reason I meant".</p>
<p>For an <code>Err</code>, the tool is different and better suited to your crate: a test may return
<code>Result</code>, which lets you use <code>?</code> in its body. The test passes on <code>Ok</code> and
fails on <code>Err</code>:</p>
<pre><code>#[test]
fn a_text_target_is_read_or_reported() -&gt; Result&lt;(), String&gt; {
let mut t = Thermostat::new(20);
t.set_from(" 21 ")?; // an Err here fails the test
assert_eq!(t.target(), 21);
assert!(t.set_from("hot").is_err());
Ok(())
}</code></pre>
<p>Two rules come with that shape, and the second one is the trap. First, the return type has to be a
<code>Result</code> whose error implements <code>Debug</code> — <code>Result&lt;(), TaskError&gt;</code>
qualifies, since <code>TaskError</code> derives <code>Debug</code>. Second, from the book:</p>
<blockquote>
<p>You can't use the <code>#[should_panic]</code> annotation on tests that use <code>Result&lt;T, E&gt;</code>.
To assert that an operation returns an <code>Err</code> variant, <em>don't</em> use the question mark operator
on the <code>Result&lt;T, E&gt;</code> value. Instead, use <code>assert!(value.is_err())</code>.</p>
</blockquote>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html">11.1 — Using
<code>Result&lt;T, E&gt;</code> in tests</a></p>
<p>Read those two together and the division of labour is clear. Use <code>?</code> for the steps that are
merely <em>setup</em> — the save, the load, the completion that has to work before the interesting assertion
can run — and use an explicit <code>assert_eq!(…unwrap_err(), …)</code> or
<code>assert!(…is_err())</code> for the failure you are actually testing. Your shipped
<code>tests/errors.rs</code> does the second half already; the drill has you write the first.</p>
<p>Which means, honestly, that <code>#[should_panic]</code> has almost no place in your crate — and that is a
result, not a gap. Since 0006 your code returns <code>TaskError</code> instead of panicking, so there is no
panic left to pin. Learn the attribute because interview questions and other people's crates use it; reach for
the <code>Result</code> form in your own.</p>
<h2>Part 4 — Where tests live, and what each kind can see</h2>
<p>Rust has exactly two homes for tests, and the choice is not stylistic — it decides what your test is allowed
to touch:</p>
<blockquote>
<p><em>Unit tests</em> are small and more focused, testing one module in isolation at a time, and can test
private interfaces. <em>Integration tests</em> are entirely external to your library and use your code in the
same way any other external code would, using only the public interface and potentially exercising multiple
modules per test.</p>
</blockquote>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 — Test
organization</a></p>
<p>A unit test lives in the same file as the code it tests, at the bottom, in a module with two attributes'
worth of ceremony:</p>
<pre><code>#[cfg(test)] // compile this only for `cargo test`
mod tests {
use super::*; // pull the whole parent module into scope
#[test]
fn the_private_cap_holds_both_ends() {
assert_eq!(capped(99), MAX); // private fn, reachable
assert_eq!(capped(-40), MIN);
assert_eq!(capped(21), 21);
}
}</code></pre>
<p>Both lines earn their place. <code>#[cfg(test)]</code> means the module is not compiled into
<code>cargo build</code> output at all, so tests cost nothing in the shipped binary. <code>use super::*</code>
is what gives the test its reach: the <code>tests</code> module is an ordinary child module, and a child may
see its parent's private items — which is the entire reason unit tests can test private functions. No
annotation grants that privilege; the module tree does, exactly as chapter 7 described it.</p>
<p>An integration test lives in <code>tests/</code>, and gets a very different deal. Each file there is
compiled as its own separate crate that <code>use</code>s yours from outside, so it sees precisely what a
stranger on crates.io would see. Ask for anything private and the compiler says so — this is a real
<code>cargo test</code> run of a <code>tests/outside.rs</code> that tries both:</p>
<pre><code>error[E0603]: function `capped` is private
--&gt; tests/outside.rs:7:25
|
7 | assert_eq!(heating::capped(99), 30); // so is the helper
| ^^^^^^ private function
|
note: the function `capped` is defined here
--&gt; src/lib.rs:48:1
|
48 | fn capped(degrees: i32) -&gt; i32 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0616]: field `target` of struct `Thermostat` is private
--&gt; tests/outside.rs:6:18
|
6 | assert_eq!(t.target, 20); // the field is private
| ^^^^^^ private field
|
help: a method `target` also exists, call it with parentheses
|
6 | assert_eq!(t.target(), 20); // the field is private
| ++</code></pre>
<p>You have met <code>E0616</code> before from the other side. In 0003 you made <code>Store.tasks</code>
private and added the <code>tasks()</code> accessor, and every one of my 46 tests goes through that accessor
because it has no choice. So the trade is now concrete: put a test in <code>src/</code> and it can reach
inside; put it in <code>tests/</code> and it is forced to use the API you actually ship, which means it also
notices when you break that API. Write both kinds, for different reasons — the file-local ones to pin down
awkward internals, the external ones to pin down the contract.</p>
<p>Sharing a helper between two integration files has one gotcha, and it is worth spending a paragraph on
because you will hit it in the drill. Since every file in <code>tests/</code> is its own crate, a
<code>tests/common.rs</code> full of helpers is compiled as a <em>test crate of its own</em> and shows up in
the output as a pointless <code>running 0 tests</code> section. The fix is the older module-file spelling:</p>
<pre><code>tests/
├── common/
│ └── mod.rs ← helpers live here; not treated as a test crate
├── cli.rs ← `mod common;` then `common::three_tasks()`
└── mine.rs ← same, its own crate, its own copy</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 —
Submodules in integration tests</a>: “Files in subdirectories of the <em>tests</em> directory don't get compiled
as separate crates or have sections in the test output.”</p>
<p>And now the rule this whole lesson turns on. It is one paragraph in the book, and it explains your
<code>stats</code> bug exactly:</p>
<blockquote>
<p>If our project is a binary crate that only contains a <em>src/main.rs</em> file and doesn't have a
<em>src/lib.rs</em> file, we can't create integration tests in the <em>tests</em> directory and bring functions
defined in the <em>src/main.rs</em> file into scope with a <code>use</code> statement. … This is one of the
reasons Rust projects that provide a binary have a straightforward <em>src/main.rs</em> file that calls logic
that lives in the <em>src/lib.rs</em> file.</p>
</blockquote>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 —
Integration tests for binary crates</a></p>
<p>Your crate has both files, which is why the tests can see <code>Store</code> at all. But your
<code>run</code> function — the one that decides the order of the <code>stats</code> lines and whether
<code>clear</code> says anything — is defined in <code>main.rs</code>, on the wrong side of that wall. No test
can import it. The book's advice is the fix: <code>main.rs</code> should be small enough that it needs no
test, and everything else belongs in the library. Step 3 of the drill moves <code>run</code> across.</p>
<p>Moving it is not enough on its own, though, and the second half is the more useful trick. A <code>run</code>
that calls <code>println!</code> writes to the process's stdout, which a test cannot read. So instead of
printing, take the destination as a parameter:</p>
<pre><code>pub fn run(
args: &amp;[String],
store: &amp;mut Store,
out: &amp;mut impl Write, // std::io::Write
) -&gt; Result&lt;(), TaskError&gt;</code></pre>
<p><code>main</code> hands it <code>io::stdout().lock()</code> and behaves exactly as before. A test hands it
a <code>Vec&lt;u8&gt;</code>, which implements <code>Write</code>, and then asserts on the bytes. That is the
whole technique: <strong>a function that returns or writes its output can be tested; a function that prints its
output cannot.</strong> It costs one parameter, and it is the single most reusable idea in this lesson —
the same move makes an HTTP handler testable without a server, and it is the answer to the interview question
“how would you test that?”</p>
<h2>Part 5 — Running them: the flags worth knowing</h2>
<p>Everything so far assumed a bare <code>cargo test</code>. Four flags cover the rest of daily use, and the
first thing to know is where the separator goes: arguments before <code>--</code> are read by cargo,
arguments after it are read by the test binary cargo just built.</p>
<table>
<tr><th>Command</th><th>What it does</th></tr>
<tr><td><code>cargo test warmer</code></td><td>runs tests whose full name contains <code>warmer</code></td></tr>
<tr><td><code>cargo test --lib</code></td><td>only the unit tests inside <code>src/</code></td></tr>
<tr><td><code>cargo test --test cli</code></td><td>only <code>tests/cli.rs</code></td></tr>
<tr><td><code>cargo test -- --show-output</code></td><td>also print stdout from tests that passed</td></tr>
<tr><td><code>cargo test -- --ignored</code></td><td>only the tests marked <code>#[ignore]</code></td></tr>
<tr><td><code>cargo test -- --test-threads=1</code></td><td>no parallelism</td></tr>
</table>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-02-running-tests.html">11.2 —
Controlling how tests are run</a></p>
<p>Filtering matches on the whole test name, module path included, which is the payoff of that
<code>tests::</code> prefix from Part 1. A real run, with three of four tests filtered out:</p>
<pre><code>$ cargo test warmer
Running unittests src/lib.rs (target/debug/deps/heating-795830f7b1de3879)
running 1 test
test tests::warmer_never_passes_the_maximum ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s</code></pre>
<p>Output capture is the behaviour that surprises people: a <code>println!</code> in a passing test is
swallowed, and only reappears if the test fails. When you want to see it anyway, ask:</p>
<pre><code>$ cargo test -- --show-output
running 1 test
test tests::warmer_never_passes_the_maximum ... ok
successes:
---- tests::warmer_never_passes_the_maximum stdout ----
target ended at 30
successes:
tests::warmer_never_passes_the_maximum
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</code></pre>
<p><code>#[ignore]</code> is for the test you want to keep but not run every time — the slow one, the one that
needs a network. It takes a reason string, which the runner prints, and the ignored tests are still one command
away:</p>
<pre><code>#[test]
#[ignore = "slow: walks the whole range"]
fn every_legal_target_round_trips() {
for degrees in MIN..=MAX {
let mut t = Thermostat::new(MIN);
t.set_from(&amp;degrees.to_string()).expect("legal target");
assert_eq!(t.target(), degrees);
}
}</code></pre>
<pre><code>$ cargo test
running 4 tests
test tests::every_legal_target_round_trips ... ignored, slow: walks the whole range
test tests::refuses_a_high_target - should panic ... ok
test tests::the_private_cap_holds_both_ends ... ok
test tests::warmer_never_passes_the_maximum ... ok
test result: ok. 3 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
$ cargo test -- --ignored
running 1 test
test tests::every_legal_target_round_trips ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s</code></pre>
<p>The last flag comes with the one hard constraint of the whole chapter. Tests run <em>in parallel</em>, on
threads, by default:</p>
<blockquote>
<p>Because the tests are running at the same time, you must make sure your tests don't depend on each other or
on any shared state, including a shared environment, such as the current working directory or environment
variables.</p>
</blockquote>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-02-running-tests.html">11.2 — Running
tests in parallel or consecutively</a></p>
<p>Your suite obeys this already, and now you can see why it was written that way. Every file-touching test
calls <code>temp_path()</code>, which mixes the process id with an atomic counter to produce a path no other
test will ever use. That is the first solution the book offers — one file per test. <code>--test-threads=1</code>
is the second, and it is a worse one: it is slower, and it hides the coupling instead of removing it. Reach for
it to diagnose a flaky suite, not to fix one. The same reasoning is why no test of yours may set
<code>TASKS_FILE</code>: environment variables are per-process, so a test that sets one is reaching into every
other test running at that moment.</p>
<h2>Part 6 — A test that cannot fail is not a test</h2>
<p>Green tests are not evidence. Forty-six of them were green while <code>stats</code> printed its lines in the
wrong order, and no amount of staring at the count would have told you. The only honest question about a test
suite is: <em>which bugs would it catch?</em></p>
<p>There is a mechanical way to ask it, called mutation testing. Plant a deliberate bug in a copy of the code,
run the suite, and see whether it goes red. A bug the suite notices is <em>killed</em>. A bug it sleeps
through <em>survives</em>, and every survivor is a precise, undeniable description of a missing test. This
lesson ships six of them in <a href="0009-mutants.sh">0009-mutants.sh</a>: it copies your crate to a temp
directory, applies one <code>sed</code> substitution, and runs your tests. Your own files are never
touched.</p>
<p>Here is that script run against your crate exactly as it stands right now, before the drill:</p>
<pre><code>$ bash ~/learn-rust/lessons/0009-mutants.sh ~/learn-rust/tasks
crate: /home/tan/learn-rust/tasks
SKIP stats-order src/cli.rs does not exist yet
SKIP stats-zero src/cli.rs does not exist yet
SKIP clear-count src/cli.rs does not exist yet
SURVIVED list-format src/task.rs
SURVIVED status-parse src/task.rs
SURVIVED command-case src/command.rs
0 killed, 3 survived, 3 skipped</code></pre>
<p>Read the six lines as a to-do list, because that is what they are. The three <code>SKIP</code>s are the
mutations that live in <code>src/cli.rs</code> — the file you have not written yet, which is where
<code>run</code> is going. The three <code>SURVIVED</code>s are real bugs your 46 tests cannot see today:
<code>Display for Task</code> could stop printing the priority, <code>Status::parse</code> could stop
understanding <code>in-progress</code>, and <code>Command::parse</code> could stop accepting
<code>ADD</code> in capitals, and every test would still pass. The drill's finishing condition is
<code>6 killed, 0 survived</code>.</p>
<p>One caveat, so you calibrate the tool correctly rather than worshipping it: a suite that kills every mutant
is not a proven-correct suite, because my six mutants are not every possible bug. Mutation testing gives you a
floor, not a ceiling. It is still the sharpest feedback available on a suite you just wrote, and it is far
better than counting tests.</p>
<h2>Check yourself before the drill</h2>
<p>Six questions before you touch the keyboard. Answer each one out loud, in full sentences, before you reveal
or click. Two of them revisit 0005–0008 rather than today's material, which is deliberate — retrieval of old
work is what keeps it.</p>
<div class="q" data-type="mcq" data-topic="Tests">
<p class="topic">Tests</p>
<p class="prompt">What actually makes a <code>#[test]</code> function fail?</p>
<div class="options">
<button class="opt" data-correct="false">It returns <code>false</code></button>
<button class="opt" data-correct="true">Its thread panics</button>
<button class="opt" data-correct="false">An <code>assert!</code> returns an error</button>
<button class="opt" data-correct="false">It prints to stderr</button>
</div>
<div class="explain hidden">A test fails when something in it panics; each test runs on its own thread, and the harness marks the test failed when that thread dies. Every assertion macro is a wrapper that panics when its condition does not hold — which is why <code>unwrap</code> in a test body is fine, and why a test may also fail by returning <code>Err</code> from a <code>Result</code>-returning test.</div>
</div>
<div class="q" data-type="recall" data-topic="Tests">
<p class="topic">Tests</p>
<p class="prompt">A unit test and an integration test: where does each file live, and what can each one reach?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">A unit test lives at the bottom of the source file it tests, in <code>#[cfg(test)] mod tests</code> with <code>use super::*</code>; because it is a child module it can reach its parent's private items — private functions, private fields. An integration test lives in <code>tests/</code>, is compiled as its own separate crate, and can only reach the public API, exactly as an outside user would. Reaching for a private item from there gives <code>E0603</code> (private function) or <code>E0616</code> (private field).</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="mcq" data-topic="Tests">
<p class="topic">Tests</p>
<p class="prompt">You write a test that returns <code>Result&lt;(), TaskError&gt;</code> and want to assert a call fails. What is the correct move?</p>
<div class="options">
<button class="opt" data-correct="true"><code>assert!(call().is_err())</code></button>
<button class="opt" data-correct="false">Add <code>#[should_panic]</code></button>
<button class="opt" data-correct="false"><code>call()?</code> and expect red</button>
<button class="opt" data-correct="false">Return <code>Err</code> from the test</button>
</div>
<div class="explain hidden"><code>#[should_panic]</code> is not allowed on a test that returns <code>Result</code>, and <code>?</code> on the failing call would turn the expected failure into a test failure. Use <code>?</code> only for setup steps that must succeed, and assert the interesting failure explicitly with <code>assert!(…is_err())</code> or <code>assert_eq!(…unwrap_err(), TaskError::NotFound(9))</code>.</div>
</div>
<div class="q" data-type="recall" data-topic="Traits">
<p class="topic">Traits</p>
<p class="prompt">Why can <code>assert_eq!</code> compare and print two <code>Task</code> values, and why did <code>TaskError</code> need a hand-written <code>PartialEq</code>?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden"><code>assert_eq!</code> compares with <code>==</code>, so it needs <code>PartialEq</code>, and prints the two values with debug formatting on failure, so it needs <code>Debug</code>. <code>Task</code> derives both. <code>TaskError</code> cannot derive <code>PartialEq</code> because it holds an <code>io::Error</code>, which does not implement it — so 0007 wrote the impl by hand and compared <code>kind()</code> for that variant. The derives are a testing requirement, not decoration.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="mcq" data-topic="Collections">
<p class="topic">Collections</p>
<p class="prompt">Why can a test not assert on <code>count_by_priority()</code> by iterating the map and printing as it goes?</p>
<div class="options">
<button class="opt" data-correct="true"><code>HashMap</code> order is arbitrary</button>
<button class="opt" data-correct="false">Iterating a map borrows it mutably</button>
<button class="opt" data-correct="false">The map is not <code>PartialEq</code></button>
<button class="opt" data-correct="false">Tests cannot iterate a <code>HashMap</code></button>
</div>
<div class="explain hidden">Iteration order over a <code>HashMap</code> is arbitrary and may differ between runs, so any assertion on the sequence is a coin toss. Assert on the map as a whole (it is <code>PartialEq</code>), or ask for specific keys, or — as <code>stats</code> does — impose your own order by looping over <code>[High, Medium, Low]</code> and asking the map for each. That last one is exactly the line the drill makes testable.</div>
</div>
<div class="q" data-type="recall" data-topic="Modules">
<p class="topic">Modules</p>
<p class="prompt">Your <code>run</code> lives in <code>src/main.rs</code>. Why can no file in <code>tests/</code> import it, and what are the two changes that make its output testable?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Only library crates expose items for other crates to <code>use</code>; a binary crate is meant to be run, so nothing in <code>main.rs</code> is importable from <code>tests/</code> — that is why <code>main.rs</code> should stay thin and the logic should live in the library. Two changes: move <code>run</code> into the library (<code>src/cli.rs</code>, declared in <code>lib.rs</code>), and give it an <code>out: &amp;mut impl Write</code> parameter instead of calling <code>println!</code>, so <code>main</code> can pass <code>io::stdout().lock()</code> while a test passes a <code>Vec&lt;u8&gt;</code> and asserts on the bytes.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div id="summary"><div id="summary-body"></div><p id="summary-total"></p>
<button id="report-btn">Copy report</button><pre id="report-output" class="hidden"></pre></div>
<h2>The drill — 45 minutes, your own crate</h2>
<p>Type it, do not paste it. The thermostat above is a different program. Keep the
<a href="../reference/rust-syntax.html#tests">tests reference</a> open — looking syntax up is free.</p>
<pre><code>cd ~/learn-rust/tasks
cargo test # 46 pass, as they did yesterday
bash ../lessons/0009-mutants.sh . # 0 killed, 3 survived, 3 skipped</code></pre>
<p>Those two lines are the starting position. Every test you write today is yours — I am shipping no new spec
file, because the skill being built is writing the assertions rather than satisfying them. The finishing line
is the mutant report reading <code>6 killed, 0 survived, 0 skipped</code>, and all 46 existing tests still
green.</p>
<h3>Step 0 — the last 0008 leftover, one minute</h3>
<p>Delete the commented-out <code>for</code> loop still sitting inside <code>Store::find</code>. The iterator
version is one line above it and git remembers the old one.</p>
<p><strong>Check:</strong> <code>grep -c "for " src/store.rs</code> prints <code>0</code>.</p>
<h3>Step 1 — your first <code>#[test]</code>, in <code>src/task.rs</code></h3>
<p>Add a <code>#[cfg(test)] mod tests</code> at the bottom of <code>src/task.rs</code> with three tests, and
one more at the bottom of <code>src/stats.rs</code>. All four target behaviour that none of my 46 tests
touches — that is why they are worth your keystrokes rather than being duplicates.</p>
<p>The first pins the <em>line format on disk</em>: build a <code>Task</code> with a known id, title, priority
and status, assert that <code>to_line()</code> produces exactly the string you expect, and assert that parsing
that string back gives the task you started with. Both directions in one test, because a round trip that only
goes one way proves nothing about the other.</p>
<p>The second pins <em>every</em> <code>Status</code> label, not just the two the CLI uses. Loop over the three
variants, and for each one assert that <code>Status::parse(status.label())</code> gives that variant back. Your
<code>in-progress</code> arm is currently unreachable from the CLI and therefore completely untested — the loop
covers it without you writing three near-identical tests. Give the assertion a failure message naming the
label, per Part 2, or a red run will not say which variant broke.</p>
<p>The third pins <em>what the user reads</em>: the <code>Display</code> impl you wrote in 0005. Assert the
exact line for a fresh task, then set its status to <code>Done</code> and assert the line again. Nothing in the
suite has ever checked this string.</p>
<p>The fourth, in <code>src/stats.rs</code>, calls <code>tally</code> with a key that is <em>owned</em> rather
than <code>Copy</code> — a closure returning <code>String</code> — and asserts both a count and the map's
length. Your <code>count_by_priority</code> only ever hands <code>tally</code> a <code>Copy</code> key, so the
generic function has never been exercised with anything else.</p>
<p><strong>Check:</strong> <code>cargo test --lib</code> → 4 passed. Note the names in the output:
<code>task::tests::…</code> and <code>stats::tests::…</code>.</p>
<details>
<summary>Forgotten what the test module looks like?</summary>
<p><code>#[cfg(test)]</code> then <code>mod tests {</code> then <code>use super::*;</code> — Part 4 has the
whole shape. Without <code>use super::*</code> you get <code>E0433: failed to resolve</code> on the first type
name, because the child module starts with an empty scope.</p>
</details>
<h3>Step 2 — shared helpers, and tests that return <code>Result</code></h3>
<p>Create <code>tests/common/mod.rs</code> — the directory spelling from Part 4, not
<code>tests/common.rs</code> — holding three helpers you will use from two files: <code>temp_path()</code>
(copy the one from the top of <code>tests/collections.rs</code>; a helper worth sharing is a helper worth
moving), <code>args(&amp;[&amp;str]) -&gt; Vec&lt;String&gt;</code>, and <code>three_tasks() -&gt; Store</code>
which returns a store holding one task per priority with task 1 already completed. Put
<code>#![allow(dead_code)]</code> at the top of the file: each test crate uses only some of the helpers, and
without it the unused ones warn.</p>
<p>Then write <code>tests/mine.rs</code> with three tests, each returning
<code>Result&lt;(), TaskError&gt;</code> so the setup steps can use <code>?</code>:</p>
<p><strong>Completing a task that is already done is not an error.</strong> Complete task 1 a second time and
assert it is still <code>Done</code>. Your <code>complete</code> takes this path today; the test decides that
the behaviour is deliberate rather than accidental, which is what a test is for.</p>
<p><strong>A reload sees exactly what was saved.</strong> Take <code>three_tasks()</code>, clear the completed
one, save to a <code>temp_path()</code>, load it back, and assert the loaded tasks equal the ones in memory.
The shipped suite tests save-then-load, but never after a removal.</p>
<p><strong>Saving a smaller store shortens the file.</strong> Save three tasks, remove the completed one, save
again to the same path, then read the file with <code>fs::read_to_string</code> and assert it has two lines. If
<code>save</code> ever stops truncating, this is the only test that will notice — and a save that appends
instead of replacing is a data-loss bug, not a cosmetic one.</p>
<p><strong>Check:</strong> <code>cargo test --test mine</code> → 3 passed, and a full <code>cargo test</code>
shows <em>no</em> <code>Running tests/common</code> section. If you see one, you named the file
<code>tests/common.rs</code>.</p>
<h3>Step 3 — move <code>run</code> into the library</h3>
<p>This is the structural step, and the point of it is Part 4's rule: nothing in <code>main.rs</code> can be
tested, so almost nothing should live there.</p>
<p>Create <code>src/cli.rs</code>, declare it in <code>lib.rs</code>, and move <code>run</code> into it with
this signature:</p>
<pre><code>pub fn run(
args: &amp;[String],
store: &amp;mut Store,
out: &amp;mut impl Write,
) -&gt; Result&lt;(), TaskError&gt;</code></pre>
<p>Replace every <code>println!(..)</code> in the body with <code>writeln!(out, ..)?</code>. The
<code>?</code> is doing real work there: <code>writeln!</code> returns <code>io::Result</code>, and your
<code>From&lt;io::Error&gt; for TaskError</code> from 0008's step 0 converts it — the second time that impl has
paid for itself. Then <code>main</code> becomes: build the path, load the store, collect the args, take
<code>io::stdout().lock()</code>, call <code>run</code>, save, and <code>fail</code> on either error.</p>
<p>While the code is open, fix the two defects from the top of this page. <code>stats</code> loops over
<code>[Priority::High, Priority::Medium, Priority::Low]</code> — fully qualified, in that order — and prints
each with <code>writeln!(out, "{:&lt;6} {}", p.label(), n)?</code>, so the counts line up in a column.
<code>clear</code> keeps the <code>usize</code> that <code>remove_completed</code> returns and prints
<code>cleared N completed</code>. Those exact formats are what the tests in step 4 assert, and they are the
output the 0008 session captured.</p>
<p><strong>Check:</strong> <code>grep -c "println!" src/cli.rs</code> prints <code>0</code>, and the CLI still
behaves — from a scratch directory, with
<code>run(){ TASKS_FILE=t.txt cargo run -q --manifest-path ~/learn-rust/tasks/Cargo.toml -- "$@"; }</code>:</p>
<pre><code>$ run add "buy milk" high ; run add "call bank" ; run add "water plants" low
$ run done 1
$ run stats
high 1
medium 1
low 1
$ run clear
cleared 1 completed
$ run done 9 ; echo $?
error: no task with id 9
1</code></pre>
<h3>Step 4 — the tests that catch what 46 could not</h3>
<p>Write <code>tests/cli.rs</code>. Start with a helper that runs one command against a store and gives back
exactly what it printed — a <code>Vec&lt;u8&gt;</code> for <code>out</code>, then
<code>String::from_utf8</code>:</p>
<pre><code>fn output(command: &amp;[&amp;str], store: &amp;mut Store) -&gt; String {
let mut out: Vec&lt;u8&gt; = Vec::new();
run(&amp;args(command), store, &amp;mut out).expect("command must succeed");
String::from_utf8(out).expect("output must be utf-8")
}</code></pre>
<p>Then six tests, each asserting on the whole printed string with <code>assert_eq!</code> rather than
searching it for a substring — an exact assertion is what kills the mutants, and the escaped
<code>\n</code>s are part of the contract:</p>
<ul>
<li><code>stats</code> on a store with one task per priority prints high, then medium, then low.</li>
<li><code>stats</code> on a store with only a medium task prints <code>0</code> for the other two, on their own
lines, rather than omitting them.</li>
<li><code>clear</code> reports how many it deleted, and reports <code>0</code> the second time.</li>
<li><code>list</code> prints one line per task in insertion order.</li>
<li>A command that fails prints <strong>nothing at all</strong> — assert the <code>Err</code> is
<code>TaskError::NotFound(9)</code> and that <code>out</code> is still empty. Errors are <code>main</code>'s
job, on stderr.</li>
<li>The command word is case-insensitive: <code>ADD</code> and <code>List</code> work, because
<code>Command::parse</code> lowercases it. Untested until now, and one of the surviving mutants.</li>
</ul>
<p><strong>Check:</strong> <code>cargo test --test cli</code> → 6 passed. Full <code>cargo test</code> → 4 + 6 +
14 + 7 + 3 + 8 + 17 = <strong>59 passed</strong>, of which 13 are yours.</p>
<h3>Step 5 — hunt the mutants</h3>
<p>Run the script against your crate. Every one of the six should now be reported <code>killed</code>:</p>
<pre><code>$ bash ../lessons/0009-mutants.sh .
killed stats-order src/cli.rs
killed stats-zero src/cli.rs
killed clear-count src/cli.rs
killed list-format src/task.rs
killed status-parse src/task.rs
killed command-case src/command.rs
6 killed, 0 survived, 0 skipped</code></pre>
<p>If one survives, do not adjust the script — read the mutation it names in the source of the script, work out
which of your tests <em>should</em> have caught it, and fix that test. A survivor is never wrong: it is a bug
that your suite genuinely cannot see. If one says <code>SKIP</code>, the pattern is not in your source, which
usually means you spelled that line differently; the script prints the file so you can compare.</p>
<h3>Then stop</h3>
<p>Not today: doc tests (<code>///</code> examples that run — chapter 14), <code>#[bench]</code>,
<code>assert_cmd</code> and <code>predicates</code> for testing the binary as a subprocess,
<code>proptest</code> for generated inputs, and <code>cargo-mutants</code>, which is the real version of this
lesson's script. Each is a small step from here, and none is on the path to the next gap.</p>
<h2>What this closed</h2>
<p>Chapter 11 moves to <em>produced</em> on the <a href="../reference/book-coverage.html">coverage map</a>, and
with it the last core gap in chapters 1–11. You have now written unit tests, integration tests, shared helpers,
<code>Result</code>-returning tests, and an assertion on a command's exact output — plus the refactor that made
the last one possible, which is the part an interviewer will actually probe.</p>
<p>What is left before the job-ready floor is short, and it is no longer about the book's core:</p>
<ol>
<li><strong>ch 10.3 — lifetimes</strong>, as reading practice. You have now written two without noticing:
<code>titles_with</code> returns <code>Vec&lt;&amp;str&gt;</code> borrowed from <code>&amp;self</code>, and
<code>Status::label</code> returns <code>&amp;str</code> borrowed from <code>&amp;self</code>. Elision filled in
both annotations for you, and reading the explicit form is a two-lesson job at most.</li>
<li><strong><code>serde</code></strong>, which replaces your <code>to_line</code>/<code>FromStr</code> pair
with two derives — worth doing <em>after</em> writing them by hand, which you now have.</li>
<li>Then <code>axum</code>, where the traits from 0005–0008 and the testing from today start paying rent
together: a handler is just a function you can call from a test.</li>
</ol>
<h2>Take it outside</h2>
<p>Here is a question with genuine disagreement behind it, which makes it a good one to ask people rather than
docs. Your <code>output</code> helper asserts on the exact bytes a command prints, which means a wording change
to <code>cleared N completed</code> breaks a test even though nothing is broken for the user. Some engineers
call that a feature — the output <em>is</em> the contract, and changing it should be deliberate. Others call it
a brittle test that will be deleted the first time it is inconvenient, and would assert only that the count
appears somewhere in the line. Ask on <a href="https://users.rust-lang.org">users.rust-lang.org</a> where they
draw that line for CLI output, and what they do differently for output a machine parses versus output a human
reads. The answers will teach you more about test design than any chapter, because it is a taste question and
the book cannot have taste for you.</p>
<h2>The five sentences worth keeping</h2>
<ol>
<li>A test fails when its thread panics; every assertion macro is a wrapper that panics, so
<code>unwrap</code> in a test is a legitimate assertion.</li>
<li>Unit tests live beside the code in <code>#[cfg(test)] mod tests</code> and can see private items;
integration tests live in <code>tests/</code>, are separate crates, and see only the public API.</li>
<li><code>#[should_panic(expected = "…")]</code> tests a panic, a <code>-&gt; Result&lt;(), E&gt;</code> test
lets you <code>?</code> the setup, and the two cannot be combined.</li>
<li>Nothing in <code>src/main.rs</code> is testable, so <code>main</code> stays thin and everything else moves
to the library — and a function that writes to <code>&amp;mut impl Write</code> is testable where one that
calls <code>println!</code> is not.</li>
<li>Tests run in parallel and share nothing safely, so give every file-touching test its own path; and judge a
suite by the bugs it kills, never by the number of tests it contains.</li>
</ol>
<footer>
<p><strong>Primary source:</strong> The Rust Book
<a href="https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html">11.1 — How to Write Tests</a>, then
<a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 — Test Organization</a>,
with <a href="https://doc.rust-lang.org/stable/book/ch11-02-running-tests.html">11.2 — Controlling How Tests
Are Run</a> as reference rather than reading. After the drill, skim
<a href="https://doc.rust-lang.org/rustc/tests/index.html">the Tests chapter of the rustc book</a> for the flags
the test binary accepts.</p>
<p>Previous: <a href="0008-iterators-and-hashmap.html">0008 — Iterators, HashMap, generics</a> ·
<a href="0007-files-and-fromstr.html">0007 — Files, io::Error, FromStr</a> ·
<a href="0006-your-own-error-type.html">0006 — Your own error type</a><br />
Reference: <a href="../reference/rust-syntax.html#tests">Tests</a> ·
<a href="../reference/rust-syntax.html#iterators">Iterators</a> ·
<a href="../reference/rust-syntax.html#traits">Traits &amp; generics</a> ·
<a href="../reference/book-coverage.html">Coverage map</a></p>
<p><strong>Ask me things.</strong> Bring the compiler output verbatim. Step 3 is the uncomfortable one: it moves
working code for no reason a user can see, and the payoff only arrives in step 4. If a paragraph did not land,
name it; that is my fault to fix, and cheaper to fix now than mid-drill.</p>
</footer>
<script src="../assets/quiz.js"></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ownership"
version = "0.1.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "ownership"
version = "0.1.0"
edition = "2024"
[dependencies]
+71
View File
@@ -0,0 +1,71 @@
fn main() {
let mut s = String::from("hello");
s.push_str(", world");
let x = calculate_length(&s);
println!("{s} with len {x}");
let y = "lksadjflksajdfc";
println!("{y}");
// {
// let x = 12;
// }
// println!("{x}");
{
let s1 = String::from("hello");
let s2 = s1;
println!("{s2}, world!");
}
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // Because i32 implements the Copy trait,
// x does NOT move into the function,
// so it's okay to use x afterward.
let mut s = String::from("hello");
{
let r1 = &mut s;
println!("{r1}");
let x = r1.len();
println!("{x}");
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
}
fn takes_ownership(some_string: String) {
// some_string comes into scope
println!("{some_string}");
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) {
// some_integer comes into scope
println!("{some_integer}");
} // Here, some_integer goes out of scope. Nothing special happens.
fn calculate_length(s: &String) -> usize {
s.len()
}
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
+136
View File
@@ -0,0 +1,136 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Rust Book coverage map — what is owned, what is missing</title>
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body>
<h1>Coverage map</h1>
<p class="subtitle">Every chapter of The Rust Programming Language against this workspace · updated after lesson 0009</p>
<div class="callout">
The chapter list is taken verbatim from the book's
<a href="https://github.com/rust-lang/book/blob/main/src/SUMMARY.md">table of contents</a>, not from memory.
<strong>Read</strong> means you covered it in your two months. <strong>Produced</strong> means code you wrote in this
workspace uses it correctly — that is the only column that counts for the
<a href="../MISSION.md">mission</a>.
</div>
<h2>The map</h2>
<table>
<tr><th>Ch</th><th>Topic</th><th>State</th><th>Evidence / gap</th></tr>
<tr><td>1–2</td><td>Getting started, guessing game</td><td>Produced</td>
<td><code>hello_cargo/</code>, <code>guessing_game/</code></td></tr>
<tr><td>3</td><td>Variables, types, functions, control flow</td><td>Produced</td>
<td><code>variables/</code>, <code>function/</code>, <code>control_flow/</code>; 0001 diagnostic solid</td></tr>
<tr><td>4</td><td>Ownership, borrowing, slices</td><td>Produced</td>
<td><code>&amp;self</code> methods and <code>&amp;[Task]</code> accessor in <code>tasks/</code>. Watch for: you have never hit
a borrow-checker fight in your own multi-owner code, because nothing has needed two owners yet.</td></tr>
<tr><td>5</td><td>Structs and methods</td><td>Produced</td><td>0003 / 0004, <code>tasks/src/task.rs</code></td></tr>
<tr><td>6</td><td>Enums and <code>match</code></td><td>Produced</td>
<td><code>Status</code>, <code>Priority</code>, <code>Command</code>, and now <code>TaskError</code> (0006)</td></tr>
<tr><td>7</td><td>Packages, crates, modules</td><td>Produced</td>
<td><code>tasks/</code> is a lib + bin package with four modules</td></tr>
<tr><td>8</td><td>Collections: <code>Vec</code>, <code>String</code>, <code>HashMap</code></td><td>Produced</td>
<td>0008 drill, in <code>tasks/</code>: <code>HashMap&lt;Priority, usize&gt;</code> built with the <code>entry</code>/<code>or_insert</code>
counting idiom, <code>get().copied().unwrap_or(0)</code>, <code>Eq + Hash + Copy</code> derives on a key type, and a
<code>String</code> built by <code>collect</code>. Not done: <code>BTreeMap</code>, <code>HashSet</code>, string slicing by byte index.</td></tr>
<tr><td>9</td><td>Error handling: <code>panic!</code>, <code>Result</code>, <code>?</code></td><td>Produced</td>
<td>0005 + 0006: <code>?</code>, <code>ok_or</code>, own error enum, stderr + exit 1</td></tr>
<tr><td>10.1</td><td>Generic data types</td><td>Produced</td>
<td>0008 drill: <code>fn tally&lt;T, K, F&gt;(items: &amp;[T], key: F) -&gt; HashMap&lt;K, usize&gt;</code> in <code>tasks/src/stats.rs</code>,
with a <code>where</code> clause bounding <code>K: Eq + Hash</code> and <code>F: Fn(&amp;T) -&gt; K</code> — written from the
signature up, called at three different <code>T</code>/<code>K</code> pairs, one of them a non-<code>Copy</code> key.
Not done: generic <em>structs</em> and generic <code>impl</code> blocks.</td></tr>
<tr><td>10.2</td><td>Traits</td><td>Produced</td>
<td><code>Display for Task</code> (0005), <code>Display</code> + <code>Error</code> + <code>From</code> for <code>TaskError</code> (0006),
<code>FromStr for Task</code> with an associated <code>type Err</code> and a hand-written <code>PartialEq</code> (0007)</td></tr>
<tr><td>10.3</td><td>Lifetimes</td><td class="gap">Gap — untouched</td>
<td>Zero exposure in this workspace. You have dodged it by owning everything (<code>String</code>, not <code>&amp;'a str</code>).
Needed to read other people's code and to review a PR; not needed to ship your CLI.</td></tr>
<tr><td>11</td><td>Writing automated tests</td><td class="gap">Taught — drill pending</td>
<td>0009: <code>#[test]</code>, <code>#[cfg(test)] mod tests</code> with <code>use super::*</code>, the three assertion macros
and their failure output, <code>#[should_panic(expected)]</code> vs a <code>-&gt; Result&lt;(), E&gt;</code> test, unit vs
integration access (<code>E0603</code>/<code>E0616</code>), <code>tests/common/mod.rs</code>, the runner flags, and why
<code>src/main.rs</code> is untestable — <code>run</code> moves to the library and writes to <code>&amp;mut impl Write</code>.
Graded by six planted mutants, not by a test count.</td></tr>
<tr><td>12</td><td>I/O project: args, files, stderr, env</td><td>Produced</td>
<td>0007: <code>fs::read_to_string</code>/<code>fs::write</code>, <code>io::Error</code> + <code>ErrorKind::NotFound</code> match guard,
<code>env::var</code> with a default, on top of <code>env::args</code> and stderr + exit 1 from 0005. Not done: <code>BufReader</code>,
file locking, serde — none needed at this size.</td></tr>
<tr><td>13</td><td>Closures and iterators</td><td>Produced</td>
<td>0008 drill: <code>filter</code>/<code>map</code>/<code>collect</code> chains, <code>find</code> vs <code>position</code>,
<code>Vec::retain</code>, <code>lines()</code>, <code>collect::&lt;Result&lt;Vec&lt;_&gt;, E&gt;&gt;()</code> short-circuiting, and a
function that <em>takes</em> a closure (<code>F: Fn</code>). Not done: <code>fold</code>, <code>zip</code>,
<code>impl Iterator</code> for your own type.</td></tr>
<tr><td>14</td><td>Cargo, crates.io, workspaces</td><td>Skip for now</td>
<td>Learn <code>cargo add</code> and features when a dependency is actually needed (serde, axum).</td></tr>
<tr><td>15</td><td>Smart pointers: <code>Box</code>, <code>Rc</code>, <code>RefCell</code>, <code>Deref</code>, <code>Drop</code></td><td>Partial</td>
<td><code>Box&lt;dyn Error&gt;</code> met in 0005/0006. <code>Rc</code>/<code>RefCell</code> untouched — reach for them only when a real
shared-ownership problem appears, not before.</td></tr>
<tr><td>16</td><td>Concurrency: threads, channels, <code>Send</code>/<code>Sync</code></td><td class="gap">Gap</td>
<td>Untouched apart from the <code>Send + Sync</code> bound your 0006 test asserts. Prerequisite for understanding
why an axum handler must be <code>Send</code>.</td></tr>
<tr><td>17</td><td>Async: futures, <code>async</code>/<code>await</code>, streams</td><td class="gap">Gap — by design</td>
<td>Required for axum/tokio, deliberately last. The unused <code>trpl</code> dependency in <code>get-dependecies/</code> is
the abandoned first attempt. Do it after 8, 11, 13.</td></tr>
<tr><td>18</td><td>Trait objects, OOP patterns</td><td>Partial</td>
<td><code>dyn Error</code> is the same mechanism as 18.2. The state-machine pattern (18.3) is optional reading.</td></tr>
<tr><td>19</td><td>Patterns and matching</td><td>Partial</td>
<td><code>match</code> ✓, <code>if let</code> ✓ (0005 drill). Guards (<code>Some(x) if x &gt; 5</code>), <code>@</code> bindings,
<code>let ... else</code>, and <code>matches!</code> — one page of reading, high value per minute.</td></tr>
<tr><td>20</td><td>Unsafe, advanced traits, macros</td><td>Skip</td>
<td>Associated types and generic-parameter defaults matter eventually (serde uses them). Not now.</td></tr>
<tr><td>21</td><td>Final project: multithreaded web server</td><td>Not started</td>
<td>The natural capstone before axum — it is a backend service with no framework.</td></tr>
</table>
<h2>The order that follows from this</h2>
<p>After the 0009 drill, one gap in the book's core is left, and it is the mildest one:</p>
<ol>
<li><strong>Lifetimes</strong> (ch 10.3) — as reading practice, not as a build. You have written two without
noticing: <code>titles_with</code> returns <code>Vec&lt;&amp;str&gt;</code> borrowed from <code>&amp;self</code>, and
<code>Status::label</code> returns <code>&amp;str</code> the same way. Elision filled both annotations in. Reading
other people's signatures needs the explicit form.</li>
<li><strong>Patterns</strong> (ch 19) — one page: match guards, <code>@</code> bindings, <code>matches!</code>,
<code>let … else</code>. Your suite already uses <code>matches!</code>; the rest is high value per minute.</li>
</ol>
<p>Then serde → axum → async, where the traits from 0005–0008 and the testing from 0009 stop being an exercise
and start being the whole API surface: an axum handler is a function a test can call. Chapter 21's web server is
the natural capstone before a framework.</p>
<footer>
<p><strong>Primary source:</strong> <a href="https://doc.rust-lang.org/stable/book/">The Rust Programming Language</a> ·
chapter list from <a href="https://github.com/rust-lang/book/blob/main/src/SUMMARY.md">SUMMARY.md</a>.</p>
<p>Reference: <a href="rust-syntax.html">Rust syntax reference</a> ·
Lessons: <a href="../lessons/0006-your-own-error-type.html">0006</a> ·
<a href="../lessons/0007-files-and-fromstr.html">0007</a> ·
<a href="../lessons/0008-iterators-and-hashmap.html">0008</a> ·
<a href="../lessons/0009-writing-your-own-tests.html">0009</a></p>
<p>Disagree with a "Produced" or a "Gap"? Say so — this table decides what gets taught next, so it is worth arguing about.</p>
</footer>
</body>
</html>
+720
View File
@@ -0,0 +1,720 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Rust Syntax Reference — ch. 1–13</title>
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body>
<h1>Rust syntax reference</h1>
<p class="subtitle">The compressed essence of Rust Book ch. 1–13 · built for lookup while typing, not for reading</p>
<div class="callout">
Keep this open in a tab while you write code. Every line here is something you already met in ch1–13 — the point is to stop it blocking you mid-sentence.
</div>
<h2>Cargo commands</h2>
<pre><code>cargo new my_proj # create project (binary)
cargo new my_lib --lib # create library
cargo run # build + run
cargo run -- 95 83 # pass args to your program (after --)
cargo build # build only (debug)
cargo build --release # optimized build
cargo check # typecheck fast, no binary
cargo test # run #[test] functions
cargo add rand # add a dependency</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch01-03-hello-cargo.html">1.3 Hello, Cargo!</a></p>
<h2>Variables</h2>
<pre><code>let x = 5; // immutable
let mut y = 5; // mutable
y = 6; // ok, y is mut
const MAX: u32 = 100; // const: always typed, UPPER_CASE
let x = 5;
let x = x + 1; // shadowing: new variable, same name</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch03-01-variables-and-mutability.html">3.1</a></p>
<h2>Types</h2>
<pre><code>i8 i16 i32 i64 i128 isize // signed ints (i32 = default)
u8 u16 u32 u64 u128 usize // unsigned ints (usize = index/len type)
f32 f64 // floats (f64 = default)
bool // true / false
char // 'a' — 4 bytes, Unicode scalar
let t: (i32, f64) = (500, 6.4); // tuple
let (a, b) = t; // destructure
let first = t.0; // index
let arr: [u16; 10] = [0; 10]; // array: fixed length, 10 zeros
let arr = [1, 2, 3]; // inferred [i32; 3]
let s = "hi"; // &str — borrowed, fixed
let s = String::from("hi"); // String — owned, growable
let n: u32 = "42".parse().unwrap(); // str -> number
let n = "42".parse::&lt;u32&gt;().unwrap(); // same, turbofish form
let s = 42.to_string(); // number -> String
let len = arr.len() as u32; // numeric cast</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch03-02-data-types.html">3.2</a></p>
<h2>Functions</h2>
<pre><code>fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon = this is the return value
}
fn shout(msg: &str) -> String {
msg.to_uppercase()
}
fn log(msg: &str) { // no -> means returns ()
println!("{msg}");
}</code></pre>
<p><strong>Statement vs expression:</strong> a line ending in <code>;</code> is a statement (produces no value). Drop the <code>;</code> and it is an expression whose value is returned. <code>return x;</code> works too, but is only idiomatic for early returns.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch03-03-how-functions-work.html">3.3</a></p>
<h2>Control flow</h2>
<pre><code>if n &lt; 5 { ... } else if n &lt; 10 { ... } else { ... }
let label = if n &gt; 0 { "pos" } else { "neg" }; // arms must be same type
loop { break; } // infinite until break
let got = loop { break 7; }; // loop can return a value
while n &lt; 10 { n += 1; }
for i in 0..5 { } // 0,1,2,3,4
for i in 0..=5 { } // 0..5 inclusive
for item in &vec { } // borrow each item
for (i, c) in word.char_indices() { }</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch03-05-control-flow.html">3.5</a></p>
<h2>Ownership — the three rules</h2>
<ol>
<li>Each value has exactly one <strong>owner</strong>.</li>
<li>There can be only one owner at a time.</li>
<li>When the owner goes out of scope, the value is <strong>dropped</strong>.</li>
</ol>
<pre><code>let s1 = String::from("hi");
let s2 = s1; // MOVE — s1 is now invalid
// println!("{s1}"); // compile error: borrow of moved value
let s2 = s1.clone(); // deep copy — both usable, costs allocation
let n1 = 5;
let n2 = n1; // COPY — i32 is Copy, n1 still valid</code></pre>
<p><strong><code>Copy</code></strong> types (stack-only, fixed size): all integers, <code>f32/f64</code>, <code>bool</code>, <code>char</code>, and tuples containing only <code>Copy</code> types. Everything heap-owning (<code>String</code>, <code>Vec</code>) moves instead.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch04-01-what-is-ownership.html">4.1</a></p>
<h2>References &amp; borrowing</h2>
<pre><code>fn length(s: &String) -> usize { s.len() } // borrow, read-only
fn push_bang(s: &mut String) { s.push('!'); } // borrow, mutable
let mut s = String::from("hi");
length(&s); // pass an immutable reference
push_bang(&mut s); // pass a mutable reference</code></pre>
<p><strong>The borrowing rule:</strong> at any one time you may have <em>either</em> one mutable reference <em>or</em> any number of immutable references — never both. Enforced at compile time; this is what rules out data races without a GC.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html">4.2</a></p>
<h2>Slices</h2>
<pre><code>let s = String::from("hello world");
let hello = &s[0..5]; // &str — pointer + length, owns nothing
let world = &s[6..11];
let whole = &s[..];
let arr = [1, 2, 3, 4, 5];
let part: &[i32] = &arr[1..3]; // [2, 3]
fn total(nums: &[u32]) -&gt; u32 { ... } // takes Vec or array — prefer &amp;[T]</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch04-03-slices.html">4.3</a></p>
<h2>Structs</h2>
<pre><code>struct Rectangle { width: u32, height: u32 } // named fields
struct Point(i32, i32); // tuple struct
struct Marker; // unit struct
#[derive(Debug)] // enables {:?} printing
struct User { name: String, active: bool }
let r = Rectangle { width: 30, height: 50 };
println!("{r:?}"); // needs #[derive(Debug)]
impl Rectangle {
fn new(width: u32, height: u32) -> Self { // associated fn (no self)
Self { width, height } // field init shorthand
}
fn area(&self) -> u32 { // method: borrows
self.width * self.height
}
fn scale(&mut self, by: u32) { // method: borrows mutably
self.width *= by;
}
fn consume(self) -> u32 { self.width } // method: takes ownership
}
Rectangle::new(3, 4).area(); // :: for associated fn, . for method</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch05-03-method-syntax.html">5.1–5.3</a></p>
<h2>Enums &amp; pattern matching</h2>
<pre><code>enum Shape {
Circle(f64), // variant with data
Rect { w: f64, h: f64 }, // variant with named fields
Empty, // variant with no data
}
enum Option&lt;T&gt; { Some(T), None } // in std — "maybe a value"
enum Result&lt;T, E&gt; { Ok(T), Err(E) } // in std — "value or error"
match shape {
Shape::Circle(r) =&gt; 3.14 * r * r,
Shape::Rect { w, h } =&gt; w * h,
Shape::Empty =&gt; 0.0,
} // must be exhaustive
match score {
90..=100 =&gt; "A", // range pattern
n if n &gt; 50 =&gt; "pass", // match guard
_ =&gt; "F", // catch-all
}
if let Some(x) = maybe { ... } // one pattern, ignore rest
if let Some(x) = maybe { ... } else { ... }
while let Some(top) = stack.pop() { ... } // loop while pattern matches</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch06-02-match.html">6.1–6.3</a></p>
<h2>Modules &amp; paths</h2>
<pre><code>// src/lib.rs or src/main.rs — the crate root
mod front_of_house; // loads src/front_of_house.rs (or .../mod.rs)
mod hosting { // inline module
pub fn add_to_waitlist() {} // pub = visible outside this module
fn seat() {} // private (default)
}
crate::front_of_house::hosting::add_to_waitlist(); // absolute path
front_of_house::hosting::add_to_waitlist(); // relative path
super::hosting::add_to_waitlist(); // parent module
use crate::front_of_house::hosting; // bring into scope
use std::collections::HashMap;
use std::io::{self, Read}; // multiple items
use std::fmt::Result as FmtResult; // rename
pub use crate::hosting; // re-export</code></pre>
<p><strong>Everything is private by default.</strong> A child module can see its parent's items; a parent needs <code>pub</code> to see into a child. <code>pub</code> on a struct does <em>not</em> make its fields public — each field needs its own <code>pub</code>.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html">7.1–7.5</a></p>
<h3>Packages vs crates — the two-crate package</h3>
<pre><code>my_proj/
├── Cargo.toml # ONE package
├── src/
│ ├── lib.rs # crate 1: the LIBRARY, named after the package
│ ├── thing.rs # a module inside crate 1
│ └── main.rs # crate 2: the BINARY — a separate crate
└── tests/
└── spec.rs # crate 3: integration tests — separate again</code></pre>
<p>Cargo finds all three by filename. No <code>[lib]</code> or <code>[[bin]]</code> in <code>Cargo.toml</code> is needed.</p>
<pre><code>// src/thing.rs — INSIDE the library crate
use crate::other::Thing; // crate:: = root of the crate I am in
// src/main.rs — a DIFFERENT crate
use my_proj::thing::Thing; // must use the package name
// tests/spec.rs — also a different crate
use my_proj::thing::Thing; // same as main.rs</code></pre>
<p><code>use crate::..</code> in <code>main.rs</code> gives <code>error[E0432]: unresolved import</code>. When a path will not resolve, ask: <strong>which crate am I in right now?</strong></p>
<p>Three wiring states, in the order you hit them:</p>
<table>
<tr><th align="left">In <code>lib.rs</code></th><th align="left">Result</th></tr>
<tr><td>nothing</td><td><code>E0433: cannot find `thing`</code> — a file is not a module until declared</td></tr>
<tr><td><code>mod thing;</code></td><td><code>E0603: module `thing` is private</code></td></tr>
<tr><td><code>pub mod thing;</code></td><td>works</td></tr>
</table>
<p>Integration tests in <code>tests/</code> can only reach <code>pub</code> items via the library crate — they cannot see inside <code>main.rs</code> at all. That is the reason to put logic in <code>lib.rs</code> and keep <code>main.rs</code> thin.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch07-01-packages-and-crates.html">7.1 Packages and Crates</a> · <a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 Test Organization</a></p>
<h3>Common derives</h3>
<pre><code>#[derive(Debug, Clone, Copy, PartialEq)]
enum Size { Small, Large }</code></pre>
<table>
<tr><th align="left">Derive</th><th align="left">Gives</th><th align="left">Needed for</th></tr>
<tr><td><code>Debug</code></td><td><code>{:?}</code></td><td>test failure output, logs</td></tr>
<tr><td><code>PartialEq</code></td><td><code>==</code></td><td><code>assert_eq!</code> on your type</td></tr>
<tr><td><code>Clone</code></td><td><code>.clone()</code></td><td>explicit second copy</td></tr>
<tr><td><code>Copy</code></td><td>assignment copies, not moves</td><td>small types, <strong>no heap data</strong></td></tr>
<tr><td><code>PartialOrd, Ord</code></td><td><code>&lt;</code>, <code>.sort()</code></td><td>ordering; enum variant order = the ordering</td></tr>
</table>
<p>A type containing a <code>String</code> or <code>Vec</code> <strong>cannot</strong> be <code>Copy</code>. Forget a derive and the compiler names the exact trait and type.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/appendix-03-derivable-traits.html">Appendix C — Derivable Traits</a></p>
<h2 id="collections">Collections</h2>
<pre><code>// Vec — owned, growable list
let mut v: Vec&lt;u32&gt; = Vec::new();
let v = vec![1, 2, 3];
v.push(4);
let third = &v[2]; // panics if out of range
let third = v.get(2); // returns Option&lt;&u32&gt; — safe
v.get(9).unwrap_or(&0); // default if missing
for n in &v { } // iterate borrowed
for n in &mut v { *n += 1; } // iterate mutably
v.len(); v.is_empty(); v.pop(); v.contains(&3);
// String — owned, growable, UTF-8
let mut s = String::new();
s.push_str("hi"); s.push('!');
let s = format!("{a}-{b}");
let joined = words.join(" ");
for w in text.split_whitespace() { }
for (i, c) in text.char_indices() { }
text.to_uppercase(); text.trim(); text.len(); // len = BYTES, not chars
// HashMap — key/value
use std::collections::HashMap;
let mut m = HashMap::new();
m.insert("a", 10);
m.get("a"); // Option&lt;&i32&gt;
let count = m.entry(key).or_insert(0); // insert-if-absent, returns &mut
*count += 1; // the counting idiom
for (k, v) in &m { }</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch08-01-vectors.html">8.1–8.3</a></p>
<h2 id="iterators">Iterators</h2>
<pre><code>// ONE trait, one method — everything else is a default method built on next()
pub trait Iterator {
type Item;
fn next(&mut self) -&gt; Option&lt;Self::Item&gt;;
}
// THREE ways in — the &self / &mut self / self choice, one element at a time
v.iter() // Item = &T reading; collection survives
v.iter_mut() // Item = &mut T editing in place; collection survives
v.into_iter() // Item = T taking the elements; collection consumed
// ADAPTERS — lazy, return an iterator, compose freely
.map(|x| ..) .filter(|x| ..) .enumerate() .skip(n) .take(n) .rev() .zip(other)
// CONSUMERS — do the work, return something that is not an iterator
.collect() .find(|x| ..) .position(|x| ..) .any(|x| ..) .all(|x| ..)
.count() .sum() .max() .min() .fold(init, |acc, x| ..) .for_each(|x| ..)
// find gives the ITEM, position gives the INDEX — both Option, both take ok_or
list.iter().find(|t| t.id == id).ok_or(NotFound(id))? // -&gt; &T
list.iter().position(|t| t.id == id).ok_or(NotFound(id))? // -&gt; usize
list.retain(|t| t.keep); // Vec method, not Iterator: delete in place, one pass
// COLLECT builds any FromIterator type — you must say which
let v: Vec&lt;String&gt; = it.collect();
let s: String = it.collect(); // String: FromIterator&lt;String&gt;
let m: HashMap&lt;K, V&gt; = pairs.collect(); // from an iterator of (K, V)
let r: Result&lt;Vec&lt;T&gt;, E&gt; = it.collect(); // SHORT-CIRCUITS on the first Err
let o: Option&lt;Vec&lt;T&gt;&gt; = it.collect(); // .. and on the first None
text.lines() // trailing "\n" is a TERMINATOR — no empty last item, strips \r
text.split('\n') // trailing "\n" is a SEPARATOR — yields a final ""</code></pre>
<p><strong>Errors you will see:</strong></p>
<pre><code>warning: unused `Map` that must be used
= note: iterators are lazy and do nothing unless consumed // no consumer
error[E0283]: type annotations needed
| let x = it.collect(); ------- type must be known at this point
= note: multiple `impl`s satisfying `_: FromIterator&lt;String&gt;` found
error[E0502]: cannot borrow `self.v` as immutable because it is also borrowed as mutable
// an iter_mut() result in a variable holds the borrow until its LAST use
error[E0507]: cannot move out of `x.field` which is behind a shared reference
// a closure over &T cannot give away an owned field — Copy, clone, or &str</code></pre>
<p><strong>Cost:</strong> an adapter chain is a tower of small structs compiled to one pass — no temporary
vectors, no runtime penalty against the equivalent <code>for</code> loop.</p>
<p><strong>When a loop still wins:</strong> folding many items into one accumulator you mutate. Iterators
replace loops that search, transform, or collect.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch13-02-iterators.html">13.2 Iterators</a> · std: <a href="https://doc.rust-lang.org/std/iter/trait.Iterator.html">Iterator</a> · <a href="https://doc.rust-lang.org/std/iter/trait.FromIterator.html">FromIterator</a></p>
<h2 id="hashmap-keys">HashMap keys &amp; counting</h2>
<pre><code>use std::collections::HashMap;
// A key must be Eq + Hash: hash to pick the bucket, compare inside it.
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] // Copy for fieldless enums
enum Priority { Low, Medium, High }
let mut m: HashMap&lt;Priority, usize&gt; = HashMap::new();
*m.entry(key).or_insert(0) += 1; // THE counting idiom: one lookup, &mut V back
m.entry(key).or_default(); // same, using Default::default()
m.get(&key).copied().unwrap_or(0); // Option&lt;&usize&gt; -&gt; usize, absent = 0
for (k, v) in &m { } // ARBITRARY order — impose your own before printing
HashMap::from([(Priority::High, 2)]); // literal, for tests</code></pre>
<p><code>Eq</code> is a marker: no methods, one extra promise over <code>PartialEq</code> — every value equals
itself. <code>f64</code> breaks it (<code>NAN != NAN</code>), so <code>f64</code> cannot be a key.</p>
<p>Want sorted keys instead of arbitrary order? <code>BTreeMap</code>, same API, <code>K: Ord</code>.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html">8.3 Hash maps</a> · std: <a href="https://doc.rust-lang.org/std/cmp/trait.Eq.html">Eq</a> · <a href="https://doc.rust-lang.org/std/hash/trait.Hash.html">Hash</a></p>
<h2>Error handling</h2>
<pre><code>panic!("boom"); // unrecoverable — stops the program
// Recoverable: return Result and let the caller decide
fn read_name() -&gt; Result&lt;String, io::Error&gt; {
let mut f = File::open("hello.txt")?; // ? = return Err early
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s) // must wrap success in Ok
}
match File::open("x.txt") {
Ok(f) =&gt; f,
Err(e) =&gt; match e.kind() {
ErrorKind::NotFound =&gt; File::create("x.txt").unwrap(),
_ =&gt; panic!("{e:?}"),
},
};
r.unwrap(); // value, or panic
r.expect("message"); // value, or panic with your message
r.unwrap_or(0); // value, or fallback
r.unwrap_or_else(|e| 0); // value, or compute fallback
r.is_ok(); r.is_err();
fn main() -&gt; Result&lt;(), Box&lt;dyn Error&gt;&gt; { ... } // main can return Result</code></pre>
<p><strong>Rule of thumb:</strong> <code>panic!</code> / <code>unwrap</code> in tests, prototypes, and truly impossible states. <code>Result</code> everywhere else — especially in library code and anything a backend service depends on.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch09-00-error-handling.html">9.1–9.3</a></p>
<h3>The Result pattern</h3>
<pre><code>enum Result&lt;T, E&gt; { Ok(T), Err(E) } // an enum: errors are VALUES
enum Option&lt;T&gt; { Some(T), None } // absence, with no reason attached</code></pre>
<p>Pick the shortest rung that fits:</p>
<pre><code>match r { Ok(v) =&gt; ..., Err(e) =&gt; ... } // full control, both arms required
if let Err(e) = r { ... } // only care about failure
r.unwrap_or(default) // fallback value
r.unwrap_or_else(|e| compute()) // fallback, computed
r.ok() // Result -&gt; Option (drops the reason)
r.map(|v| v + 1) // change Ok, leave Err untouched
r.is_ok() / r.is_err() // just ask
r.unwrap() / r.expect("why") // value, or PANIC
r? // hand the error to my caller</code></pre>
<p><strong>What <code>?</code> really does:</strong></p>
<pre><code>let v = thing()?;
// expands to roughly:
let v = match thing() {
Ok(value) =&gt; value,
Err(e) =&gt; return Err(From::from(e)), // early exit + TYPE CONVERSION
};</code></pre>
<p>The <code>From::from</code> step is why <code>?</code> can mix error types in one function:</p>
<pre><code>fn load_port(path: &str) -&gt; Result&lt;u16, Box&lt;dyn Error&gt;&gt; {
let text = fs::read_to_string(path)?; // io::Error -\
let port = text.trim().parse::&lt;u16&gt;()?; // ParseIntError -&gt; Box&lt;dyn Error&gt;
Ok(port)
}</code></pre>
<p><strong><code>?</code> requires the enclosing function to return <code>Result</code> or <code>Option</code>:</strong></p>
<pre><code>error[E0277]: the `?` operator can only be used in a function that returns `Result`
| cannot use the `?` operator in a function that returns `()`
help: consider adding return type</code></pre>
<p>Fix the signature, not the <code>?</code>. Even <code>main</code> can return <code>Result</code> — end it with <code>Ok(())</code>.</p>
<p><strong>Dropping a Result is a warning, not silence:</strong></p>
<pre><code>warning: unused `Result` that must be used
= note: this `Result` may be an `Err` variant, which should be handled
help: use `let _ = ...` to ignore the resulting value</code></pre>
<p><strong>panic vs propagate:</strong> <code>unwrap</code>/<code>expect</code> in tests, prototypes, and states that truly cannot happen. <code>Result</code> for anything touching files, args, network, or users. In a service: propagate with <code>?</code> through the inner layers, and decide what the user sees at <em>one</em> boundary (the request handler).</p>
<h3 id="error-types">Custom error types — the recipe</h3>
<pre><code>#[derive(Debug)] // Error requires Debug
enum SensorError {
Empty, // no data
NotANumber(ParseFloatError), // wrap the cause
OutOfRange(f64), // keep the bad value
}
impl fmt::Display for SensorError { // the human sentence
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SensorError::Empty =&gt; write!(f, "no reading given"),
SensorError::NotANumber(_) =&gt; write!(f, "reading is not a number"),
SensorError::OutOfRange(v) =&gt; write!(f, "{v} is outside -90..60"),
}
}
}
impl Error for SensorError {} // std::error::Error — marker, no body needed
// …or, when a variant WRAPS another error, hand the cause to source() instead:
impl Error for SensorError {
fn source(&self) -&gt; Option&lt;&amp;(dyn Error + 'static)&gt; {
match self {
SensorError::NotANumber(e) =&gt; Some(e), // the wrapped cause
_ =&gt; None, // nothing underneath
}
}
}
impl From&lt;ParseFloatError&gt; for SensorError { // makes bare `?` convert for you
fn from(e: ParseFloatError) -&gt; SensorError { SensorError::NotANumber(e) }
}</code></pre>
<p><strong>Three obligations, in order:</strong> <code>#[derive(Debug)]</code>, then <code>Display</code>, then the empty <code>impl Error</code>. Skip <code>Display</code> and the marker impl fails:</p>
<pre><code>error[E0277]: `SensorError` doesn't implement `std::fmt::Display`
13 | impl Error for SensorError {}
| ^^^^^^^^^^^ unsatisfied trait bound</code></pre>
<p><strong><code>From</code> is a trait with one method</strong> — <code>fn from(value: T) -&gt; Self</code>, i.e. "how to build me out of a T". <code>String::from("hi")</code> is the same trait. Three equivalent spellings once the impl exists:</p>
<pre><code>Err(SensorError::from(e)) // call it yourself
Err(e.into()) // same trait, from the value's side (Into comes free)
text.parse::&lt;f64&gt;()? // `?` calls From::from(e) for you
// what `?` expands to:
match thing() { Ok(v) =&gt; v, Err(e) =&gt; return Err(From::from(e)) }</code></pre>
<p><strong>Missing <code>From</code> impl, seen through <code>?</code>:</strong></p>
<pre><code>error[E0277]: `?` couldn't convert the error to `SensorError`
28 | Ok(text.parse::&lt;f64&gt;()?)
| --------------^ the trait `From&lt;ParseFloatError&gt;` is not implemented for `SensorError`
// with an annotated `let`, the same missing impl is reported as:
error[E0271]: type mismatch resolving `&lt;f64 as FromStr&gt;::Err == SensorError`
29 | let value: f64 = text.parse()?;
| ^^^^^ expected `SensorError`, found `ParseFloatError`</code></pre>
<p><code>ok_or("text")?</code> in a function returning <code>Result&lt;_, String&gt;</code> already relies on this: std ships <code>impl From&lt;&amp;str&gt; for String</code>.</p>
<p><strong>Reporting it in a CLI</strong> — <code>main -&gt; Result</code> prints with <code>{:?}</code> (Debug), so handle it yourself when a human reads the output:</p>
<pre><code>if let Err(e) = run(&args, &mut store) {
eprintln!("error: {e}"); // stderr, Display
process::exit(1); // status a script can test
}</code></pre>
<p><strong>Wrapped cause: <code>source()</code> or <code>Display</code>, never both.</strong> std's own rule — "the underlying error should be either returned by the outer error's <code>Error::source()</code>, or rendered by the outer error's <code>Display</code> implementation, but not both." So write your one sentence in <code>Display</code>, drop the <code>{e}</code>, and expose the cause through <code>source()</code> for logs and <code>--verbose</code>.</p>
<pre><code>let e = parse_reading("nope").unwrap_err();
e.to_string() // "reading is not a number" &lt;- yours
e.source().map(|s| s.to_string()) // Some("invalid float literal") &lt;- std's
</code></pre>
<p><strong>Reviewer's checklist for an error type</strong> (API Guidelines C-GOOD-ERR): implements <code>Error</code>; is <code>Send + Sync</code> (needed to cross threads, so needed by every web framework); never <code>()</code>; <code>Display</code> message lowercase, no trailing punctuation, concise. Assert the bounds in a test with an empty generic fn:</p>
<pre><code>fn assert_usable_as_error&lt;E: Error + Send + Sync + 'static&gt;() {}
assert_usable_as_error::&lt;TaskError&gt;(); // compile-time check, no body needed</code></pre>
<p><strong>Matching without naming every variant</strong> — <code>matches!</code> returns a bool:</p>
<pre><code>assert!(matches!(err, TaskError::BadId(_))); // true if the shape matches
if matches!(status, Status::Todo | Status::InProgress) { .. }</code></pre>
<p><strong>Adding a variant is a compiler-enforced review:</strong></p>
<pre><code>error[E0004]: non-exhaustive patterns: `&amp;TaskError::StoreFull` not covered
19 | match self {
| ^^^^ pattern `&amp;TaskError::StoreFull` not covered
note: `TaskError` defined here … 14 | StoreFull, --------- not covered</code></pre>
<p>That failure is the reason to prefer an enum over a <code>String</code> — and the reason not to write <code>_ =&gt; ..</code> when matching your own error type at the reporting edge.</p>
<p><strong>Two more errors from the same migration:</strong></p>
<pre><code>error[E0432]: unresolved import `crate::error` // `pub mod error;` missing
error[E0369]: binary operation `==` cannot be applied to type `TaskError`
note: `TaskError` does not implement `PartialEq` // tests use assert_eq!</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch09-02-recoverable-errors-with-result.html">9.2</a> · std: <a href="https://doc.rust-lang.org/std/error/trait.Error.html">Error</a> · <a href="https://doc.rust-lang.org/std/convert/trait.From.html">From</a></p>
<h2 id="files">Files, <code>io::Error</code> &amp; <code>FromStr</code></h2>
<pre><code>use std::fs;
use std::io::{self, ErrorKind};
use std::path::{Path, PathBuf};
fs::read_to_string(path)? // io::Result&lt;String&gt; = Result&lt;String, io::Error&gt;
fs::write(path, text)? // io::Result&lt;()&gt; — creates or truncates, one call
text.lines() // iterator of &amp;str, no trailing newlines
// a path from the environment, with a default
let path = PathBuf::from(
env::var("TASKS_FILE").unwrap_or_else(|_| "tasks.txt".to_string()));</code></pre>
<p><strong>One io failure is not another</strong> — <code>e.kind()</code> returns an <code>ErrorKind</code>. A missing file on first run is expected; everything else is not. Use a <em>match guard</em>:</p>
<pre><code>let text = match fs::read_to_string(path) {
Ok(text) =&gt; text,
Err(e) if e.kind() == ErrorKind::NotFound =&gt; return Ok(Store::new()),
Err(e) =&gt; return Err(TaskError::Io(e)), // permissions, disk, …
};</code></pre>
<p><strong><code>FromStr</code> is the trait behind <code>.parse()</code></strong>. Implement it and <code>.parse::&lt;YourType&gt;()</code> starts working — <code>type Err</code> is an <em>associated type</em>: a slot the implementor fills once, not a parameter the caller passes.</p>
<pre><code>pub trait FromStr: Sized {
type Err;
fn from_str(s: &amp;str) -&gt; Result&lt;Self, Self::Err&gt;;
}
impl FromStr for Task {
type Err = TaskError;
fn from_str(line: &amp;str) -&gt; Result&lt;Task, TaskError&gt; {
let bad = || TaskError::BadLine(line.to_string()); // built on failure
let mut parts = line.splitn(4, '|'); // splitn: last piece keeps its '|'
let id: u32 = parts.next().ok_or_else(bad)?.parse().map_err(|_| bad())?;
..
}
}</code></pre>
<pre><code>error[E0046]: not all trait items implemented, missing: `Err` // no `type Err`
error[E0277]: the trait bound `Task: FromStr` is not satisfied // no impl</code></pre>
<p><strong><code>?</code> vs <code>map_err</code>:</strong> only <em>one</em> <code>impl From&lt;T&gt; for YourError</code> may exist per <code>T</code> (a second is <code>error[E0119]: conflicting implementations</code>). So <code>?</code> handles the canonical meaning, and <code>map_err</code> names the variant for every other meaning of the same error type.</p>
<pre><code>let id: u32 = text.parse()?; // -&gt; BadId, via From
let id: u32 = text.parse().map_err(|_| bad())?; // -&gt; BadLine, chosen here</code></pre>
<p><strong>Not everything derives.</strong> <code>io::Error</code> is not <code>PartialEq</code>, so a variant holding one breaks <code>#[derive(PartialEq)]</code> (<code>E0369</code>). Write it by hand; <code>mem::discriminant</code> covers the payload-free variants:</p>
<pre><code>impl PartialEq for TaskError {
fn eq(&amp;self, other: &amp;Self) -&gt; bool {
match (self, other) { // match on a TUPLE of both
(Io(a), Io(b)) =&gt; a.kind() == b.kind(),
(NotFound(a), NotFound(b)) =&gt; a == b,
_ =&gt; std::mem::discriminant(self) == std::mem::discriminant(other),
}
}
}</code></pre>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch12-02-reading-a-file.html">12.2</a>, <a href="https://doc.rust-lang.org/stable/book/ch12-05-working-with-environment-variables.html">12.5</a> · std: <a href="https://doc.rust-lang.org/std/fs/">fs</a> · <a href="https://doc.rust-lang.org/std/io/enum.ErrorKind.html">ErrorKind</a> · <a href="https://doc.rust-lang.org/std/str/trait.FromStr.html">FromStr</a></p>
<h2 id="traits">Traits &amp; generics</h2>
<pre><code>trait Reading {
fn celsius(&self) -&gt; f64; // REQUIRED — semicolon
fn label(&self) -&gt; String { // DEFAULT — has a body, may be overridden
format!("{:.1}C", self.celsius())
}
}
impl Reading for Kettle { // impl TRAIT for TYPE
fn celsius(&self) -&gt; f64 { (self.fahrenheit - 32.0) * 5.0 / 9.0 }
}
impl fmt::Display for Thermometer { // the trait behind `{}`
fn fmt(&self, f: &mut fmt::Formatter) -&gt; fmt::Result {
write!(f, "{} [{:.1}C]", self.room, self.celsius) // INTO f, no `;`
}
}</code></pre>
<p><strong>Bounds — four spellings, one meaning</strong> ("any T that implements Reading"):</p>
<pre><code>fn show&lt;T: Reading&gt;(r: &T) // angle brackets
fn show(r: &impl Reading) // shorthand
fn show&lt;T&gt;(r: &T) where T: Reading // where clause, for long lists
fn show(r: &dyn Reading) // trait OBJECT: type chosen at runtime
fn show&lt;T: Reading + Clone&gt;(r: &T) // two promises at once</code></pre>
<p><code>Box&lt;dyn Error&gt;</code> is the same <code>dyn</code> idea: "some heap value that implements <code>Error</code>". Use it when the failures are unrelated and an enum is not worth writing.</p>
<p><strong>A generic function of your own</strong> — one definition, one specialised copy compiled per set of
types actually used (monomorphisation), so the generality is free at runtime:</p>
<pre><code>pub fn tally&lt;T, K, F&gt;(items: &[T], key: F) -&gt; HashMap&lt;K, usize&gt;
where
K: Eq + Hash, // required by the HashMap being returned, not by the body
F: Fn(&T) -&gt; K, // each closure has its OWN anonymous type
{
let mut counts = HashMap::new();
for item in items { *counts.entry(key(item)).or_insert(0) += 1; }
counts
}
tally(&tasks, |t| t.priority) // T = Task, K = Priority
tally(&["a", "bb"], |w| w.len()) // T = &str, K = usize</code></pre>
<p>The clause is a contract both ways: callers must satisfy it, and the body may use <em>only</em> what it
promises. <code>Fn</code> = borrows its captures · <code>FnMut</code> = mutates them · <code>FnOnce</code> =
consumes them. Take <code>Fn</code> unless you need more.</p>
<p><strong>Errors you will see:</strong></p>
<pre><code>error[E0046]: not all trait items implemented, missing: `celsius`
| ^^^^^^^^^^^^^^^^^^^^^^^ missing `celsius` in implementation
error[E0277]: `Kettle` doesn't implement `std::fmt::Display`
= note: in format strings you may be able to use `{:?}` instead
error[E0599]: no method named `celsius` found for reference `&&T` in the current scope
= help: items from traits can only be used if the trait is implemented and in scope
note: `Reading` defines an item `celsius`, perhaps you need to implement it</code></pre>
<p>That last one is the missing-bound error: a bare <code>&lt;T&gt;</code> promises nothing, so no method exists on it. Add the bound.</p>
<p><strong>Free consequences of one impl:</strong> <code>Display</code> grants <code>ToString</code> (so <code>.to_string()</code> works); <code>From&lt;A&gt; for B</code> grants <code>A.into(): B</code> and makes <code>?</code> convert.</p>
<p><strong>Orphan rule:</strong> <code>impl Trait for Type</code> is allowed only if the trait or the type is yours. <code>Display for MyTask</code> ✓ · <code>MyTrait for Vec&lt;T&gt;</code> ✓ · <code>Display for Vec&lt;String&gt;</code> ✗.</p>
<p><strong>Trait in scope:</strong> to call a trait method you must have the trait imported (<code>use std::io::Write;</code> before <code>.write_all()</code>). <code>println!("{}")</code> is exempt — the macro names <code>Display</code> by full path.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch10-02-traits.html">10.2 Traits</a> · <a href="https://doc.rust-lang.org/stable/book/ch10-01-syntax.html">10.1 Generics</a> · std: <a href="https://doc.rust-lang.org/std/fmt/trait.Display.html">Display</a></p>
<h2>Printing</h2>
<pre><code>println!("plain text");
println!("{}", value); // Display
println!("{value}"); // inline variable (preferred)
println!("{value:?}"); // Debug — needs #[derive(Debug)]
println!("{value:#?}"); // Debug, pretty-printed
eprintln!("to stderr");</code></pre>
<h3>Debug vs Display</h3>
<p>Two independent traits. A type can have one, both, or neither.</p>
<pre><code>{} {value} needs Display — text for END USERS
{:?} {value:?} needs Debug — text for PROGRAMMERS
{:#?} {value:#?} needs Debug — same, multi-line</code></pre>
<pre><code>let s = String::from("hi\tthere");
println!("{s}"); // hi there <- raw, for a user
println!("{s:?}"); // "hi\tthere" <- quoted + escaped, exact</code></pre>
<pre><code>#[derive(Debug)] // compiler writes Debug for you
struct Config { name: String }
impl fmt::Display for Point { // Display is NEVER derivable — write by hand
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}</code></pre>
<p><strong>Why no <code>derive(Display)</code>:</strong> how a value should look to a human is your program's decision, not something the compiler can guess. <code>Debug</code> has one obvious form (type name + fields), so it can be generated.</p>
<p><strong>Why <code>Vec</code> has <code>Debug</code> but no <code>Display</code>:</strong> there is no single correct way to show a list to a user (commas? bullets? brackets?), so std refuses to pick. You choose:</p>
<pre><code>println!("{v:?}"); // ["a", "b"] — diagnostics
println!("{}", v.join(", ")); // a, b — you pick the format</code></pre>
<p><strong>Errors you will see:</strong></p>
<pre><code>error[E0277]: `Vec&lt;String&gt;` doesn't implement `std::fmt::Display`
= note: in format strings you may be able to use `{:?}` instead
error[E0277]: `Point` doesn't implement `Debug`
= note: add `#[derive(Debug)]` to `Point` or manually `impl Debug for Point`</code></pre>
<p><strong>Rule of thumb:</strong> use <code>{:?}</code> while developing and in logs. Write a <code>Display</code> impl only when a real person reads the output. Custom error types want both — <code>Display</code> for the caller's message, <code>Debug</code> for the log.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch05-02-example-structs.html">5.2 (derive Debug)</a> · <a href="https://doc.rust-lang.org/stable/book/ch10-02-traits.html">10.2 Traits</a> · <a href="https://doc.rust-lang.org/std/fmt/">std::fmt docs</a></p>
<h2 id="tests">Tests</h2>
<pre><code>#[cfg(test)] // compiled by `cargo test`, not by `cargo build`
mod tests {
use super::*; // the child module needs its parent's items
#[test] // no arguments, no return value
fn a_task_starts_as_todo() {
assert_eq!(Task::new(1, "x", Low).status, Status::Todo);
}
}</code></pre>
<p><strong>A test fails by panicking.</strong> Each test runs on its own thread; the harness marks it failed when that thread dies. So every assertion macro is a wrapper that panics, and <code>unwrap</code>/<code>expect</code> in a test body is a legitimate assertion.</p>
<pre><code>assert!(cond) // prints the SOURCE TEXT of cond
assert!(cond, "id {} was wrong", id) // extra args go to format!
assert_eq!(a, b) // prints both values as `left` / `right`
assert_ne!(a, b) // same, passes when they differ</code></pre>
<p><code>assert_eq!</code> needs <code>PartialEq</code> (to compare) and <code>Debug</code> (to print) on the values — that is what the <code>#[derive(Debug, PartialEq)]</code> on your own types is for. Prefer it over <code>assert!(a == b)</code>, which throws the values away. An assertion inside a loop needs a message naming the case.</p>
<p><strong>Two ways to test a failure</strong> — one per way your code fails:</p>
<pre><code>#[test]
#[should_panic(expected = "at most 30")] // substring of the panic message
fn refuses_a_high_target() { Thermostat::new(99); }
#[test]
fn a_reload_sees_what_was_saved() -&gt; Result&lt;(), TaskError&gt; {
store.save(&amp;path)?; // `?` for SETUP that must work
assert!(Store::load(&amp;bad).is_err()); // is_err for the tested failure
Ok(())
}</code></pre>
<p>Always give <code>should_panic</code> its <code>expected</code>, or any panic passes the test. <code>#[should_panic]</code> and a <code>Result</code> return cannot be combined; assert with <code>is_err()</code> or <code>unwrap_err()</code> instead. A <code>Result</code> test's error type only needs <code>Debug</code>.</p>
<p><strong>Two homes, two levels of access:</strong></p>
<table>
<tr><th></th><th>Unit test</th><th>Integration test</th></tr>
<tr><td>Lives in</td><td>bottom of the source file</td><td><code>tests/&lt;name&gt;.rs</code></td></tr>
<tr><td>Compiled as</td><td>part of your crate</td><td>its own separate crate</td></tr>
<tr><td>Can reach</td><td>private items too</td><td>the public API only</td></tr>
<tr><td>Needs</td><td><code>#[cfg(test)]</code> + <code>use super::*</code></td><td><code>use mycrate::…</code></td></tr>
<tr><td>Run alone</td><td><code>cargo test --lib</code></td><td><code>cargo test --test name</code></td></tr>
</table>
<p><strong>Nothing in <code>src/main.rs</code> is testable</strong> — a binary crate exposes nothing to <code>use</code>. Keep <code>main</code> thin, put the logic in the library, and take the output destination as a parameter so a test can read it:</p>
<pre><code>pub fn run(args: &amp;[String], store: &amp;mut Store, out: &amp;mut impl Write)
-&gt; Result&lt;(), TaskError&gt;
{
writeln!(out, "added task {}", id)?; // never println! in library code
}
run(&amp;args, &amp;mut store, &amp;mut io::stdout().lock())?; // in main
let mut out: Vec&lt;u8&gt; = Vec::new(); // in a test
run(&amp;args, &amp;mut store, &amp;mut out)?;
assert_eq!(String::from_utf8(out)?, "added task 1\n");</code></pre>
<p><strong>Helpers shared by two integration files</strong> go in <code>tests/common/mod.rs</code>, never <code>tests/common.rs</code> — a plain file there is compiled as its own test crate and shows up as a stray <code>running 0 tests</code> section.</p>
<pre><code>tests/
├── common/mod.rs mod common; then common::three_tasks()
├── cli.rs #![allow(dead_code)] in mod.rs: each crate uses some
└── mine.rs</code></pre>
<p><strong>Tests run in parallel</strong> and share nothing safely — no shared file, no current directory, no environment variable. Give every file-touching test its own path (process id + an <code>AtomicU32</code> counter, inside <code>env::temp_dir()</code>).</p>
<pre><code>cargo test # everything
cargo test warmer # every test whose FULL name contains it
cargo test --lib # unit tests only (--test cli = one file)
cargo test -- --show-output # also print stdout of tests that PASSED
cargo test -- --ignored # only the #[ignore] ones
cargo test -- --test-threads=1 # no parallelism — diagnose, do not fix</code></pre>
<p>Flags before <code>--</code> go to cargo, flags after it go to the test binary. The module path is part of a test's name (<code>task::tests::…</code>), so filtering on a module name runs that module.</p>
<pre><code>running 4 tests
test tests::refuses_a_high_target - should panic ... ok
test tests::slow_one ... ignored, only when the range changes
test result: ok. 3 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
// measured = nightly benchmarks · filtered out = excluded by your filter</code></pre>
<p><strong>Errors you will see:</strong></p>
<pre><code>error[E0433]: cannot find type `Thermostat` in this scope // no use super::*
error[E0603]: function `capped` is private // private fn, from tests/
error[E0616]: field `target` of struct `Thermostat` is private
help: a method `target` also exists, call it with parentheses</code></pre>
<p><strong>Judge a suite by the bugs it kills, not by the number of tests.</strong> Change one operator in the code by hand, run the suite, and put it back: if nothing went red, that behaviour is untested.</p>
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html">11.1 Writing tests</a> · <a href="https://doc.rust-lang.org/stable/book/ch11-02-running-tests.html">11.2 Running them</a> · <a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 Organization</a></p>
<footer>
<p><strong>Primary source:</strong> <a href="https://doc.rust-lang.org/stable/book/">The Rust Programming Language</a>, chapters 1–13.</p>
<p>Lessons: <a href="../lessons/0001-diagnostic-ch1-9.html">0001 Diagnostic</a> · <a href="../lessons/0002-write-a-cli-from-blank.html">0002 Write a CLI from blank</a> · <a href="../lessons/0004-structs-enums-packages.html">0004 Structs, enums, packages</a> · <a href="../lessons/0003-build-a-task-cli.html">0003 Build a task CLI</a> · <a href="../lessons/0005-traits-display-and-errors.html">0005 Traits, Display, errors</a> · <a href="../lessons/0006-your-own-error-type.html">0006 Your own error type</a> · <a href="../lessons/0007-files-and-fromstr.html">0007 Files, io::Error, FromStr</a> · <a href="../lessons/0008-iterators-and-hashmap.html">0008 Iterators, HashMap, generics</a> · <a href="../lessons/0009-writing-your-own-tests.html">0009 Writing your own tests</a></p>
<p>Other reference: <a href="book-coverage.html">Coverage map</a> — every book chapter marked Read / Produced / Gap, and what gets taught next.</p>
<p>Something here unclear or contradicting what you remember? Ask the agent.</p>
</footer>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "restauran"
version = "0.1.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "restauran"
version = "0.1.0"
edition = "2024"
[dependencies]
+1
View File
@@ -0,0 +1 @@
pub mod hosting;
+1
View File
@@ -0,0 +1 @@
pub fn add_to_waitlist() {}
+31
View File
@@ -0,0 +1,31 @@
mod front_of_house;
// {
// pub mod hosting {
// pub fn add_to_waitlist() {}
//
// fn seat_at_table() {}
// }
//
// mod serving {
// fn take_order() {}
// fn serve_order() {}
// fn take_payment() {}
// }
// }
pub fn eat_at_restaurant() {
let arr: [u16; 10] = [0; 10];
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
use crate::front_of_house::hosting;
mod customer {
pub fn eat_at_restaurant() {
super::hosting::add_to_waitlist();
}
}
+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "slice"
version = "0.1.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "slice"
version = "0.1.0"
edition = "2024"
[dependencies]
+19
View File
@@ -0,0 +1,19 @@
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
println!("{word}");
s.clear();
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
+1
View File
@@ -0,0 +1 @@
TASK_FILE=t.txt
+1
View File
@@ -0,0 +1 @@
/target
+16
View File
@@ -0,0 +1,16 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "dotenv"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]]
name = "tasks"
version = "0.1.0"
dependencies = [
"dotenv",
]
+7
View File
@@ -0,0 +1,7 @@
[package]
name = "tasks"
version = "0.1.0"
edition = "2024"
[dependencies]
dotenv = "0.15.0"
+43
View File
@@ -0,0 +1,43 @@
use std::io::Write;
use crate::{command::Command, error::TaskError, store::Store, task::Priority};
pub fn run(args: &[String], memory: &mut Store, out: &mut impl Write) -> Result<(), TaskError> {
match Command::parse(args)? {
Command::Add { title, priority } => {
let n = memory.add(&title, priority);
writeln!(out, "added task {}", n)?;
Ok(())
}
Command::List => {
memory
.tasks()
.iter()
.try_for_each(|x| writeln!(out, "{}", x))?;
Ok(())
}
Command::Done { id } => {
memory.complete(id)?;
writeln!(out, "completed {}", id)?;
Ok(())
}
Command::Remove { id } => {
memory.remove(id)?;
writeln!(out, "removed {}", id)?;
Ok(())
}
Command::Stats => {
let counts = memory.count_by_priority();
for p in [Priority::High, Priority::Medium, Priority::Low] {
let count = counts.get(&p).copied().unwrap_or(0);
writeln!(out, "{:<6} {}", p.label(), count)?;
}
Ok(())
}
Command::Clear => {
let n = memory.remove_completed();
writeln!(out, "cleared {} completed", n)?;
Ok(())
}
}
}

Some files were not shown because too many files have changed in this diff Show More