Context
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.
Concretely
The first layer is a content-addressible storage layer, parameterized on a type to be stored, T. I have (with function bodies omitted, since the question is about the method types):
type Hash = String;
pub trait CAS<T> {
fn store(&self, value: &T) -> String;
fn retrieve(&self, hash: &Hash) -> Option<T>;
}
pub struct Storage<T: Encodable + Decodable> {}
impl<T: Encodable + Decodable> CAS<T> for Storage<T> {
fn store(&self, value: &T) -> Hash {}
fn retrieve(&self, hash: &Hash) -> Option<T> {}
}
pub struct FakeStorage<T: Encodable + Decodable> {}
impl<T: Encodable + Decodable> CAS<T> for FakeStorage<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 enum Object {}
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> 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, allowing lazy loading.
The Problem
The FS
trait references type Commit<'a, C>
, where that type is a part of the definition of the "real" filesystem. That will preclude using a FakeCommit
struct there.
Help?
What is the most idiomatic way to approach this situation?
I am told associated types will not work, because higher-order types are not supported.
One idea 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.
Perhaps the only viable option is to define Commit such that it does not contain any real implementation, and can thus be shared between the real and fake FS implementations.