TL;DR: A simple crate to read and write directory structures.
Motivation
There were a few times when I needed to read or write a directory structure, but I didn’t want to use the std::fs API directly. If you think about it, it is pretty simple to read, parse and write directory structures, so I went through a lot of crates on crates.io that referenced directories, but I didn’t really find anything that would do this. So I’ve decided to write my own.
This blog post serves both as a guide to getting started with dir-structure, as well as a bit of documentation on the crate itself.
Getting started
The crate is available on crates.io, so you can just add it to your Cargo.toml:
But before being able to use it, we need to first think of what kind of directory structure we want to model. The particular use case I had in mind was a directory, filled with many other subdirectories, each containing 2 files: input.txt and output.txt, which would later be used for testing purposes (via a crate like libtest-mimic for example), but the directory structure part can be applied to many other use cases.
In the example above, we knew that the root directory always had the same 4 subdirectories, but what if we didn’t know that? What if we wanted to read a directory structure that had an arbitrary number of subdirectories, but their contents all shared the same structure?
Well, we can do that really easily too:
Since by default the DirStructure derive macro will use a child directory with the name of the field, we have to explicitly tell it that we want to use the current directory instead. We can do that by adding the #[dir_structure(path = self)] attribute.
Lazy reading of directory contents
In the examples above, we read the directory structure immediately, but what if we had so much data that we didn’t want to read it all at once? Well, the library also provides a DeferredRead<T> type, which only stores the path, and will read the value later when we explicitly ask it to.
In this example however, we have to explicitly call perform_read on the DeferredRead values, which is a bit annoying, as it does no caching of the values. So if we wanted to read the same value multiple times, we would have to call perform_read multiple times, which would be inefficient. We will explore an alternative in the next section.
Lazy and cached reading of directory contents
In the previous example, we had to explicitly call perform_read on the DeferredRead, but if we have to read it multiple times, it would be inefficient. So we can use the DeferredReadOrOwn<T> type, which is also able to cache the value, so that we don’t have to read it multiple times.
In a nutshell, here is the API:
Versioning of file contents
The library also exposes the Versioned wrapper, which wraps a value and counts how many times it has been modified.
There are 2 ways to modify a value wrapped in a Versioned:
Use its Deref and DerefMut implementations: using DerefMut increments the version.
Use the edit_eq_check associated function, which applies the given closure to the wrapped value and checks whether it has been modified during the execution of the closure; if it has, then it increments the version. It needs the wrapped type to implement Eq and Clone.
An example of how one might use it:
Reading / writing JSON
With the json feature, we can also read and write JSON files using serde_json.
Library internals
The whole library works with 2 building-block traits: ReadFrom and WriteTo.
The ReadFrom trait is implemented for types that can be read from a path, while the WriteTo trait is the opposite of that, and is implemented for types that can be written to a path.
They are both implemented for types that represent whole directory structures, as well as for types that represent individual files.
Directory structures are read and written recursively, so if we had a directory structure like in the beginning:
The FmtWrapper<T> type is a newtype around T, which implements ReadFrom and WriteTo using std::fmt::Display and std::str::FromStr.
It can be used like this:
#[dir_structure(with_newtype = T)]
The specific traits involved in the conversions are:
NewtypeToInner is pretty much straight-forward, but FromRefForWriter is a bit more complicated. It is used to convert a reference to the inner type to a type that holds said reference and implements WriteTo. Essentially it is a newtype around &'a Self::Inner which implements WriteTo.
Both of those functions are used when we use a with_newtype attribute on a field.
In the general case:
The following bounds must be satisfied for the with_newtype attribute to work: