use std::future::Future;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TenantKey(String);
impl TenantKey {
pub fn new(s: impl Into<String>) -> Self {
TenantKey(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Default)]
pub struct RouteContext {
tenant: Option<TenantKey>,
session_vars: Vec<(String, String)>,
user: Option<String>,
extensions: http::Extensions,
}
impl RouteContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_tenant(mut self, tenant: TenantKey) -> Self {
self.tenant = Some(tenant);
self
}
pub fn tenant(&self) -> Option<&TenantKey> {
self.tenant.as_ref()
}
pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
self.user = Some(user_id.into());
self
}
pub fn set_user(&mut self, user_id: impl Into<String>) {
self.user = Some(user_id.into());
}
pub fn user(&self) -> Option<&str> {
self.user.as_deref()
}
pub fn with_session_var(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.session_vars.push((name.into(), value.into()));
self
}
pub fn add_session_var(&mut self, name: impl Into<String>, value: impl Into<String>) {
self.session_vars.push((name.into(), value.into()));
}
pub fn session_vars(&self) -> &[(String, String)] {
&self.session_vars
}
pub fn insert<T: Clone + Send + Sync + 'static>(&mut self, value: T) {
self.extensions.insert(value);
}
pub fn get<T: Clone + Send + Sync + 'static>(&self) -> Option<&T> {
self.extensions.get::<T>()
}
}
tokio::task_local! {
static ROUTE_CONTEXT: Arc<RouteContext>;
}
pub fn current() -> Arc<RouteContext> {
ROUTE_CONTEXT
.try_with(|c| c.clone())
.unwrap_or_else(|_| Arc::new(RouteContext::default()))
}
pub fn current_user_id() -> Option<String> {
ROUTE_CONTEXT
.try_with(|c| c.user().map(str::to_string))
.unwrap_or(None)
}
pub async fn scope<F: Future>(ctx: RouteContext, fut: F) -> F::Output {
ROUTE_CONTEXT.scope(Arc::new(ctx), fut).await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn current_is_default_when_unset() {
assert!(current().tenant().is_none());
}
#[tokio::test]
async fn scope_sets_and_restores_context() {
let ctx = RouteContext::new().with_tenant(TenantKey::new("acme"));
scope(ctx, async {
assert_eq!(current().tenant().unwrap().as_str(), "acme");
})
.await;
assert!(current().tenant().is_none());
}
#[tokio::test]
async fn spawned_task_does_not_inherit_context() {
let ctx = RouteContext::new().with_tenant(TenantKey::new("acme"));
scope(ctx, async {
let handle = tokio::spawn(async { current().tenant().cloned() });
assert!(handle.await.unwrap().is_none());
})
.await;
}
#[tokio::test]
async fn session_vars_round_trip_through_scope() {
let ctx = RouteContext::new()
.with_session_var("app.user_id", "42")
.with_session_var("app.tenant_id", "acme");
scope(ctx, async {
let vars = current().session_vars().to_vec();
assert_eq!(
vars,
vec![
("app.user_id".to_string(), "42".to_string()),
("app.tenant_id".to_string(), "acme".to_string()),
]
);
})
.await;
assert!(current().session_vars().is_empty());
}
#[tokio::test]
async fn extensions_store_typed_values() {
#[derive(Clone, PartialEq, Debug)]
struct Region(&'static str);
let mut ctx = RouteContext::new();
ctx.insert(Region("eu"));
scope(ctx, async {
assert_eq!(current().get::<Region>(), Some(&Region("eu")));
})
.await;
}
}