I have an application that extends over 3 crates: A crate that holds the abstract framework, another that holds one of a number of plugins selected as a cargo feature, and a third that contains a concrete implementation.
The problem is that the plugin determines the "Version" type throughout the application, and the implementation determines the Errors type throughout the application. To make the application plug-able across multiple plug-ins and across multiple implementations, I need the Errors type in the plugin to be generic, and I can't figure out how to do that.
In the minimal code below, I have hard coded the Plugin type Errors = MyThingErrors
to show something that works. But I need the type of Errors here to be generic, not concrete. I've tried all sorts of combinations of generic parameters, but can't get it to compile.
So, is there a trick? Am I pushing Rust generics too far? Is this a Problem XY example, Perhaps I should follow a different approach?
Any suggestions gratefully received.
Here is the working example:
use thiserror::Error;
// ----------------------------------------
// Abstract traits crate
trait Thing {
type Errors;
type Version;
fn plugin(&self) -> &Box<dyn Plugin<Errors = Self::Errors, Version = Self::Version>>;
fn foo(&self) -> Result<(), Self::Errors>;
}
trait Plugin {
type Errors;
type Version;
fn bar(&self) -> Result<(), Self::Errors>;
}
// ----------------------------------------
// plugin crate
#[derive(Error, Debug, PartialEq)]
enum PluginErrors {
#[error("First Plugin error")]
Error1,
}
struct PluginVersion {}
struct MyPlugin {}
impl Plugin for MyPlugin {
type Errors = MyThingErrors;
type Version = PluginVersion;
fn bar(&self) -> Result<(), Self::Errors> {
Err(MyThingErrors::PluginError(PluginErrors::Error1))
}
}
// ----------------------------------------
// concrete implementation crate
#[derive(Error, Debug, PartialEq)]
enum MyThingErrors {
#[error("First MyThing error")]
MTError1,
#[error("Plugin Error: {0}")]
PluginError(#[from] PluginErrors),
}
struct MyThing {
p: Box<dyn Plugin<Errors = MyThingErrors, Version = <MyThing as Thing>::Version>>,
}
impl Thing for MyThing {
type Version = PluginVersion;
type Errors = MyThingErrors;
fn plugin(&self) -> &Box<dyn Plugin<Version = Self::Version, Errors = Self::Errors>> {
&self.p
}
fn foo(&self) -> Result<(), Self::Errors> {
Err(MyThingErrors::MTError1)
}
}
fn main() {
let t = MyThing {
p: Box::new(MyPlugin {}),
};
if let Err(e1) = t.foo() {
assert_eq!(e1, MyThingErrors::MTError1);
}
if let Err(e2) = t.p.bar() {
assert_eq!(e2, MyThingErrors::PluginError(PluginErrors::Error1));
}
}
Another way to approach this is to provide a way to map error types on the plugins.
I'd do this by adding a MappedPlugin type to the traits crate
struct MappedPlugin<P,F> {
p: P,
f: F,
}
impl<P,F, E> Plugin for MappedPlugin<P,F>
where
P: Plugin,
F: Fn(P::Errors) -> E,
{
type Errors = E;
type Version = P::Version;
fn bar(&self) -> Result<(), Self::Errors> {
self.p.bar().map_err(&self.f)
}
}
Then wrapping and creating the created plugin in the main crate:
fn main() {
let f = |e:PluginErrors| -> MyThingErrors { MyThingErrors::PluginError(e) };
let t = MyThing {
p: Box::new(MappedPlugin{ p:MyPlugin {}, f:f }),
};
if let Err(e1) = t.foo() {
assert_eq!(e1, MyThingErrors::MTError1);
}
if let Err(e2) = t.p.bar() {
assert_eq!(e2, MyThingErrors::PluginError(PluginErrors::Error1));
}
}
You could add a simple function to do that wrapping for you so that this becomes MyPlugin{}.map_err(|e| MyThingErrors::PluginError)
.
The main crate still needs to know about the error types in the plugin crates.
A full working version can be seen here.