Setup -
Service
trait that has multiple implementationsContext
type that adds a context to anyService
ContextedService
trait that provides awith_context()
method that returns aGuard
type that captures the context within it.Guard
object implements theService
trait - ie,Guard
acts as the Service itself, but scoped within the context.- Need to independently create both Service and Context objects and associate each other using
ContextedService
trait. - Have a design for it, but it fails due to lifetime issues / trait boundary definitions when service and context objects are created across function boundaries.
Below is what i came up with, but it has the issue mentioned above.
// --- Service --- //
trait Service {
async fn get(&self, prefix: String) -> String;
}
// MyService is an implementation for Service trait. There can be many impls for ServiceTrait
struct MyService {
name: String,
}
impl Service for MyService {
async fn get(&self, prefix: String) -> String {
format!("{prefix} works")
}
}
// --- Context && ContextGuard --- //
// Context captures a scope within application logic
struct Context {
scope: String,
}
// ContextGuard captures references to Service and Context
struct ContextGuard<'svc, 'ctx, S>
where
S: Service,
{
service: &'svc S,
context: &'ctx Context,
}
// Overall idea of ContextedService is to prefix-inline a context before sending it to underlying service
impl<'svc, 'ctx, S> Service for ContextGuard<'svc, 'ctx, S>
where
S: Service,
{
async fn get(&self, prefix: String) -> String {
let contexted_prefix = format!("{}::{}", self.context.scope, prefix);
self.service.get(contexted_prefix).await
}
}
// --- ContextedService --- //
// ContextedService implements Service trait and the implementation is bounded within the lifetimes of both Service and Context params
trait ContextedService {
// Guard captures the underlying service and "scopes" (or decorates) it within context's lifetime
type Guard<'svc, 'ctx, S>
where
S: 'svc + Service,
Self: 'svc + Service;
// Creates the guard object capturing Service (Self) and Context by reference
fn with_context<'svc, 'ctx>(
&'svc self,
context: &'ctx Context,
) -> Self::Guard<'svc, 'ctx, Self>
where
Self: Service + Sized;
}
impl ContextedService for MyService {
type Guard<'svc, 'ctx, S> = ContextGuard<'svc, 'ctx, S>
where
S: 'svc + Service,
Self: 'svc + Service;
fn with_context<'svc, 'ctx>(
&'svc self,
context: &'ctx Context,
) -> ContextGuard<'svc, 'ctx, MyService>
where
Self: Service,
{
ContextGuard {
service: self,
context,
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let s = MyService { name: "testing_service".to_string() };
println!("{}", s.get("test".to_string()).await);
let context = Context { scope: "scoped".to_string() };
let cs = s.with_context(&context);
println!("{}", cs.get("test".to_string()).await);
call_contexted_from_inside(&s).await;
Ok(())
}
// This fn fails to compile
async fn call_contexted_from_inside<'svc, 'ctx, S>(service: &'svc S)
where
S: Service + ContextedService,
<S as ContextedService>::Guard<'svc, 'ctx, S>: Service,
{
let context = Context { scope: "scoped_in_fn".to_string() };
let cs = service.with_context(&context);
println!("{}", cs.get("test_in_fn".to_string()).await);
}
Overall, what i am looking for is to have a type (eg: ContextedService
) that captures references to both an actual Service
and a Context
, and use the type as if it is a Service
, but scoped within a context. What would be the best way to achieve this?