Right now we have a temporary file with a temporary filename. The temporary file will stick around if our program crashes for some reason, so the user won’t lose any data, but the filename isn’t what the user wanted yet.
We’ll want to rename the file to something that is related to the title.
This is mostly what we’ll end up with, but it does have one major issue still: we use title
after we move it into the format!
macro.
use edit::{edit_file, Builder};
use std::{fs, io::Write, path::PathBuf};
pub fn write(
garden_path: PathBuf,
title: Option<String>,
) -> Result<(), std::io::Error> {
let (mut file, filepath) = Builder::new()
.suffix(".md")
.rand_bytes(5)
.tempfile_in(garden_path)?
.keep()?;
dbg!(&filepath);
let template =
format!("# {}", title.unwrap_or("".to_string()));
file.write_all(template.as_bytes())?;
edit_file(&filepath)?;
let contents = fs::read_to_string(&filepath)?;
let document_title = title.or_else(|| {
contents.lines().find(|v| v.starts_with("# ")).map(
|line| {
line.trim_start_matches("# ").to_string()
},
)
});
dbg!(document_title);
Ok(())
}
Running the program shows us the use of moved value title
.
❯ cargo run -- write -t "My New Post"
Compiling garden v0.1.0 (/rust-adventure/digital-garden)
error[E0382]: use of moved value: `title`
--> src/lib.rs:20:26
|
6 | title: Option<String>,
| ----- move occurs because `title` has type `Option<String>`, which does not implement the `Copy` trait
...
15 | format!("# {}", title.unwrap_or("".to_string()));
| ----- value moved here
...
20 | let document_title = title.or_else(|| {
| ^^^^^ value used here after move
|
help: consider cloning the value if the performance cost is acceptable
|
15 | format!("# {}", title.clone().unwrap_or("".to_string()));
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `garden` (lib) due to previous error
and Copy
OptionThe issue is that our title has the type Option<String>
, which doesn’t implement the Copy
trait.
Usually variables and values in Rust have what we call “move semantics”, which moves the value into the function when we use it because a value in Rust can only have a single owner.
Implementing Copy
would make the value have “copy semantics” instead. This is what happens when you use a simple primitive type like a number. That number is copied around rather than being moved.
An Option<T>
will automatically implement Copy
if the T
type inside of the Option
implements Copy
.
String
, however, doesn’t (and can’t) implement Copy
. This is because of the restrictions on Copy
such that it can’t be implemented for a type that also implements the Drop
trait, which String
implements.
String
implements Drop
automatically, because a String
is a Vec<u8>
under the hood, and Vec<u8>
implements drop.
Cloning
The error message suggests cloning, which will work, because it makes a full copy of the Option<String>
value and moves the new value into format
, leaving the old value alone.
format!("# {}", title.clone().unwrap_or("".to_string()));
We can run the application now.
❯ cargo run -- write -t "My New Post"
Compiling garden v0.1.0 (/rust-adventure/digital-garden)
Finished dev [unoptimized + debuginfo] target(s) in 0.68s
Running `target/debug/garden write -t 'My New Post'`
[src/lib.rs:13] &filepath = "/Users/chris/garden/.tmpMpHg4.md"
[src/lib.rs:31] document_title = Some(
"My New Post",
)
There’s another way we can fix this though. Remember that the Deref
trait allows us to convert between types.
There’s a function on Option
called as_deref
. Option::as_deref()
creates a new Option
with a reference to the inner value. The inner data is still owned by the original Option
.
The key for us is that as_deref
also runs the inner type through the Deref
trait, which for us converts to a str
.
This means running title.as_deref()
converts from Option<String>
to Option<&str>
. The outer Option
is new, and the inner &str
is a reference to the original String
.
This allows us to also remove the to_string()
in our unwrap_or
, since we’re working with &str
now.
let template = format!("# {}", title.as_deref().unwrap_or(""));
It’s almost like we cloned the Option
without cloning the String
by using as_deref
.
Reading the temporary file
If the user passed in a title
, we’re going to use that for the new filename. If not, then we’re going to try to find a markdown heading in the contents of the file that they wrote.
Reading the file into a String
is possible using std::fs::read_to_string
.
let contents = fs::read_to_string(&filepath)?;
read_to_string
returns an io::Result<String>
, so that gives us the same io::Error
and we can use ?
to handle the error again.
Finding a markdown heading
title
is an Option<String>
, so we can use or_else
to run a function to find a markdown title if the title
is None
.
Given a markdown file, headings are written as a hashtag with a space then the title.
This means we can use .lines()
to get an Iterator
over all of the lines in the user’s file, then .find
to test each line to look for a heading, which is a line that starts_with("# ")
.
let document_title = title.or_else(|| {
contents.lines().find(|v| v.starts_with("# ")).map(
|line| {
line.trim_start_matches("# ").to_string()
},
)
});
find
might not find a line that matches, so it returns us an Option
, which we can .map
into.
If we’re in the .map
then we have the line we’re looking for, so using .trim_start_matches
we can strip the hashtag and the space off the front of the string and return just the title.
Running the program
If we run the program now we’ll get a document_title
of whatever title we pass in.
❯ cargo run -- write -t "My New Post"
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/garden write -t 'My New Post'`
[src/lib.rs:13] &filepath = "/Users/chris/garden/.tmpcRQaT.md"
[src/lib.rs:27] document_title = Some(
"My New Post",
)
Or we’ll get the opportunity to write in a title in the document, which will also be found.
❯ cargo run -- write
Finished dev [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/garden write`
[src/lib.rs:13] &filepath = "/Users/chris/garden/.tmpJ8N2D.md"
[src/lib.rs:27] document_title = Some(
"Some new Post",
)
❯ cat /Users/chris/garden/.tmpJ8N2D.md
# Some new Post