Often times, I need to open a file with one of several reader structs. For instance, user may want to read .csv
, .csv.gz
, or .csv.bz2
. In GIS-related work, there's a dozen of formats, and in real life I must support at least 5 of them (GeoPackage, CSV, Shapefile, FlatGeobuf, GeoJSON).
So, a function opening them must return a dynamic type or an enum wrapper.
Is there a better way?
Box
ing seems shorter. But if I remember correctly, there are things you can't do with it.
fn open(path: std::path::Path) -> Result<Box<dyn Read>, MyErrorType> {
let fp = std::fs::File::open(path)?;
let rd = if path.ends_with(".csv.gz") {
Box::new(GzDecoder::new(fp)) as Box<dyn Read + Send>
} else if path.ends_with(".csv.bz2") {
Box::new(BzDecoder::new(fp)) as Box<dyn Read + Send>
} else if path.ends_with(".csv") {
Box::new(fp) as Box<dyn Read + Send>
} else {
return Err("unsupported file extension".into());
};
Ok(rd)
}
Enum seems more "grounded", but I suspect this one will not have Copy
or other necessary traits, for instance, to send into a thread.
enum ReaderWrapper {
Plain(File),
Gz(GzDecoder),
Bz2(BzDecoder)
}
impl Read for ReaderWrapper {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, IoError> {
match self {
// seems repetitive to me
ReaderWrapper::Plain(mut ref f) => self.f.read(buf),
ReaderWrapper::Gz(mut ref f) => self.f.read(buf),
ReaderWrapper::Bz2(mut ref f) => self.f.read(buf)
}
}
fn open(path: std::path::Path) -> Result<ReaderWrapper, MyErrorType> {
let fp = std::fs::File::open(path)?;
let rd = if path.ends_with(".csv.gz") {
ReaderWrapper::Gz(GzDecoder::new(fp)))
} else if path.ends_with(".csv.bz2") {
ReaderWrapper::Bz2(BzDecoder::new(fp))
} else if path.ends_with(".csv") {
ReaderWrapperBox::new(fp) as Box<dyn Read + Send>
} else {
return Err("unsupported file extension".into());
};
Ok(rd)
}
Maybe there's a macro to define this enum automatically, with all impl
s? (like From<File>
, From<GzDecoder>
etc.
type MyEnum = enum![File, GzDecoder, BzDecoder];
...
let rd:MyEnum = if path.endswith('.csv.gz') { GzDecoder::new(fp).into() }
else if path.endswith('.csv.bz2') { BzDecoder::new(fp).into() }
else { fp.into() };
// .into call From<T> for MyEnum which is generated by the macro automatically
This is such a frequent case, causes so much pain, and yet I didn't see a doc on this.
What suprises me is that Rust went further with the idea from dynamic languages, like Python, but didn't get it to the end.
In Python community, there's an advice to check if class has a particular method (if hasattr(my_file_obj, iter): ...
) instead of class per se (if isinstance(my_file_obj, GzipFile): ...
).
But it's not formalized in any way. Rust did formalize this in form of traits. But it works only at compile time. You can't return just an obj with a trait, e.g. fn (path: std::path::Path) -> dyn Read { ... }
.