use toasty::stmt::{Expr, IntoExpr, Path};
use topcoat::context::{Cx, try_app_context, try_request_context};
use crate::DeclarationErrorKind;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Membership {
pub tenant: uuid::Uuid,
pub name: String,
}
impl Membership {
pub fn new(tenant: uuid::Uuid, name: impl Into<String>) -> Self {
Self {
tenant,
name: name.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Tenant(pub uuid::Uuid);
pub fn tenant_id(cx: &Cx) -> Option<uuid::Uuid> {
if let Some(parts) = try_request_context::<http::request::Parts>(cx)
&& let Some(t) = parts.extensions.get::<Tenant>()
{
return Some(t.0);
}
if let Some(t) = try_request_context::<Tenant>(cx) {
return Some(t.0);
}
let TenantSource(session) = try_app_context::<TenantSource>(cx)?;
session(cx)
}
pub(crate) struct TenantSource(pub(crate) fn(&Cx) -> Option<uuid::Uuid>);
pub fn require_tenant(cx: &Cx) -> Result<uuid::Uuid, topcoat::Error> {
tenant_id(cx).ok_or_else(|| topcoat::router::error::forbidden().into())
}
pub struct Tenancy<M> {
scope: Scope,
_model: std::marker::PhantomData<fn() -> M>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TenantColumn {
pub(crate) index: usize,
pub(crate) name: String,
}
type TenantFilter = Box<dyn Fn(uuid::Uuid) -> Expr<bool> + Send + Sync>;
enum Scope {
None,
Column {
filter: TenantFilter,
field: Option<TenantColumn>,
},
Via {
filter: TenantFilter,
single: bool,
hop: Option<usize>,
},
}
impl<M: toasty::schema::Model + 'static> Tenancy<M> {
fn scoped(scope: Scope) -> Self {
Self {
scope,
_model: std::marker::PhantomData,
}
}
pub fn none() -> Self {
Self::scoped(Scope::None)
}
pub fn column<T>(lens: impl Into<Path<M, T>>) -> Self
where
T: Send + Sync + 'static,
M: Send + Sync,
uuid::Uuid: IntoExpr<T>,
{
let lens = lens.into();
let field = crate::schema::lens_field(lens.clone(), &M::schema())
.ok()
.map(|field| TenantColumn {
index: field.id.index,
name: field.name.app_unwrap().to_string(),
});
Self::scoped(Scope::Column {
filter: Box::new(move |tenant| lens.clone().eq(tenant)),
field,
})
}
pub fn via<T>(lens: impl Into<Path<M, T>>) -> Self
where
T: Send + Sync + 'static,
M: Send + Sync,
uuid::Uuid: IntoExpr<T>,
{
let lens = lens.into();
let hop = toasty_core::stmt::Path::from(lens.clone())
.projection
.as_slice()
.first()
.copied();
let single = crate::schema::lens_field(lens.clone(), &M::schema()).is_ok();
Self::scoped(Scope::Via {
filter: Box::new(move |tenant| lens.clone().eq(tenant)),
single,
hop,
})
}
pub fn is_scoped(&self) -> bool {
!matches!(self.scope, Scope::None)
}
pub(crate) fn filter(&self, tenant: uuid::Uuid) -> Option<Expr<bool>> {
match &self.scope {
Scope::None => None,
Scope::Column { filter, .. } | Scope::Via { filter, .. } => Some(filter(tenant)),
}
}
pub(crate) fn via_is_single(&self) -> Option<bool> {
match &self.scope {
Scope::Via { single, .. } => Some(*single),
Scope::None | Scope::Column { .. } => None,
}
}
pub(crate) fn via_hop(&self) -> Option<usize> {
match &self.scope {
Scope::Via { hop, .. } => *hop,
Scope::None | Scope::Column { .. } => None,
}
}
pub(crate) fn column_field(&self) -> Option<Result<&TenantColumn, DeclarationErrorKind>> {
match &self.scope {
Scope::Column { field, .. } => Some(
field
.as_ref()
.ok_or(DeclarationErrorKind::TenancyColumnNotAField),
),
Scope::None | Scope::Via { .. } => None,
}
}
}
impl<M: toasty::schema::Model + 'static> Default for Tenancy<M> {
fn default() -> Self {
Self::none()
}
}
#[cfg(test)]
mod tests;