how can I create a sort of code, where I can just call
let saleor_app = SaleorApp::new(config);
let saleor_app.apl.get("10.1:3000/gql/")
where SaleorApp has a dyn Trait / Generic field, that allows me to call a function and have any one of different APLs/handlers doing the underlying work? It would get chosen via env variable. I know I need traits for this, but I'm unsure if SaleorApp should be
struct SaleorApp<A: APL> {
pub apl: A
}
Or
struct SaleorApp {
pub apl: Box<dyn APL>
}
The trait:
pub trait APL: Send + Sized + Sync + Clone + std::fmt::Debug {
fn get(&self, saleor_api_url: &str) -> impl Future<Output = Result<AuthData>> + Send;
fn set(&self, auth_data: AuthData) -> impl Future<Output = Result<()>> + Send;
fn delete(&self, saleor_api_url: &str) -> impl Future<Output = Result<()>> + Send;
}
Either way I can't get it to work, because of multiple reasons.
As for having a Box, I fall short because the trait APL
cannot be made into an object, as it returns impl Futures
, and async traits are maybe maybe not allowed(Apparently they got stabilized in 1.75, but they don't work for me). Boxing the whole return type of all functions in the trait is the only solution I found, but having all that happen on the heap doesn't sound very Rusty.
And when trying to make it with generics, I fall flat because of:
pub fn create_app<A: APL>(config: Config) -> anyhow::Result<SaleorApp<A>> {
use AplType::{Env, File, Redis};
SaleorApp {
apl: match config.apl {
Env => EnvApl {}?,
File => FileApl { path: "apls.txt" }?,
Redis => RedisApl::new(config.apl_url, config.app_api_base_url)?,
},
}
}
Gives error:
`match` arms have incompatible types
expected `RedisApl`, found `EnvApl` [E0308]
or
expected A, found RedisApl
How can I achieve this functionality? Thanks for tips :)
If you want to use generics to decide which implementation to use, then your decision on which to use has to be done at compile time, because Rust is going to emit code specific to the implementation you've chosen. Since you want to make this work based on the environment or a configuration option, you need to use a trait object (that is, dyn
with a Box
, Arc
, reference, or similar).
Now, it is true that you can't use impl
in a trait object because the compiler needs to know the type (and the size) of the object it's returning. If you use the async-trait
crate, it effectively implements this by boxing the return values, as you'd mentioned doing, just with some nicer syntax. You certainly can use the new async trait functionality in 1.75, but because I try to target older versions of Rust as well, I've just opted to use async-trait
.
It is true that many times it is nicer and more performant to make these choices statically by using generics instead of trait objects and it can be a little faster to avoid boxing code. However, in your case, there's not really much of an option if you want to adopt the approach you have, and choosing an implementation at runtime based on configuration is a valid and legitimate choice, so I don't see a huge problem with that approach.
The code would look a little like this:
struct SaleorApp {
pub apl: Box<dyn APL>
}
#[async_trait]
pub trait APL: Send + Sync + std::fmt::Debug {
async fn get(&self, saleor_api_url: &str) -> Result<AuthData>;
async fn set(&self, auth_data: AuthData) -> Result<()>;
async fn delete(&self, saleor_api_url: &str) -> Result<()>;
}
pub fn create_app(config: Config) -> anyhow::Result<SaleorApp> {
use AplType::{Env, File, Redis};
Ok(SaleorApp {
apl: match config.apl {
Env => Box::new(EnvApl {}),
File => Box::new(FileApl { path: "apls.txt" }),
Redis => Box::new(RedisApl{}),
},
})
}
Note that Clone
and Sized
cannot be implemented for trait objects because they require Sized
, and trait objects don't have a size known at compile time. Thus, I've omitted their inclusion above.