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 From
trait for converting between a
PersistErrorand an
io::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(())
.