Files
learn-rust/lessons/0007-files-and-fromstr.html

589 lines
36 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>0007 — Files, io::Error, and FromStr</title>
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body>
<h1>Files, <code>io::Error</code>, and <code>FromStr</code></h1>
<p class="subtitle">Lesson 0007 · after <a href="0006-your-own-error-type.html">0006</a> · reading, then a 30-minute drill against a shipped test file · ~45 minutes</p>
<div class="callout">
Every code block and every compiler message on this page was produced by running it today, in a scratch project.
Nothing is written from memory. The demo domain is a weather log — your project is a task CLI, so nothing here
pastes in. Translating is the work.
</div>
<h2>Where 0006 left you, and the one loose end</h2>
<p>Twenty-four tests are green, and the error type behind them is real work: <code>TaskError</code> has seven
variants, each with its own <code>Display</code> sentence, a <code>source()</code> that hands back the one wrapped
cause, and an <code>impl From&lt;ParseIntError&gt;</code>. That is the full set of obligations for a std-compatible
error type, and you wrote all of it from the signatures up.</p>
<p>There is one loose end, and it is worth looking at closely before adding anything new. The <code>From</code>
impl you wrote is never actually called. Over in <code>command.rs</code>, the conversion is still done by hand —
once in the <code>done</code> arm, once in <code>remove</code>:</p>
<pre><code>let id: u32 = match id.parse() {
Ok(n) =&gt; n,
Err(e) =&gt; return Err(TaskError::BadId(e)), // this IS From::from, typed out
};</code></pre>
<p>Compare that with what <code>From</code> exists to do. Your impl says "given a <code>ParseIntError</code>,
build a <code>TaskError::BadId</code>" — which is exactly what the <code>Err</code> arm above says, in five lines
instead of zero. The impl is correct; it simply never gets reached, because <code>match</code> handles the error
before <code>?</code> would have had a chance to convert it. So the mechanism was learned and the reflex was not,
which is the most common way a Rust concept half-lands.</p>
<p>Step 0 of today's drill deletes both blocks. Then today adds a <em>second</em> <code>From</code> impl, and this
one you will not be able to route around by hand: the shape the code needs makes <code>?</code> the only
reasonable option, and the compiler stops the build until the impl exists.</p>
<h2>Part 1 — A file is a <code>String</code> that can fail</h2>
<p>A save file is, from Rust's point of view, nothing more exotic than a <code>String</code> that might not arrive.
Two functions in <a href="https://doc.rust-lang.org/std/fs/">std::fs</a> cover everything a CLI of this size needs.
Each one opens the file, does the work, and closes it again, all inside the single call — there is no handle to keep
track of and nothing to remember to close:</p>
<pre><code>fn read_to_string&lt;P: AsRef&lt;Path&gt;&gt;(path: P) -&gt; io::Result&lt;String&gt;
fn write&lt;P: AsRef&lt;Path&gt;, C: AsRef&lt;[u8]&gt;&gt;(path: P, contents: C) -&gt; io::Result&lt;()&gt;</code></pre>
<p class="cite">std: <a href="https://doc.rust-lang.org/std/fs/fn.read_to_string.html">fs::read_to_string</a> ·
<a href="https://doc.rust-lang.org/std/fs/fn.write.html">fs::write</a></p>
<p>The return types look unfamiliar, so take them apart before going further.
<code>io::Result&lt;T&gt;</code> is not a new kind of <code>Result</code>; it is
<a href="https://doc.rust-lang.org/std/io/type.Result.html">a type alias</a>, declared in std as
<code>type Result&lt;T&gt; = std::result::Result&lt;T, io::Error&gt;</code>. In other words the error half has already been
filled in for you, because every function in that module fails the same way. Whenever you meet
<code>io::Result&lt;String&gt;</code> in a signature, read it silently as <code>Result&lt;String, io::Error&gt;</code> and
carry on — it is the same <code>Result</code> you have been matching on since chapter 9, and <code>?</code> works
on it exactly as you would expect.</p>
<p>(The <code>P: AsRef&lt;Path&gt;</code> in the signature is a convenience bound, and you can read past it for now.
All it means is that you may pass a <code>&amp;str</code>, a <code>String</code>, a <code>&amp;Path</code>, or a
<code>PathBuf</code>, and std will accept any of them. It is the same idea as the trait bounds from 0005, used to
widen what a function will take.)</p>
<p>The interesting half is <code>io::Error</code>. Unlike <code>ParseIntError</code>, which really only means "that
was not a number", an <code>io::Error</code> stands for dozens of distinct situations: the file does not exist, the
path is a directory rather than a file, the process lacks permission to read it, the disk is full, the name is too
long. All of those arrive as the same type, so the type alone cannot tell you what went wrong. You separate them by
asking the value, using <a href="https://doc.rust-lang.org/std/io/enum.ErrorKind.html"><code>e.kind()</code></a>,
which returns a variant of the <code>ErrorKind</code> enum:</p>
<pre><code>let missing = fs::read_to_string("/tmp/definitely-not-here.txt");
println!("{:?}", missing.map_err(|e| e.kind()));</code></pre>
<pre><code>missing -&gt; Err(NotFound)</code></pre>
<p>Keep that <code>NotFound</code> in mind. It looks like a small detail, but it turns into the most important
design decision of the whole lesson, and Part 4 comes back to it: on the very first run of your CLI, the save file
legitimately does not exist yet, and how you treat that one kind decides whether a fresh install works or looks
broken.</p>
<h2>Part 2 — The second <code>From</code>, and why it is legal</h2>
<p>You asked, after the last lesson, whether a type can have two <code>From</code> impls. The answer decides how
today's code is shaped, so here is the rule stated precisely: <strong>for any given source type <code>T</code>,
there may be exactly one <code>impl From&lt;T&gt; for YourType</code> in the whole program.</strong> Write a second
one for the same <code>T</code> and the compiler stops you with
<code>error[E0119]: conflicting implementations</code>.</p>
<p>The reason is worth holding on to, because it explains a lot of Rust's trait rules. At the moment you write
<code>value?</code>, the only information the compiler has is the pair of types involved: it is converting a
<code>ParseIntError</code> into a <code>TaskError</code>. Nothing at that call site records what you <em>meant</em>
by the failure. If two impls existed for that pair, there would be two possible answers and no way to choose
between them, so the language forbids the ambiguity up front rather than picking one for you.</p>
<p>Nothing stops you from adding an impl for a <em>different</em> source type, though, and that is what today
needs — one conversion for parse failures, and a new one for file failures:</p>
<pre><code>impl From&lt;ParseIntError&gt; for TaskError { .. } // T = ParseIntError (0006)
impl From&lt;io::Error&gt; for TaskError { .. } // T = io::Error (0007)</code></pre>
<p>Write <code>fs::write(path, out)?</code> before the impl exists and the compiler tells you exactly what is
missing. Real output:</p>
<pre><code>error[E0277]: `?` couldn't convert the error to `TaskError`
--&gt; src/store.rs:82:29
|
82 | fs::write(path, out)?;
| --------------------^ the trait `From&lt;std::io::Error&gt;` is not implemented for `TaskError`
| |
| this can't be annotated with `?` because it has type `Result&lt;_, std::io::Error&gt;`
|
note: `TaskError` needs to implement `From&lt;std::io::Error&gt;`
= note: the question mark operation (`?`) implicitly performs a conversion
on the error value using the `From` trait</code></pre>
<p>The final note in that message is the part worth reading twice. <code>?</code> has no special knowledge of
<code>io</code>, and it is not a built-in shortcut for file handling: it simply calls <code>From::from</code> on
whatever error it is given. It is the identical mechanism that has been quietly converting your
<code>ParseIntError</code> into a <code>BadId</code> since 0006. Once you see <code>?</code> as "return early, and
run the error through <code>From</code> on the way out", every one of these messages becomes predictable rather
than mysterious.</p>
<h2>Part 3 — <code>FromStr</code>: the trait behind <code>.parse()</code></h2>
<p>You have been calling <code>.parse()</code> since the guessing game, and it has probably felt like a built-in
piece of string handling. It is not. It is a trait method, and once you see the two declarations behind it, the
whole thing stops being magic:</p>
<pre><code>impl str {
pub fn parse&lt;F: FromStr&gt;(&amp;self) -&gt; Result&lt;F, F::Err&gt; { .. }
}
pub trait FromStr: Sized {
type Err; // an ASSOCIATED TYPE
fn from_str(s: &amp;str) -&gt; Result&lt;Self, Self::Err&gt;;
}</code></pre>
<p class="cite">std: <a href="https://doc.rust-lang.org/std/str/trait.FromStr.html">FromStr</a> ·
Book: <a href="https://doc.rust-lang.org/stable/book/ch20-02-advanced-traits.html">20.2 — associated types</a></p>
<p>Read the two together. <code>"42".parse::&lt;u32&gt;()</code> works for exactly one reason: somewhere in std there
is an <code>impl FromStr for u32</code>, and it declares <code>type Err = ParseIntError</code>. There is no
special case for integers in the language. That means the door is open to you — implement the same trait for your
own type and <code>.parse()</code> begins working on it immediately. This is a genuinely different move from
writing a <code>Task::from_line</code> helper of your own. A helper is a function only your code knows about;
implementing the trait means your type joins an interface that std and every other crate already speak, so any
generic function taking <code>F: FromStr</code> will now accept a <code>Task</code> as well.</p>
<p>The unfamiliar line is <code>type Err;</code>, and it deserves its own paragraph because it is your first
<strong>associated type</strong>. Think of it as a slot in the trait that the <em>implementor</em> fills in, once,
and permanently — as opposed to a generic parameter, which the <em>caller</em> chooses at each call site. That
distinction is exactly why <code>ParseIntError</code> appears nowhere in <code>parse()</code>'s signature: the
signature says <code>F::Err</code>, meaning "whatever error type <code>F</code> declared when it implemented the
trait". When you write <code>type Err = TaskError</code> in your impl, you are filling that slot for
<code>Task</code>, and from then on <code>line.parse::&lt;Task&gt;()</code> is known to return
<code>Result&lt;Task, TaskError&gt;</code> without anyone having to say so again.</p>
<p>Leave the slot out and the compiler is explicit about the missing piece:</p>
<pre><code>error[E0046]: not all trait items implemented, missing: `Err`
--&gt; src/task.rs:103:1
|
103 | impl FromStr for Task {
| ^^^^^^^^^^^^^^^^^^^^^ missing `Err` in implementation
|
= help: implement the missing item: `type Err = /* Type */;`</code></pre>
<p>And call <code>.parse::&lt;Task&gt;()</code> before implementing it at all:</p>
<pre><code>error[E0277]: the trait bound `Task: FromStr` is not satisfied
--&gt; src/store.rs:98:29
|
98 | tasks.push(line.parse::&lt;Task&gt;()?);
| ^^^^^ unsatisfied trait bound
|
help: the trait `FromStr` is not implemented for `Task`</code></pre>
<p>The whole thing, in the weather-log demo — run today, output below:</p>
<pre><code>#[derive(Debug, PartialEq)]
struct Reading { station: String, celsius: f64 }
impl FromStr for Reading {
type Err = String; // your error type goes in the slot
fn from_str(line: &amp;str) -&gt; Result&lt;Reading, String&gt; {
let (station, temp) =
line.split_once('=').ok_or_else(|| line.to_string())?;
Ok(Reading {
station: station.to_string(),
celsius: temp.parse().map_err(|_| line.to_string())?,
})
}
}</code></pre>
<pre><code>Ok(Reading { station: "oslo", celsius: -3.5 })
Ok(Reading { station: "lagos", celsius: 31.0 })
Err("broken")</code></pre>
<p>One line in there is doing something you have not seen before, so look at the inner
<code>temp.parse().map_err(|_| ..)?</code> closely. It parses a <code>f64</code>, and the failure it can produce is
a <code>ParseFloatError</code> — but notice what that failure <em>means</em> in this context. It does not mean "the
user typed a bad number at the keyboard"; it means "the line stored in this file is corrupt". Those are two
different problems, they deserve two different variants, and only one of them can be the one that <code>From</code>
produces automatically.</p>
<p>So the shape to remember is this: <strong><code>?</code> on its own handles the single canonical conversion,
and <code>map_err</code> is how you name a different variant for any other meaning of the same error type.</strong>
In the drill you will write both, a few lines apart, on the very same <code>ParseIntError</code> — the CLI path
keeps <code>?</code> and produces <code>BadId</code>, while the file path uses <code>map_err</code> and produces
<code>BadLine</code>.</p>
<h2>Part 4 — Three decisions the tests will hold you to</h2>
<h3>A missing file is not an error</h3>
<p>Picture the very first time anyone runs your CLI. There is no <code>tasks.txt</code> yet, because nothing has
ever created one. If <code>load</code> simply propagates whatever <code>fs::read_to_string</code> returns, that
first run prints an error to stderr and exits 1 — the program looks broken before the user has done anything wrong.
A missing file here is not a failure at all; it is the normal starting state, and it means "you have no tasks yet".</p>
<p>So this is one of the rare places where you deliberately catch a single kind of error and turn it into a
successful result, while letting every other kind through untouched:</p>
<pre><code>let text = match fs::read_to_string(path) {
Ok(text) =&gt; text,
Err(e) if e.kind() == io::ErrorKind::NotFound =&gt; return Ok(Store::new()),
Err(e) =&gt; return Err(TaskError::Io(e)), // permissions etc. still fail
};</code></pre>
<p>The new syntax is <code>Err(e) if ..</code>, which is called a <strong>match guard</strong>: an extra condition
attached to an arm, so the arm only matches when the pattern fits <em>and</em> the condition holds. Here the first
<code>Err</code> arm catches only <code>NotFound</code>, and anything else falls through to the arm below it.</p>
<p>It is worth being clear about what this is not, because the lazy version is tempting. This is not
<code>unwrap_or_default()</code> and it is not <code>.ok()</code>. Both of those would treat <em>every</em> io
failure as "no tasks" — so a permissions problem, or a disk that has gone read-only, would silently present the
user with an empty list, and the next <code>save</code> would overwrite their real file with nothing. One specific
kind of failure is expected; the rest genuinely are failures and must still be reported.</p>
<h3>The saved format is not the <code>Display</code> format</h3>
<p>You already have a <code>Display for Task</code> from lesson 0005, and it prints
<code>1 [done] buy milk (high)</code>. That is a good sentence for a person reading a terminal, and it is a poor
format to read back in: to reconstruct the task you would have to find the brackets and parentheses, while allowing
for a title that might itself contain either. The format fights you because it was never designed to be parsed.</p>
<p>Storage has different requirements from presentation, so give it its own format — one with a separator that
splits cleanly and a fixed field order:</p>
<pre><code>1|done|high|buy milk</code></pre>
<p>Two audiences, two formats: <code>Display</code> stays exactly as it is for the <code>list</code> command, and
<code>to_line</code> is added beside it for the file. Do not be tempted to make one serve both.</p>
<p>Notice also that the title is placed <em>last</em>. That is deliberate, and it lets the title contain anything at
all, including the separator itself. The reason is how <code>splitn</code> works:
<code>"a|b|c|d|e".splitn(4, '|')</code> stops splitting after it has produced four pieces, so the fourth piece is
the entire remainder, <code>"d|e"</code>, with its <code>|</code> intact. Reach for <code>split</code> instead and a
title containing a pipe silently loses everything after it. One of the shipped tests covers exactly this case.</p>
<h3><code>#[derive(PartialEq)]</code> will break, and that is informative</h3>
<p>Add <code>Io(io::Error)</code> to the enum and the derive on line 3 fails:</p>
<pre><code>error[E0369]: binary operation `==` cannot be applied to type `&amp;std::io::Error`
--&gt; src/error.rs:12:8
|
3 | #[derive(Debug, PartialEq)]
| --------- in this derive macro expansion
...
12 | Io(std::io::Error),
| ^^^^^^^^^^^^^^
|
note: `std::io::Error` does not implement `PartialEq`</code></pre>
<p>The note at the bottom is the interesting part: <code>io::Error</code> deliberately does not implement
<code>PartialEq</code>. That is a considered decision by the std authors, not an oversight — two io failures can
carry the same message and still come from entirely different OS state, so "are these two errors equal?" has no
honest answer. Your enum now contains one, and equality for the whole enum is therefore no longer derivable.</p>
<p>This is also a useful moment to see what <code>derive</code> actually is. It is not a language feature attached
to the type; it is a code generator that writes an ordinary <code>impl</code> for you, comparing every field with
<code>==</code>. When one field cannot be compared, the generated line does not compile, and you get the error
above pointing at the derive itself.</p>
<p>Dropping <code>PartialEq</code> is not an option, because the 0006 tests compare <code>TaskError</code> values
with <code>assert_eq!</code> and you may not edit them. So write the impl by hand instead. This part is mechanical
rather than conceptual — type it, understand the three notes underneath, and move on:</p>
<pre><code>impl PartialEq for TaskError {
fn eq(&amp;self, other: &amp;Self) -&gt; bool {
use TaskError::*;
match (self, other) {
(UnknownCommand(a), UnknownCommand(b))
| (BadPriority(a), BadPriority(b))
| (BadLine(a), BadLine(b)) =&gt; a == b,
(BadId(a), BadId(b)) =&gt; a == b,
(NotFound(a), NotFound(b)) =&gt; a == b,
(Io(a), Io(b)) =&gt; a.kind() == b.kind(), // the kind, not the error
_ =&gt; std::mem::discriminant(self) == std::mem::discriminant(other),
}
}
}</code></pre>
<p>Three pieces of that impl are new, and each is useful well beyond this one function.</p>
<p>First, <code>match (self, other)</code> matches on a <strong>tuple of two values at once</strong>. You build a
temporary pair and pattern-match both halves together, which is how you ask "are these the same variant, and if so,
are their payloads equal?" in a single expression.</p>
<p>Second, <code>(A(a), A(b)) | (B(a), B(b)) =&gt;</code> is an <strong>or-pattern</strong>: several patterns
sharing one arm. Rust allows it here because every alternative binds the same names, <code>a</code> and
<code>b</code>, at the same types, so the arm's body is valid whichever alternative matched. That is what lets three
<code>String</code>-carrying variants share a single line instead of taking three.</p>
<p>Third, <a href="https://doc.rust-lang.org/std/mem/fn.discriminant.html"><code>mem::discriminant</code></a>
returns an opaque value identifying <em>which</em> variant a value is, ignoring any payload. Comparing two of them
answers "same variant?" without your having to name the variants at all, which handles the four payload-free cases
in one line.</p>
<p>That last convenience has a real cost, and it is the kind of thing to notice now rather than discover later. The
<code>_</code> arm means the compiler will never again force you to update this impl when you add a variant — and a
new variant carrying data would then be compared by variant alone, treating two different payloads as equal. It is
an acceptable trade for a small error type, but it is a trade, not a free win.</p>
<h2>Check yourself before the drill</h2>
<p>Six questions before you touch the keyboard. Try to answer each one out loud, in full sentences, before you
reveal or click — an answer you can say is an answer you have understood, and one you can only recognise on a page
usually is not. Getting one wrong here costs you nothing; getting the same thing wrong twenty minutes into the
drill costs you the drill.</p>
<div class="q" data-type="mcq" data-topic="Traits">
<p class="topic">Traits</p>
<p class="prompt">You have <code>impl From&lt;ParseIntError&gt; for TaskError</code>. Which second impl is rejected by the compiler?</p>
<div class="options">
<button class="opt" data-correct="true">A second <code>From&lt;ParseIntError&gt; for TaskError</code></button>
<button class="opt" data-correct="false">An added <code>From&lt;io::Error&gt; for TaskError</code></button>
<button class="opt" data-correct="false">An added <code>From&lt;ParseIntError&gt; for LineError</code></button>
<button class="opt" data-correct="false">An added <code>From&lt;ParseFloatError&gt; for TaskError</code></button>
</div>
<div class="explain hidden">One impl per (trait, source type, target type). Two impls for the same <code>T</code> give <code>error[E0119]: conflicting implementations</code>, because <code>?</code> would have no single answer for what to convert into. Changing either the source type or the target type makes it a different impl, so the other three are all legal.</div>
</div>
<div class="q" data-type="recall" data-topic="Traits">
<p class="topic">Traits</p>
<p class="prompt">What is <code>type Err</code> in <code>impl FromStr</code>, and why is it not written <code>impl FromStr&lt;Err&gt;</code>?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">It is an <strong>associated type</strong>: a slot in the trait that the implementor fills in exactly once. A generic parameter is chosen by the <em>caller</em> and lets one type implement the trait many times; an associated type is chosen by the <em>implementor</em>, so <code>Task</code> implements <code>FromStr</code> once and <code>"…".parse::&lt;Task&gt;()</code> is never ambiguous about which error comes back. Omit it and you get <code>error[E0046]: missing Err in implementation</code>.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="mcq" data-topic="Error handling">
<p class="topic">Error handling</p>
<p class="prompt">Your <code>load</code> calls <code>fs::read_to_string(path)?</code> and the file does not exist yet. What does the user see on their first ever run?</p>
<div class="options">
<button class="opt" data-correct="false">An empty task list, printed normally</button>
<button class="opt" data-correct="true">An error on stderr, and exit code 1</button>
<button class="opt" data-correct="false">A new empty file, created silently</button>
<button class="opt" data-correct="false">A panic with a full stack backtrace</button>
</div>
<div class="explain hidden">A bare <code>?</code> propagates every <code>io::Error</code>, including <code>ErrorKind::NotFound</code>. Your <code>main</code> prints it to stderr and exits 1 — so a brand-new install looks broken. That is why <code>load</code> needs the match guard <code>Err(e) if e.kind() == ErrorKind::NotFound =&gt; Ok(Store::new())</code>, and why the shipped test <code>a_missing_file_is_an_empty_store_not_an_error</code> exists.</div>
</div>
<div class="q" data-type="recall" data-topic="Error handling">
<p class="topic">Error handling</p>
<p class="prompt">Both <code>done abc</code> (a CLI argument) and a corrupt saved line produce a <code>ParseIntError</code>. You want two different variants. How, given only one <code>From</code> impl is allowed?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden"><code>?</code> for the canonical one, <code>map_err</code> for the other. The CLI path keeps <code>id.parse()?</code>, which calls <code>From</code> and yields <code>BadId</code>. The file path writes <code>part.parse().map_err(|_| TaskError::BadLine(line.to_string()))?</code>, choosing the variant explicitly. Same error type, two meanings, and the meaning is a property of the call site, not of the type — which is exactly what <code>From</code> cannot express.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="recall" data-topic="Ownership">
<p class="topic">Ownership</p>
<p class="prompt"><code>fn load(path: &amp;Path) -&gt; Result&lt;Store, TaskError&gt;</code> — no <code>&amp;self</code>, and it returns a <code>Store</code> by value. Why is that not a copy, and where does the returned value live?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">It is an <strong>associated function</strong>, not a method — no receiver, called as <code>Store::load(&amp;path)</code>, the same shape as <code>Store::new()</code>. Returning by value <em>moves</em> ownership to the caller; the <code>Vec</code>'s heap buffer is never copied, only the three-word handle. <code>&amp;Path</code> is borrowed because <code>load</code> only reads the path and the caller keeps it for the later <code>save</code>.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div class="q" data-type="recall" data-topic="Enums">
<p class="topic">Enums</p>
<p class="prompt">After loading two tasks from a file, why must <code>Store</code> recompute its next id instead of starting at 1?</p>
<button class="reveal-btn">Show answer</button>
<div class="answer hidden">Because the counter is state that lives in memory only, and the file does not save it. Load without recomputing and the next <code>add</code> hands out id 1 again — two tasks share an id, and <code>done 1</code> silently completes the wrong one. The fix is one line: <code>tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1</code>. The test <code>ids_do_not_restart_after_a_reload</code> is exactly this bug, caught.</div>
<div class="grade hidden">
<button data-grade="hit">Got it</button>
<button data-grade="miss">Missed it</button>
</div>
</div>
<div id="summary"><div id="summary-body"></div><p id="summary-total"></p>
<button id="report-btn">Copy report</button><pre id="report-output" class="hidden"></pre></div>
<h2>The drill — 30 minutes, your own crate</h2>
<p>Type it, do not paste it. The weather log above is a different program. Keep the
<a href="../reference/rust-syntax.html#files">files &amp; FromStr reference</a> open — looking syntax up is free.</p>
<pre><code>cd ~/learn-rust/tasks
cp ../lessons/0007-persist-spec.rs tests/persist.rs
cargo test # 8 new tests fail to compile — that is the starting line</code></pre>
<p>Do not edit anything in <code>tests/</code>. All 24 existing tests must still pass. Target at the end:
<strong>32 passing</strong>.</p>
<h3>Step 0 — pay off 0006 (2 minutes)</h3>
<p>Delete both <code>match id.parse()</code> blocks in <code>command.rs</code>. Each becomes one line, and the
<code>From</code> impl you wrote last lesson finally does its job:</p>
<pre><code>let id: u32 = args.get(1).ok_or(TaskError::MissingId)?.parse()?;</code></pre>
<p><strong>Check:</strong> <code>grep -c "match id.parse" src/command.rs</code> prints <code>0</code>, and
<code>cargo test --test errors</code> still passes 7.</p>
<h3>Step 1 — two new variants, and a hand-written <code>PartialEq</code></h3>
<p>Add to <code>TaskError</code>:</p>
<pre><code>BadLine(String), // a saved line that cannot be read back — carries the line
Io(io::Error), // the file could not be read or written — wraps std's error</code></pre>
<p>Then: remove <code>PartialEq</code> from the derive and write the impl from Part 4; add both
<code>Display</code> arms; add <code>Io(e) =&gt; Some(e)</code> to <code>source()</code>; add
<code>From&lt;io::Error&gt;</code>. Exact messages, checked by the tests:</p>
<table>
<tr><th>Variant</th><th><code>to_string()</code> must be</th></tr>
<tr><td><code>BadLine("rubbish")</code></td><td><code>cannot read saved line: rubbish</code></td></tr>
<tr><td><code>Io(..)</code></td><td><code>cannot read or write the task file</code></td></tr>
</table>
<p><strong>Check:</strong> <code>cargo build</code> passes, and <code>cargo test --test errors</code> is still 7/7
— the old variants must behave exactly as before.</p>
<h3>Step 2 — <code>task.rs</code>: one line out, one line in</h3>
<p>Three additions:</p>
<ul>
<li><code>Status::parse(&amp;str) -&gt; Option&lt;Status&gt;</code> — the mirror of <code>label()</code>, same shape as
<code>Priority::parse</code>, which you already have.</li>
<li><code>Task::to_line(&amp;self) -&gt; String</code> — <code>id|status|priority|title</code>.</li>
<li><code>impl FromStr for Task</code> with <code>type Err = TaskError</code>, using
<code>splitn(4, '|')</code>. Every failure is <code>BadLine(line.to_string())</code> — including the id, which is
where <code>map_err</code> earns its keep.</li>
</ul>
<p><strong>Check:</strong> <code>cargo test --test persist a_task_becomes</code> and
<code>cargo test --test persist a_title_may</code> both pass.</p>
<details>
<summary>Stuck on repeating <code>BadLine(line.to_string())</code> five times?</summary>
<p>Bind it once as a closure and hand it to <code>ok_or_else</code>: <code>let bad = || TaskError::BadLine(line.to_string());</code>
then <code>parts.next().ok_or_else(bad)?</code>. Note <code>ok_or_else</code>, not <code>ok_or</code> — the first
takes a closure and only builds the error when there is one, the second builds it every time. With a
<code>String</code> allocation inside, that difference is real.</p>
</details>
<h3>Step 3 — <code>store.rs</code>: <code>save</code> and <code>load</code></h3>
<pre><code>pub fn save(&amp;self, path: &amp;Path) -&gt; Result&lt;(), TaskError&gt;
pub fn load(path: &amp;Path) -&gt; Result&lt;Store, TaskError&gt; // associated fn</code></pre>
<p><code>save</code> builds one <code>String</code> — one line per task, each ending in <code>\n</code> — and calls
<code>fs::write</code> once. <code>load</code> reads, skips empty lines, parses each into a <code>Task</code>, and
recomputes the next id. Both the missing-file guard and the id recomputation are in Part 4.</p>
<p><strong>Check:</strong> <code>cargo test --test persist</code> passes all 8.</p>
<h3>Step 4 — <code>main.rs</code>: load, act, save</h3>
<p>Where the file lives should not be hard-coded into the logic. One line of ch12 gets you an override for free:</p>
<pre><code>let path = PathBuf::from(
env::var("TASKS_FILE").unwrap_or_else(|_| "tasks.txt".to_string()));</code></pre>
<p>Then <code>run</code> takes the path, loads the store itself, and saves at the end. Note the ordering that falls
out of <code>?</code>: an error anywhere means <code>save</code> is never reached, so a failed command cannot
corrupt the file.</p>
<p><strong>Check — your CLI now remembers things:</strong></p>
<pre><code>$ cd $(mktemp -d) # empty dir: proves a first run works with no file
$ run(){ TASKS_FILE=t.txt cargo run -q --manifest-path ~/learn-rust/tasks/Cargo.toml -- "$@"; }
$ run add "buy milk" high
added task 1
$ run add "call bank"
added task 2
$ run done 1
completed 1
$ run list
1 [done] buy milk (high)
2 [todo] call bank (medium)
$ cat t.txt
1|done|high|buy milk
2|todo|medium|call bank
$ echo "rubbish" &gt;&gt; t.txt ; run list ; echo $?
error: cannot read saved line: rubbish
1</code></pre>
<p>That last one is the payoff for <code>BadLine(String)</code> carrying the line: the message names the exact
text to go and fix. A <code>String</code> error, or a bare <code>Io</code>, could not.</p>
<p><strong>Final check:</strong> <code>cargo test</code> → 17 + 7 + 8 = <strong>32 passed</strong>. And add
<code>tasks.txt</code> to <code>.gitignore</code> — it is user data, not source.</p>
<h3>Then stop</h3>
<p>Not today: <code>serde</code> and JSON (the real answer for storage, but it teaches a crate rather than a
concept), file locking, and <code>BufReader</code> for files too big to hold in memory. Your task file is a few
kilobytes; <code>read_to_string</code> is the correct tool, and reaching for a buffered reader here would be
copying a pattern you do not need.</p>
<h2>What this closed</h2>
<p>Chapter 12 moves to <em>produced</em> on the <a href="../reference/book-coverage.html">coverage map</a>:
<code>env::args</code>, <code>env::var</code>, <code>fs</code>, stderr, and exit codes are now all in your own
code. Two gaps left before the job-ready floor, and they are next:</p>
<ol>
<li><strong>ch 8 + 13 — <code>HashMap</code>, <code>map</code>/<code>filter</code>/<code>collect</code></strong>,
plus your first hand-written generic function (lesson 0008). Your <code>load</code> loop is a
<code>collect::&lt;Result&lt;Vec&lt;_&gt;, _&gt;&gt;()</code> waiting to happen — I left it as a <code>for</code> loop
deliberately so 0008 has something of yours to rewrite.</li>
<li><strong>ch 11 — writing your own tests</strong>: you have now consumed 32 of mine and written zero
(lesson 0009).</li>
</ol>
<h2>Take it outside</h2>
<p>The forum ask from 0006 still stands and is now stronger: <code>error.rs</code> holds both
<em>user mistakes</em> (<code>UnknownCommand</code>, <code>BadPriority</code>) and <em>system failures</em>
(<code>Io</code>, <code>BadLine</code>) in one enum. Many Rust developers would split those into two types. Post it
on <a href="https://users.rust-lang.org">users.rust-lang.org</a> (Code Review category) and ask which they would
do and why. That is a genuine design question with real disagreement behind it — the answer you get back is wisdom
you cannot derive from the book.</p>
<h2>The five sentences worth keeping</h2>
<ol>
<li><code>io::Result&lt;T&gt;</code> is just <code>Result&lt;T, io::Error&gt;</code>; <code>e.kind()</code> is how you
tell one io failure from another.</li>
<li>One <code>impl From&lt;T&gt; for YourError</code> per <code>T</code> — a different <code>T</code> is a new impl,
a repeat <code>T</code> is <code>E0119</code>.</li>
<li><code>?</code> for the canonical conversion, <code>map_err</code> for every other meaning of the same error.</li>
<li><code>FromStr</code> is what <code>.parse()</code> calls; <code>type Err</code> is an associated type — a slot
the implementor fills, not a parameter the caller passes.</li>
<li>A missing file on first run is expected, not exceptional: match <code>ErrorKind::NotFound</code>, and let every
other kind fail loudly.</li>
</ol>
<footer>
<p><strong>Primary source:</strong> The Rust Book
<a href="https://doc.rust-lang.org/stable/book/ch12-00-an-io-project.html">chapter 12 — An I/O Project</a>, and
specifically <a href="https://doc.rust-lang.org/stable/book/ch12-02-reading-a-file.html">12.2 reading a file</a>
and <a href="https://doc.rust-lang.org/stable/book/ch12-05-working-with-environment-variables.html">12.5
environment variables</a>. Then the one-screen
<a href="https://doc.rust-lang.org/std/str/trait.FromStr.html">std::str::FromStr</a> page — read the
<code>Point</code> example there, it is the same shape as your <code>Task</code>.</p>
<p>Previous: <a href="0006-your-own-error-type.html">0006 — Your own error type</a> ·
<a href="0005-traits-display-and-errors.html">0005 — Traits, Display, errors</a> ·
<a href="0004-structs-enums-packages.html">0004 — Structs, enums, packages</a><br />
Reference: <a href="../reference/rust-syntax.html#files">Files &amp; FromStr</a> ·
<a href="../reference/rust-syntax.html#error-types">Custom error types</a> ·
<a href="../reference/book-coverage.html">Coverage map</a></p>
<p><strong>Ask me things.</strong> Bring the compiler output verbatim — step 1 breaks the derive and step 2 is the
first trait you have implemented with an associated type. If a paragraph did not land, name it; that is my fault to
fix, and cheaper to fix now than mid-drill.</p>
</footer>
<script src="../assets/quiz.js"></script>
</body>
</html>