I'm building a layered application, and I would like to use generic types to be able to test each layer independently of the others. Very generally, (and incorrectly, as lifetimes are omitted) this would look like
trait Layer1 {}
struct RealLayer2<L1: Layer1> {
layer1: &L1,
}
#[cfg(test)]
mod test {
struct FakeLayer1 {}
impl Layer1 for FakeLayer1 {}
#[test]
test_foo() {
let mut l2 = RealLayer2<FakeLayer1>{}
}
}
That is, RealLayer2
can be used monomorphically with FakeLayer1
to do some testing, and then again monomorphically with RealLayer1
in production code.
The first layer is a content-addressible storage layer, parameterized on a type to be stored, T. I have:
pub trait CAS<T> {
fn store(&self, value: &T) -> String;
fn retrieve(&self, hash: &String) -> Option<T>;
}
pub struct Storage<T: Encodable + Decodable> {
// ...
}
impl<T: Encodable + Decodable> Storage<T> {
/// Create a new, empty storage pool.
pub fn new() -> Storage<T> {
// ...
}
}
impl<T: Encodable + Decodable> CAS<T> for Storage<T> {
fn store(&self, value: &T) -> Hash {
// ...
}
fn retrieve(&self, hash: &Hash) -> Option<T> {
// ...
}
}
The second layer is a git-like filesystem. It requires something that implements CAS<Object>
, where Object
is a type specific to the filesystem layer.
pub trait FS<'a, C>
where C: 'a + CAS<Object>
{
fn root_commit(&self) -> Commit<'a, C>;
fn get_commit(&self, hash: Hash) -> Result<Commit<'a, C>, String>;
}
pub struct FileSystem<'a, C: 'a + CAS<Object>> {
storage: &'a C,
}
impl<'a, C> FileSystem<'a, C>
where C: 'a + CAS<Object>
{
pub fn new(storage: &'a C) -> FileSystem<'a, C> {
// ...
}
}
impl<'a, C> FS<'a, C> for FileSystem<'a, C>
where C: 'a + CAS<Object>
{
fn root_commit(&self) -> Commit<'a, C> {
// ...
}
fn get_commit(&self, hash: Hash) -> Result<Commit<'a, C>, String> {
// ...
}
}
The intent with the lifetime 'a
is that everything has a lifetime shorter than the content-addressible storage layer.
That layer uses interior mutability to allow liberal sharing of immutable references, with the result that anything needing access to the storage layer can find a pointer to it easily.
I won't bore you with the definitions of Commit<'a, C>
and its friends.
The issue is manifest just in this much exposition: the FS
trait references concrete type Commit<'a, C>
, which will preclude using a FakeCommit
struct there.
The lifetime annotations are also getting a bit laborious, but maybe that's a price I have to pay.
What is the most idiomatic way to approach this situation?
The best idea I can think of is to define traits for the secondary types like Commit
.
But this means that the FS
methods return Trait objects, leading to some lifetime issues (who owns those trait objects?).
Moreover, it means that I will use dynamic dispatch in production, even though there will only be one Commit type in use in production.