"Bypassing" specialization in Rust...: Follow-up
This is a follow-up to my previous article
The issue
The method described in that article has a flow. Take the following example:
use simple_fatfs::*;
fn main() {
// we don't care about the storage implementation, just keep in mind that
// it implements all of the Read, Write & Seek trait
let mut rw_storage = ...;
let fs = FileSystem::from_ro_storage(&mut rw_storage).unwrap();
let file = fs.get_ro_file(/* Path goes here */).unwrap();
}
Everything seems alright until we try to get that file using the
get_rw_file method. Since
rw_storage implements Read,
Write & Seek, we can call the
get_rw_file method as if everything is fine. In that case,
we can modify and write to the RWFile, but after we
Drop it, those data we have modified won't be synced back
to the filesystem, since the sync_fn field will be
None
The solution
Instead of using 2 constructors, one for a RO filesystem and another for
a R/W filesystem, we use one for both cases. We also keep the
sync_fn and other similar fields and upon initialization we
set them to None. We then need to modify our
Write-related functions so that each time after a
Write-related operation has occured, they set the
sync_fn and the other related fields to
Some value containing the function pointer to the sync
function.
That's how our code should look like with that in mind:
struct FileSystem<S> where S: Read + Seek {
// we got some fields here...
storage: S
sync_fn: Option<SyncFn>
// ...and some more there
};
impl<S> FileSystem<S> where S: Read + Seek {
pub fn from_storage(storage: S) -> Self {
// code, lot's of code
Self {
storage: S,
sync_fn: None
}
}
}
impl<S> FileSystem<S> where S: Read + Write + Seek {
fn set_modified(&mut self) {
self.sync_fn = Some(Self::sync_sector);
}
fn sync_sector(&mut self) {
// implementation doesn't matter here
}
}
impl<S> FileSystem<S> where S: Read + Seek {
pub fn get_ro_file(&mut self) -> ROFile {
// code, a lot of code
rofile
}
}
impl<S> FileSystem<S> where S: Read + Write + Seek {
pub fn get_rw_file(&mut self) -> RWFile {
// code, a lot of code
rwfile
}
pub fn create_file(&mut self) -> Result<RWFile, Error> {
// code, a lot of code
self.set_modified();
created_file
}
}
Why did it take me a while to stumble upon this?
Not enough testing. That's the answer.
Even though I have loads of unit tests that are run by a CI each time I
push something to Github, I didn't have one for this particular case. In
fact, now that I have finally finished with most of the
Write-related stuff, testing is an area of my crate that I
feel like it need some serious changes.
Today's lesson: no number of unit tests is enough.