In our brand new write function, we need to create a temporary file that we can use to let the user write whatever they want.
To do that, and pass control to the user and back, we’ll use the edit crate which in turn relies on the tempfile crate
❯ cargo add edit
Updating crates.io index
Adding edit v0.1.4 to dependencies.
Features:
+ better-path
+ which
- quoted-env
- shell-words
Updating crates.io index
The edit crate contains functionality that will allow us to send the user to their editor of choice, let them write in the file we give them, and then allow us to read the file after the user has modified it.
Here’s what our new write function is going to look like. There are a couple variables we aren’t using, so to silence warnings about that we prefix the variables with _, which we can remove later.
There’s a decent amount going on here so let’s walk through it.
use edit::Builder;
use std::path::PathBuf;
pub fn write(
garden_path: PathBuf,
_title: Option<String>,
) -> Result<(), std::io::Error> {
let (_file, filepath) = Builder::new()
.suffix(".md")
.rand_bytes(5)
.tempfile_in(garden_path)?
.keep()?;
dbg!(filepath);
Ok(())
}
however, these are not the only changes we need to make.
If we build, check, or run our application we’ll see compilation errors.
❯ cargo build
Compiling garden v0.1.0 (/rust-adventure/digital-garden)
error[E0308]: mismatched types
--> src/main.rs:69:13
|
40 | fn main() {
| - expected `()` because of default return type
...
69 | garden::write(garden_path, title)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^- help: consider using a semicolon here: `;`
| |
| expected `()`, found `Result<(), Error>`
|
= note: expected unit type `()`
found enum `Result<(), std::io::Error>`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `garden` (bin "garden") due to previous error
We’ve added a Result as the return type of our write function, and we’re also returning the return value of garden::write in main.rs, so what the error is telling us is that main isn’t currently typed to return a Result, it is typed to return (), which is the default return type for Rust functions.
One option is to use ; to throw away the value returned from write, but main also lets us return a Result, so let’s do that instead.
fn main() -> Result<(), std::io::Error> {
With that return value (which just mirrors the one from write) in place, our program will run.
We’ll come back to examine these types later.
To see the file being created, you may also want to add a small dbg in lib.rs.
dbg!(filepath);
Ok(())
then you can see the temporary file being created and the filepath it was created at.
❯ 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/.tmp41o12.md"
What’s going on here?
The edit crate gives us a type called Builder that we can use to create a new temporary file in a specific directory.
Our Builder::new() is
- creating a file with the suffix
.md, which is a markdown file extension.- with
.suffix(".md")
- with
- including a series of random bytes in the filename to try to avoid conflicts with anything else that might be in the directory.
- with
.rand_bytes(5)
- with
- creating the actual file (which could fail)
- and keeping the file around more permanently, which could also fail.
- with
.keep()?
- with
In Rust this is usually called a builder pattern. We kick off the process with new(), which returns us a Builder instance. We can then take that instance and continue to configure it using suffix and rand_bytes , which both continue to return a Builder before making use of that configuration to create the actual file with tempfile_in where the return type changes.
tempfile_in returns a Result<NamedTempFile, std::io::Error>. This is the same error we’ve modified our write function to return, which is why the ? syntax works here.
The ? will return an Err variant from the write function, or it will unwrap an Ok variant and return the inner value.
So after ? we have either returned from the function entirely, or we have a NamedTempFile.
Since we can assume we have a NamedTempFile, we can call .keep on it.
Converting Errors Automatically
.keep has its own return type: Result<(File, PathBuf), PersistError>... which we also use ? on… But a PersistError is not a std::io::Error, so what’s going on?
A PersistError is defined as a struct. The struct itself holds a std::io::Error in the error field, so you could imagine how to convert a PersistError to a std::io::Error yourself: return the .error field.
pub struct PersistError {
pub error: Error,
pub file: NamedTempFile,
}
In the tempfile source code, which is where PersistError comes from is this bit of code.
impl From<PersistError> for io::Error {
#[inline]
fn from(error: PersistError) -> io::Error {
error.error
}
}
This is an implementation of the Fromtrait for converting between aPersistErrorand anio::Error`.
Having a From implementation like this is super useful for us because the ? operator passes the error value through the From::from function.
So ? is automatically handling the conversion from a PersistError to the io::Error we want by using the From implementation.
That’s why we can use ? on a PersistError even though we want to return an io::Error.
Now we do lose some information doing this, like the NamedTempFile , but for now that’s an acceptable tradeoff. We’ll come back to this later.
Being Ok
Finally, because our write function, and the main function as well, return Result, our final return value if everything goes well is going to be Ok(()).