Rust learning: lessons, notes, and exercise crates
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
target/
|
||||||
|
hello_world/main
|
||||||
|
.playwright-mcp/
|
||||||
+18
@@ -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).
|
||||||
@@ -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.
|
||||||
@@ -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
@@ -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 () {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+7
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "control_flow"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -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!!!");
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+7
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "function"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+1981
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "get-dependecies"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rand = "0.8.5"
|
||||||
|
trpl = "0.2.0"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+7
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "grader"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+133
@@ -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",
|
||||||
|
]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "guessing_game"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rand = "0.8.5"
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+7
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "hello_cargo"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("Hello from cargo, world!");
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("Hello, World");
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+7
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "learn-challenges"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -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(" ")
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+7
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "learn-collections"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -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:?}");
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+7
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "learn-modules"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod vegetables;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub struct Asparagus {}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use crate::garden::vegetables::Asparagus;
|
||||||
|
|
||||||
|
pub mod garden;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let _plant = Asparagus {};
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+7
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "learn-panic"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alkdsfjlkafdsj
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+7
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "learn-struct"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+7
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "learn_enum"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -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.
|
||||||
@@ -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 & 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 & 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<T></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 & 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 & 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 & 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 & 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>
|
||||||
@@ -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<String> = 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<String></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() < 2 {
|
||||||
|
println!("usage: cargo run -- <score> [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 &args[1..] {
|
||||||
|
match arg.parse::<u32>() {
|
||||||
|
Ok(score) => println!("{score} -> ok"),
|
||||||
|
Err(_) => println!("{arg} -> 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>&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>::<u32></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) -> String {
|
||||||
|
match score {
|
||||||
|
90..=100 => "A".to_string(),
|
||||||
|
80..=89 => "B".to_string(),
|
||||||
|
70..=79 => "C".to_string(),
|
||||||
|
_ => "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) => println!("{score} -> {}", 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 & Pattern Matching">
|
||||||
|
<p class="topic">Checkpoint</p>
|
||||||
|
<p class="prompt">Delete the <code>_ => "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>&str</code>. The function builds a value and hands ownership to its caller. Returning a borrowed <code>&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<u32> = 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) => {
|
||||||
|
println!("{score} -> {}", 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(&scores));
|
||||||
|
}</code></pre>
|
||||||
|
<p>And a second function at the bottom of the file:</p>
|
||||||
|
<pre><code>fn average(scores: &[u32]) -> 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<u32></code>, then try to print <code>scores.len()</code> on the line <em>after</em> that call. What happens, and why does the <code>&</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>&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>&</code> by default and take ownership only on purpose. <strong>Put the <code>&</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>&[u32]</code>, not <code>&Vec<u32></code>. A slice accepts a <code>Vec</code>, an array, or part of either — strictly more useful, same speed. Idiomatic Rust prefers <code>&[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(&[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>
|
||||||
@@ -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 & 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 <title> [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 <id></code></td><td>Marks that task finished.</td></tr>
|
||||||
|
<tr><td><code>remove <id></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) -> &str }
|
||||||
|
impl Priority { pub fn label(&self) -> &str }
|
||||||
|
impl Priority { pub fn parse(text: &str) -> Option<Priority> }
|
||||||
|
impl Task { pub fn new(id: u32, title: &str, priority: Priority) -> 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]) -> Result<Command, String>
|
||||||
|
}</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() -> Store
|
||||||
|
pub fn add(&mut self, title: &str, priority: Priority) -> u32 // -> new id
|
||||||
|
pub fn complete(&mut self, id: u32) -> Result<(), String>
|
||||||
|
pub fn remove(&mut self, id: u32) -> Result<(), String>
|
||||||
|
pub fn tasks(&self) -> &[Task]
|
||||||
|
pub fn find(&self, id: u32) -> Option<&Task>
|
||||||
|
}</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) -> u32 {
|
||||||
|
match self {
|
||||||
|
Coin::Nickel => 5,
|
||||||
|
Coin::Dime => 10,
|
||||||
|
Coin::Quarter => 25,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(text: &str) -> Option<Coin> {
|
||||||
|
match text {
|
||||||
|
"nickel" => Some(Coin::Nickel),
|
||||||
|
"dime" => Some(Coin::Dime),
|
||||||
|
_ => 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 } => println!("{count} x {}", coin.value()),
|
||||||
|
Event::Select { slot } => println!("slot {slot}"),
|
||||||
|
Event::Refund => 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<Coin>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Machine {
|
||||||
|
pub fn new() -> Machine { // associated fn: no self, called Machine::new()
|
||||||
|
Machine { credit: 0, coins: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, coin: Coin) -> u32 { // &mut self: may change it
|
||||||
|
self.credit += coin.value();
|
||||||
|
self.coins.push(coin);
|
||||||
|
self.credit
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn credit(&self) -> u32 { self.credit } // &self: read only
|
||||||
|
|
||||||
|
pub fn coins(&self) -> &[Coin] { &self.coins } // borrowed view, not the Vec
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<h3>Reading optional arguments</h3>
|
||||||
|
<pre><code>let words: Vec<String> = std::env::args().collect();
|
||||||
|
|
||||||
|
words.first() // Option<&String> — the first, if any
|
||||||
|
words.get(2) // Option<&String> — index 2, if any
|
||||||
|
&words[1..] // slice of everything after the first
|
||||||
|
|
||||||
|
// Option -> 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" => { }
|
||||||
|
other => return Err(format!("unknown: {other}")),
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<h3>Turning a parse failure into your own error type</h3>
|
||||||
|
<pre><code>// parse gives Result<u32, ParseIntError>; map_err rewrites the error side
|
||||||
|
let slot = text.parse::<u32>()
|
||||||
|
.map_err(|_| format!("slot must be a number, got: {text}"))?;
|
||||||
|
|
||||||
|
// Option -> 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() > 5);
|
||||||
|
|
||||||
|
// the short forms — |coin| ... is a closure (ch 13); both are worth knowing now
|
||||||
|
self.coins.iter().find(|coin| coin.value() == 10) // -> Option<&Coin>
|
||||||
|
self.coins.iter().position(|coin| coin.value() == 10) // -> Option<usize></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) -> &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::<u32>()</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>
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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) -> Drink { // no self
|
||||||
|
Drink { name: name.to_string(), shots, iced: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn price(&self) -> u32 { // &self
|
||||||
|
250 + self.shots * 50
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_shot(&mut self) { // &mut self
|
||||||
|
self.shots += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_name(self) -> 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<String>, // private: no `pub`
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Menu {
|
||||||
|
pub fn new() -> Menu { Menu { items: Vec::new() } }
|
||||||
|
pub fn add(&mut self, name: &str) { self.items.push(name.to_string()); }
|
||||||
|
pub fn items(&self) -> &[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) -> u32 {
|
||||||
|
match self {
|
||||||
|
Size::Small => 240,
|
||||||
|
Size::Medium => 350,
|
||||||
|
Size::Large => 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>_ => {}</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 } => format!("cash, {received} received"),
|
||||||
|
Payment::Card(last4) => format!("card ending {last4}"),
|
||||||
|
Payment::Voucher { code, off } => format!("voucher {code}, {off} off"),
|
||||||
|
Payment::OnTheHouse => String::from("free"),
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<pre><code>5. Cash { received: 500 } -> cash, 500 received
|
||||||
|
5. Card("4242") -> card ending 4242
|
||||||
|
5. Voucher { code: "FREE10", off: 100 } -> voucher FREE10, 100 off
|
||||||
|
5. OnTheHouse -> 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<T> { Some(T), None }
|
||||||
|
enum Result<T, E> { 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) => println!("Option::Some carried a {:?}", s),
|
||||||
|
None => 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) -> Option<Size> {
|
||||||
|
match text {
|
||||||
|
"small" => Some(Size::Small),
|
||||||
|
"medium" => Some(Size::Medium),
|
||||||
|
"large" => Some(Size::Large),
|
||||||
|
_ => 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>
|
||||||
@@ -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(&self) -> f64;
|
||||||
|
|
||||||
|
fn label(&self) -> 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(&self) -> f64 { self.celsius }
|
||||||
|
|
||||||
|
fn label(&self) -> String { // overrides the default
|
||||||
|
format!("{} is {:.1}C", self.room, self.celsius)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reading for Kettle {
|
||||||
|
fn celsius(&self) -> 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`
|
||||||
|
--> src/main.rs:31:1
|
||||||
|
|
|
||||||
|
5 | fn celsius(&self) -> 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(&self, f: &mut fmt::Formatter) -> 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<(), fmt::Error></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`
|
||||||
|
--> 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> </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<T></code>: fine, the trait is yours. <code>Display</code> for <code>Vec<String></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><T></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<T>(items: &[T]) -> Option<&T> {
|
||||||
|
items.iter().max_by(|a, b| a.celsius().total_cmp(&b.celsius()))
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<pre><code>error[E0599]: no method named `celsius` found for reference `&&T` in the current scope
|
||||||
|
--> src/main.rs:46:34
|
||||||
|
|
|
||||||
|
46 | items.iter().max_by(|a, b| a.celsius().total_cmp(&b.celsius()))
|
||||||
|
| ^^^^^^^ method not found in `&&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<T: Reading>(items: &[T]) -> Option<&T> {
|
||||||
|
items.iter().max_by(|a, b| a.celsius().total_cmp(&b.celsius()))
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<pre><code>5. hottest: attic [28.0C]</code></pre>
|
||||||
|
|
||||||
|
<p>Read <code><T: Reading></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<T: Reading>(r: &T) // bound in the angle brackets
|
||||||
|
fn show(r: &impl Reading) // same thing, shorter
|
||||||
|
fn show<T>(r: &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(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
SensorError::Empty => write!(f, "no reading given"),
|
||||||
|
SensorError::NotANumber(e) => write!(f, "not a number: {}", e),
|
||||||
|
SensorError::OutOfRange(v) => 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`
|
||||||
|
--> 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<T> {
|
||||||
|
fn from(value: T) -> 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<&str> 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<ParseFloatError> for SensorError {
|
||||||
|
fn from(e: ParseFloatError) -> 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::<f64>().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) => v,
|
||||||
|
Err(e) => return Err(From::from(e)), // <- 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: &str) -> Result<f64, SensorError> {
|
||||||
|
match text.parse::<f64>() {
|
||||||
|
Ok(v) => Ok(v),
|
||||||
|
Err(e) => Err(SensorError::from(e)), // call it yourself
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_into(text: &str) -> Result<f64, SensorError> {
|
||||||
|
match text.parse::<f64>() {
|
||||||
|
Ok(v) => Ok(v),
|
||||||
|
Err(e) => Err(e.into()), // `.into()` is From from the other side
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_question(text: &str) -> Result<f64, SensorError> {
|
||||||
|
Ok(text.parse::<f64>()?) // `?` 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`
|
||||||
|
--> src/main.rs:28:27
|
||||||
|
|
|
||||||
|
27 | fn with_question(text: &str) -> Result<f64, SensorError> {
|
||||||
|
| ------------------------ expected `SensorError` because of this
|
||||||
|
28 | Ok(text.parse::<f64>()?)
|
||||||
|
| --------------^ the trait `From<ParseFloatError>` is not implemented for `SensorError`
|
||||||
|
| |
|
||||||
|
| this can't be annotated with `?` because it has type `Result<_, ParseFloatError>`</code></pre>
|
||||||
|
|
||||||
|
<p>"the trait <code>From<X></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: &[String]) -> Result<Command, String> {
|
||||||
|
let first = args.first().ok_or("No Arguments Found")?;
|
||||||
|
// ^^^^^^^^^^^^^^^^^^ this is a &str, not a String
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<p><code>ok_or("No Arguments Found")</code> produces <code>Result<_, &str></code>, and your function promises <code>Result<_, String></code>. Two different types — the same mismatch as above. It compiled because the standard library already ships <code>impl From<&str> 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: &str) -> Result<f64, SensorError> {
|
||||||
|
if text.is_empty() {
|
||||||
|
return Err(SensorError::Empty);
|
||||||
|
}
|
||||||
|
let value: f64 = text.parse()?; // ParseFloatError becomes SensorError here
|
||||||
|
if value < -90.0 || value > 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<dyn Error></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() -> Result<(), Box<dyn Error>> {
|
||||||
|
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) => println!("reading {:.1}C", v),
|
||||||
|
Err(e) => {
|
||||||
|
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<T, MyError></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<io::Error> 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<T>(a: &T, b: &T)</code> refuse to compare <code>a</code> and <code>b</code> with <code>></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<T: PartialOrd>(..)</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(&self, f: &mut fmt::Formatter) -> fmt::Result</code>, why is the first parameter <code>&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 & 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(&self, f: &mut fmt::Formatter) -> 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<(), String></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 <id></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: &[String], store: &mut Store) -> Result<(), String></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>/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(&args, &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><T></code> can do nothing; <code><T: Trait></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 & 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>
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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) -> Option<&(dyn Error + 'static)> { ... }
|
||||||
|
// …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><T: Reading></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(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ConfigError::Missing(key) => write!(f, "missing setting: {}", key),
|
||||||
|
// no `{e}` on the next arm - the port text is not worth echoing
|
||||||
|
ConfigError::BadPort(_) => write!(f, "port must be a number"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for ConfigError {
|
||||||
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
ConfigError::BadPort(e) => Some(e), // the cause lives here instead
|
||||||
|
ConfigError::Missing(_) => None, // nothing underneath this one
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseIntError> for ConfigError {
|
||||||
|
fn from(e: ParseIntError) -> ConfigError { ConfigError::BadPort(e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port(text: Option<&str>) -> Result<u16, ConfigError> {
|
||||||
|
let text = text.ok_or(ConfigError::Missing("port".to_string()))?;
|
||||||
|
Ok(text.parse()?) // ParseIntError -> 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<&(dyn Error + 'static)></code> as "maybe a reference to some error, whatever type it
|
||||||
|
is" — the <code>dyn</code> from 0005's <code>Box<dyn Error></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<&str>) -> Result<u16, ConfigError> {
|
||||||
|
match port(text) {
|
||||||
|
Err(ConfigError::Missing(_)) => Ok(8080), // recover from ONE variant
|
||||||
|
other => 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<u16, String></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: `&TaskError::StoreFull` not covered
|
||||||
|
--> src/error.rs:19:15
|
||||||
|
|
|
||||||
|
19 | match self {
|
||||||
|
| ^^^^ pattern `&TaskError::StoreFull` not covered
|
||||||
|
|
|
||||||
|
note: `TaskError` defined here
|
||||||
|
--> src/error.rs:6:10
|
||||||
|
|
|
||||||
|
6 | pub enum TaskError {
|
||||||
|
| ^^^^^^^^^
|
||||||
|
...
|
||||||
|
14 | StoreFull,
|
||||||
|
| --------- not covered
|
||||||
|
= note: the matched value is of type `&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>_ => …</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`
|
||||||
|
--> 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
|
||||||
|
--> 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 `<u32 as FromStr>::Err == TaskError`
|
||||||
|
--> 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<ParseIntError> 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>_ => …</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><T: Reading></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<E: Error + Send + Sync + 'static>() {}</code> then <code>assert_usable_as_error::<TaskError>();</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>&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>&str</code> in the variant would need a lifetime parameter — <code>TaskError<'a></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 & paths">
|
||||||
|
<p class="topic">Modules & 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<ParseIntError></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(&self) -> Option<&(dyn Error + 'static)></code> — copy it from Part 2, it is not worth
|
||||||
|
deriving. Exactly one variant returns
|
||||||
|
<code>Some(e)</code>; a <code>_ => 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<Command, TaskError></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>_ =></code> arm can now name the word it rejected, and both
|
||||||
|
<code>match id.parse() { Ok(n) => n, Err(_) => 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>_ => …</code> discards the value it matched. Bind it instead:
|
||||||
|
<code>other => Err(TaskError::UnknownCommand(other.to_string()))</code>. <code>other</code> is a
|
||||||
|
<code>&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<(), TaskError></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>/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<Cause> 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>
|
||||||
@@ -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<ParseIntError></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) => n,
|
||||||
|
Err(e) => 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<P: AsRef<Path>>(path: P) -> io::Result<String>
|
||||||
|
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()></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<T></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<T> = std::result::Result<T, io::Error></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<String></code> in a signature, read it silently as <code>Result<String, io::Error></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<Path></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>&str</code>, a <code>String</code>, a <code>&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 -> 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<T> 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<ParseIntError> for TaskError { .. } // T = ParseIntError (0006)
|
||||||
|
impl From<io::Error> 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`
|
||||||
|
--> src/store.rs:82:29
|
||||||
|
|
|
||||||
|
82 | fs::write(path, out)?;
|
||||||
|
| --------------------^ the trait `From<std::io::Error>` is not implemented for `TaskError`
|
||||||
|
| |
|
||||||
|
| this can't be annotated with `?` because it has type `Result<_, std::io::Error>`
|
||||||
|
|
|
||||||
|
note: `TaskError` needs to implement `From<std::io::Error>`
|
||||||
|
= 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<F: FromStr>(&self) -> Result<F, F::Err> { .. }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FromStr: Sized {
|
||||||
|
type Err; // an ASSOCIATED TYPE
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err>;
|
||||||
|
}</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::<u32>()</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::<Task>()</code> is known to return
|
||||||
|
<code>Result<Task, TaskError></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`
|
||||||
|
--> 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::<Task>()</code> before implementing it at all:</p>
|
||||||
|
|
||||||
|
<pre><code>error[E0277]: the trait bound `Task: FromStr` is not satisfied
|
||||||
|
--> src/store.rs:98:29
|
||||||
|
|
|
||||||
|
98 | tasks.push(line.parse::<Task>()?);
|
||||||
|
| ^^^^^ 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: &str) -> Result<Reading, String> {
|
||||||
|
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) => text,
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Store::new()),
|
||||||
|
Err(e) => 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 `&std::io::Error`
|
||||||
|
--> 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(&self, other: &Self) -> bool {
|
||||||
|
use TaskError::*;
|
||||||
|
match (self, other) {
|
||||||
|
(UnknownCommand(a), UnknownCommand(b))
|
||||||
|
| (BadPriority(a), BadPriority(b))
|
||||||
|
| (BadLine(a), BadLine(b)) => a == b,
|
||||||
|
(BadId(a), BadId(b)) => a == b,
|
||||||
|
(NotFound(a), NotFound(b)) => a == b,
|
||||||
|
(Io(a), Io(b)) => a.kind() == b.kind(), // the kind, not the error
|
||||||
|
_ => 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)) =></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<ParseIntError> 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<ParseIntError> for TaskError</code></button>
|
||||||
|
<button class="opt" data-correct="false">An added <code>From<io::Error> for TaskError</code></button>
|
||||||
|
<button class="opt" data-correct="false">An added <code>From<ParseIntError> for LineError</code></button>
|
||||||
|
<button class="opt" data-correct="false">An added <code>From<ParseFloatError> 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<Err></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::<Task>()</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 => 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: &Path) -> Result<Store, TaskError></code> — no <code>&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(&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>&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 & 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) => Some(e)</code> to <code>source()</code>; add
|
||||||
|
<code>From<io::Error></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(&str) -> Option<Status></code> — the mirror of <code>label()</code>, same shape as
|
||||||
|
<code>Priority::parse</code>, which you already have.</li>
|
||||||
|
<li><code>Task::to_line(&self) -> 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(&self, path: &Path) -> Result<(), TaskError>
|
||||||
|
pub fn load(path: &Path) -> Result<Store, TaskError> // 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" >> 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::<Result<Vec<_>, _>>()</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<T></code> is just <code>Result<T, io::Error></code>; <code>e.kind()</code> is how you
|
||||||
|
tell one io failure from another.</li>
|
||||||
|
<li>One <code>impl From<T> 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 & 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>
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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(¬hing, |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"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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: &str, shelf: Shelf, borrowed: bool) -> Book {
|
||||||
|
Book { title: title.to_string(), shelf, borrowed }
|
||||||
|
}
|
||||||
|
|
||||||
|
// "fiction" -> Some(Fiction), anything unknown -> None
|
||||||
|
fn shelf_of(word: &str) -> Option<Shelf> {
|
||||||
|
match word {
|
||||||
|
"fiction" => Some(Fiction),
|
||||||
|
"history" => Some(History),
|
||||||
|
"poetry" => Some(Poetry),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The data. Four books, three shelves, one of them out on loan.
|
||||||
|
let mut shelf: Vec<Book> = 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<&str> = 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<Book></code>, so <code>shelf.iter()</code> hands you
|
||||||
|
<code>&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<&str></code>, so <code>titles.iter()</code> hands you <code>&&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<io::Error> 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) -> 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(&mut self) -> Option<Self::Item>;
|
||||||
|
// ~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
|
||||||
|
--> 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>&T</code></td><td>You are reading. The collection survives.</td></tr>
|
||||||
|
<tr><td><code>v.iter_mut()</code></td><td><code>&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>&self</code>, <code>&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<String> } // titles only, to keep the error bare
|
||||||
|
|
||||||
|
impl Library {
|
||||||
|
fn rename(&mut self, from: &str, to: &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
|
||||||
|
--> 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<Book> = vec![
|
||||||
|
book("Dubliners", Fiction, true), book("SPQR", History, false),
|
||||||
|
book("Ariel", Poetry, false), book("Beloved", Fiction, false),
|
||||||
|
];
|
||||||
|
|
||||||
|
let all_titles: Vec<&str> = shelf.iter().map(|b| b.title.as_str()).collect();
|
||||||
|
shelf.iter_mut().for_each(|b| b.borrowed = false);
|
||||||
|
|
||||||
|
let fiction: Vec<&str> = 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 -> ["Dubliners", "SPQR", "Ariel", "Beloved"]
|
||||||
|
iter_mut -> every book returned, borrowed set to false on each
|
||||||
|
fiction -> ["Dubliners", "Beloved"]
|
||||||
|
find -> Some(Poetry) // the ITEM, mapped: Option<Shelf>
|
||||||
|
position -> Some(2) // the INDEX: Option<usize>, Ariel is 3rd
|
||||||
|
any / count -> false / 4 // nothing is borrowed now, so all 4 are in
|
||||||
|
retain -> 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<B: FromIterator<Self::Item>>(self) -> 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
|
||||||
|
--> 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<String>` found in the `alloc` crate:
|
||||||
|
- impl FromIterator<String> for Box<str>;
|
||||||
|
- impl FromIterator<String> 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<String> = ...</code>, or on the right with a turbofish,
|
||||||
|
<code>.collect::<Vec<String>>()</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<Option<Shelf>> = words.iter().map(|w| shelf_of(w)).collect();
|
||||||
|
let all: Option<Vec<Shelf>> = words.iter().map(|w| shelf_of(w)).collect();
|
||||||
|
|
||||||
|
let numbers: Result<Vec<u32>, _> =
|
||||||
|
"1 2 x 4".split(' ').map(str::parse::<u32>).collect();</code></pre>
|
||||||
|
<pre><code>Vec<Option> -> [Some(Fiction), Some(History), None, Some(Poetry)]
|
||||||
|
Option<Vec> -> None
|
||||||
|
Result<Vec> -> 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<Result<A, E>> for Result<V, E></a></p>
|
||||||
|
|
||||||
|
<p><code>Vec<Option<Shelf>></code> keeps every outcome, hole included. <code>Option<Vec<Shelf>></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<Task> = contents.lines()
|
||||||
|
.map(str::parse)
|
||||||
|
.collect::<Result<Vec<Task>, TaskError>>()?;</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<Task></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') // -> ["1|fiction|Dubliners", "2|poetry|Ariel", ""]
|
||||||
|
file.lines() // -> ["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<K, V></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<Shelf, usize> = HashMap::new();
|
||||||
|
counts.insert(Shelf::Fiction, 2);
|
||||||
|
counts.get(&Shelf::Fiction); // Option<&usize> — may be absent
|
||||||
|
counts.get(&Shelf::Poetry).copied().unwrap_or(0); // absent counts as 0
|
||||||
|
for (shelf, n) in &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<&V></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 &shelf {
|
||||||
|
*counts.entry(b.shelf).or_insert(0) += 1;
|
||||||
|
}</code></pre>
|
||||||
|
<pre><code>entry() -> {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>&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<T, K, F>(items: &[T], key: F) -> HashMap<K, usize>
|
||||||
|
|
||||||
|
// 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
|
||||||
|
--> 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
|
||||||
|
--> 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>&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>&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<T, K, F>(items: &[T], key: F) -> HashMap<K, usize>
|
||||||
|
where
|
||||||
|
K: Eq + Hash,
|
||||||
|
F: Fn(&T) -> 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(&T) -> K</code>, which reads as “anything callable that takes a <code>&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(&shelf, |b| b.shelf) // -> {History: 1, Fiction: 2}
|
||||||
|
tally(&["a", "bb", "cc"], |w| w.len()) // -> {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<usize></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::<Result<Vec<Task>, TaskError>>()</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<Vec<_>, E></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<Result<Task, TaskError>></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>&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>&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<T, K, F>(items: &[T], key: F)</code> with <code>K: Eq + Hash, F: Fn(&T) -> 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<io::Error> 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<io::Error>" 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(&self) -> HashMap<Priority, usize></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(&self, priority: Priority) -> Vec<&str>
|
||||||
|
pub fn remove_completed(&mut self) -> 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<&str></code>, not <code>Vec<String></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::<String>()</code>, and <code>load</code> with
|
||||||
|
<code>lines().map(str::parse).collect::<Result<Vec<Task>, TaskError>>()?</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<String></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(&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<&str></code> borrowed from <code>&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>&[T]</code>. Most experienced Rust developers would write it to
|
||||||
|
take <code>impl IntoIterator<Item = T></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>&self</code>/<code>&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<Vec<_>, E></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 & 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>
|
||||||
Executable
+65
@@ -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 ]
|
||||||
@@ -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::<Result<Vec<Task>,
|
||||||
|
TaskError>>()?</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) -> Thermostat {
|
||||||
|
if target < MIN {
|
||||||
|
panic!("target must be at least {MIN}, got {target}");
|
||||||
|
} else if target > MAX {
|
||||||
|
panic!("target must be at most {MAX}, got {target}");
|
||||||
|
}
|
||||||
|
Thermostat { target }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn target(&self) -> i32 {
|
||||||
|
self.target
|
||||||
|
}
|
||||||
|
|
||||||
|
// never leaves the legal range, however big `by` is
|
||||||
|
pub fn warmer(&mut self, by: i32) {
|
||||||
|
self.target = capped(self.target + by);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_heating(&self, room: i32) -> bool {
|
||||||
|
room < self.target
|
||||||
|
}
|
||||||
|
|
||||||
|
// "21" -> Ok, "hot" or "99" -> Err
|
||||||
|
pub fn set_from(&mut self, text: &str) -> Result<(), String> {
|
||||||
|
let degrees: i32 = text
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("not a number: {text}"))?;
|
||||||
|
if degrees < MIN || degrees > 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) -> 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 <name></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 > 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() -> Result<(), String> {
|
||||||
|
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<(), TaskError></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<T, E></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<T, E></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<T, E></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
|
||||||
|
--> 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
|
||||||
|
--> src/lib.rs:48:1
|
||||||
|
|
|
||||||
|
48 | fn capped(degrees: i32) -> i32 {
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
error[E0616]: field `target` of struct `Thermostat` is private
|
||||||
|
--> 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: &[String],
|
||||||
|
store: &mut Store,
|
||||||
|
out: &mut impl Write, // std::io::Write
|
||||||
|
) -> Result<(), TaskError></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<u8></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(&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<(), TaskError></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: &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<u8></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(&[&str]) -> Vec<String></code>, and <code>three_tasks() -> 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<(), TaskError></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: &[String],
|
||||||
|
store: &mut Store,
|
||||||
|
out: &mut impl Write,
|
||||||
|
) -> Result<(), TaskError></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<io::Error> 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, "{:<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<u8></code> for <code>out</code>, then
|
||||||
|
<code>String::from_utf8</code>:</p>
|
||||||
|
|
||||||
|
<pre><code>fn output(command: &[&str], store: &mut Store) -> String {
|
||||||
|
let mut out: Vec<u8> = Vec::new();
|
||||||
|
run(&args(command), store, &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<&str></code> borrowed from <code>&self</code>, and
|
||||||
|
<code>Status::label</code> returns <code>&str</code> borrowed from <code>&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>-> Result<(), E></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>&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 & 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 |
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+7
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "ownership"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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>&self</code> methods and <code>&[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<Priority, usize></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<T, K, F>(items: &[T], key: F) -> HashMap<K, usize></code> in <code>tasks/src/stats.rs</code>,
|
||||||
|
with a <code>where</code> clause bounding <code>K: Eq + Hash</code> and <code>F: Fn(&T) -> 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>&'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>-> Result<(), E></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>&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::<Result<Vec<_>, E>>()</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<dyn Error></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 > 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<&str></code> borrowed from <code>&self</code>, and
|
||||||
|
<code>Status::label</code> returns <code>&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>
|
||||||
@@ -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::<u32>().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 < 5 { ... } else if n < 10 { ... } else { ... }
|
||||||
|
let label = if n > 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 < 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 & 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]) -> u32 { ... } // takes Vec or array — prefer &[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 & 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<T> { Some(T), None } // in std — "maybe a value"
|
||||||
|
enum Result<T, E> { Ok(T), Err(E) } // in std — "value or error"
|
||||||
|
|
||||||
|
match shape {
|
||||||
|
Shape::Circle(r) => 3.14 * r * r,
|
||||||
|
Shape::Rect { w, h } => w * h,
|
||||||
|
Shape::Empty => 0.0,
|
||||||
|
} // must be exhaustive
|
||||||
|
|
||||||
|
match score {
|
||||||
|
90..=100 => "A", // range pattern
|
||||||
|
n if n > 50 => "pass", // match guard
|
||||||
|
_ => "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 & 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><</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<u32> = 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<&u32> — 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<&i32>
|
||||||
|
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) -> Option<Self::Item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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))? // -> &T
|
||||||
|
list.iter().position(|t| t.id == id).ok_or(NotFound(id))? // -> 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<String> = it.collect();
|
||||||
|
let s: String = it.collect(); // String: FromIterator<String>
|
||||||
|
let m: HashMap<K, V> = pairs.collect(); // from an iterator of (K, V)
|
||||||
|
let r: Result<Vec<T>, E> = it.collect(); // SHORT-CIRCUITS on the first Err
|
||||||
|
let o: Option<Vec<T>> = 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<String>` 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 & 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<Priority, usize> = 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<&usize> -> 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() -> Result<String, io::Error> {
|
||||||
|
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) => f,
|
||||||
|
Err(e) => match e.kind() {
|
||||||
|
ErrorKind::NotFound => File::create("x.txt").unwrap(),
|
||||||
|
_ => 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() -> Result<(), Box<dyn Error>> { ... } // 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<T, E> { Ok(T), Err(E) } // an enum: errors are VALUES
|
||||||
|
enum Option<T> { Some(T), None } // absence, with no reason attached</code></pre>
|
||||||
|
<p>Pick the shortest rung that fits:</p>
|
||||||
|
<pre><code>match r { Ok(v) => ..., Err(e) => ... } // 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 -> 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) => value,
|
||||||
|
Err(e) => 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) -> Result<u16, Box<dyn Error>> {
|
||||||
|
let text = fs::read_to_string(path)?; // io::Error -\
|
||||||
|
let port = text.trim().parse::<u16>()?; // ParseIntError -> Box<dyn Error>
|
||||||
|
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 => write!(f, "no reading given"),
|
||||||
|
SensorError::NotANumber(_) => write!(f, "reading is not a number"),
|
||||||
|
SensorError::OutOfRange(v) => 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) -> Option<&(dyn Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
SensorError::NotANumber(e) => Some(e), // the wrapped cause
|
||||||
|
_ => None, // nothing underneath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseFloatError> for SensorError { // makes bare `?` convert for you
|
||||||
|
fn from(e: ParseFloatError) -> 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) -> 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::<f64>()? // `?` calls From::from(e) for you
|
||||||
|
|
||||||
|
// what `?` expands to:
|
||||||
|
match thing() { Ok(v) => v, Err(e) => 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::<f64>()?)
|
||||||
|
| --------------^ the trait `From<ParseFloatError>` is not implemented for `SensorError`
|
||||||
|
|
||||||
|
// with an annotated `let`, the same missing impl is reported as:
|
||||||
|
error[E0271]: type mismatch resolving `<f64 as FromStr>::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<_, String></code> already relies on this: std ships <code>impl From<&str> for String</code>.</p>
|
||||||
|
<p><strong>Reporting it in a CLI</strong> — <code>main -> 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" <- yours
|
||||||
|
e.source().map(|s| s.to_string()) // Some("invalid float literal") <- 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<E: Error + Send + Sync + 'static>() {}
|
||||||
|
assert_usable_as_error::<TaskError>(); // 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: `&TaskError::StoreFull` not covered
|
||||||
|
19 | match self {
|
||||||
|
| ^^^^ pattern `&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>_ => ..</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> & <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<String> = Result<String, io::Error>
|
||||||
|
fs::write(path, text)? // io::Result<()> — creates or truncates, one call
|
||||||
|
text.lines() // iterator of &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) => text,
|
||||||
|
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(Store::new()),
|
||||||
|
Err(e) => 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::<YourType>()</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: &str) -> Result<Self, Self::Err>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Task {
|
||||||
|
type Err = TaskError;
|
||||||
|
fn from_str(line: &str) -> Result<Task, TaskError> {
|
||||||
|
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<T> 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()?; // -> BadId, via From
|
||||||
|
let id: u32 = text.parse().map_err(|_| bad())?; // -> 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(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) { // match on a TUPLE of both
|
||||||
|
(Io(a), Io(b)) => a.kind() == b.kind(),
|
||||||
|
(NotFound(a), NotFound(b)) => a == b,
|
||||||
|
_ => 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 & generics</h2>
|
||||||
|
<pre><code>trait Reading {
|
||||||
|
fn celsius(&self) -> f64; // REQUIRED — semicolon
|
||||||
|
fn label(&self) -> String { // DEFAULT — has a body, may be overridden
|
||||||
|
format!("{:.1}C", self.celsius())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reading for Kettle { // impl TRAIT for TYPE
|
||||||
|
fn celsius(&self) -> f64 { (self.fahrenheit - 32.0) * 5.0 / 9.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Thermometer { // the trait behind `{}`
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> 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<T: Reading>(r: &T) // angle brackets
|
||||||
|
fn show(r: &impl Reading) // shorthand
|
||||||
|
fn show<T>(r: &T) where T: Reading // where clause, for long lists
|
||||||
|
fn show(r: &dyn Reading) // trait OBJECT: type chosen at runtime
|
||||||
|
fn show<T: Reading + Clone>(r: &T) // two promises at once</code></pre>
|
||||||
|
<p><code>Box<dyn Error></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<T, K, F>(items: &[T], key: F) -> HashMap<K, usize>
|
||||||
|
where
|
||||||
|
K: Eq + Hash, // required by the HashMap being returned, not by the body
|
||||||
|
F: Fn(&T) -> 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><T></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<A> 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<T></code> ✓ · <code>Display for Vec<String></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<String>` 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() -> Result<(), TaskError> {
|
||||||
|
store.save(&path)?; // `?` for SETUP that must work
|
||||||
|
assert!(Store::load(&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/<name>.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: &[String], store: &mut Store, out: &mut impl Write)
|
||||||
|
-> Result<(), TaskError>
|
||||||
|
{
|
||||||
|
writeln!(out, "added task {}", id)?; // never println! in library code
|
||||||
|
}
|
||||||
|
|
||||||
|
run(&args, &mut store, &mut io::stdout().lock())?; // in main
|
||||||
|
let mut out: Vec<u8> = Vec::new(); // in a test
|
||||||
|
run(&args, &mut store, &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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+7
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "restauran"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod hosting;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub fn add_to_waitlist() {}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+7
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "slice"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -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[..]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
TASK_FILE=t.txt
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+16
@@ -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",
|
||||||
|
]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "tasks"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dotenv = "0.15.0"
|
||||||
@@ -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
Reference in New Issue
Block a user