Rust learning: lessons, notes, and exercise crates
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user