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
Option and Copy
The 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