use std::sync::Arc;
use derive_where::derive_where;
use toasty::{Deferred, Executor};
use topcoat::{Result, context::Cx};
use super::{Column, ColumnToggle, ColumnWidth, Includes, Related};
use crate::{BoxFuture, DeclarationErrorKind, lens::Reads, schema::OptionSource};
mod lens;
mod restrict;
pub use lens::RelationLens;
use restrict::{Restrict, restrict_many, restrict_one, shows};
const UNLOADED: &str = "(unloaded)";
type Loaded<M, T> = Arc<dyn Fn(&Cx, &M) -> Option<T> + Send + Sync>;
type LabelSource = (&'static str, fn(&Cx) -> bool);
type Masked = Arc<dyn Fn(&Cx) -> bool + Send + Sync>;
type LoadedText<M> = Arc<dyn Fn(&Cx, &M) -> Option<String> + Send + Sync>;
pub trait ToOneRelation<T>: sealed::Sealed<T> {
#[doc(hidden)]
fn loaded(&self) -> Option<Option<&T>>;
}
mod sealed {
pub trait Sealed<T> {}
impl<T: toasty::schema::Model> Sealed<T> for toasty::Deferred<T> {}
impl<T: toasty::schema::Model> Sealed<T> for toasty::Deferred<Option<T>> {}
}
impl<T: toasty::schema::Model> ToOneRelation<T> for Deferred<T> {
fn loaded(&self) -> Option<Option<&T>> {
(!self.is_unloaded()).then(|| Some(self.get()))
}
}
impl<T: toasty::schema::Model> ToOneRelation<T> for Deferred<Option<T>> {
fn loaded(&self) -> Option<Option<&T>> {
(!self.is_unloaded()).then(|| self.get().as_ref())
}
}
#[derive_where(Clone, Debug)]
pub struct RelationColumn<M> {
relation: Declared<M>,
#[derive_where(skip(Debug))]
project: LoadedText<M>,
}
impl<M> RelationColumn<M>
where
M: toasty::schema::Model + Send + Sync + 'static,
{
pub fn new<S>(
relation: RelationLens<M, impl ToOneRelation<S::Model> + 'static>,
reads: impl Into<Reads<S::Model>>,
project: impl Fn(&S::Model) -> String + Send + Sync + 'static,
) -> Self
where
S: OptionSource,
{
let read = relation.read;
let reads = reads.into();
Self {
relation: Declared::of(&relation, ColumnWidth::Grow)
.scoped::<S>(restrict_one::<M, S, _>(relation.read))
.masked(Arc::new(move |cx| reads.reads_any(&S::hidden_fields(cx)))),
project: Arc::new(move |cx, row| {
read(row).loaded().map(|target| {
target
.filter(|target| shows::<S>(cx, target))
.map(&project)
.unwrap_or_default()
})
}),
}
}
pub fn of<S>(relation: RelationLens<M, impl ToOneRelation<S::Model> + 'static>) -> Self
where
S: OptionSource,
{
let read = relation.read;
Self {
relation: Declared::of(&relation, ColumnWidth::Grow)
.scoped::<S>(restrict_one::<M, S, _>(relation.read)),
project: Arc::new(move |cx, row| {
read(row).loaded().map(|target| {
target
.filter(|target| shows::<S>(cx, target))
.map(|target| S::label(cx, target))
.unwrap_or_default()
})
}),
}
}
pub fn list<S>(relation: RelationLens<M, Deferred<Vec<S::Model>>>) -> Self
where
S: OptionSource,
{
let read = relation.read;
Self {
relation: Declared::of(&relation, ColumnWidth::Grow)
.scoped::<S>(restrict_many::<M, S>(relation.read)),
project: Arc::new(move |cx, row| {
let records = read(row);
(!records.is_unloaded()).then(|| {
records
.get()
.iter()
.filter(|record| shows::<S>(cx, record))
.map(|record| S::label(cx, record))
.collect::<Vec<_>>()
.join(", ")
})
}),
}
}
pub fn unscoped<T>(
relation: RelationLens<M, impl ToOneRelation<T> + 'static>,
project: impl Fn(&T) -> String + Send + Sync + 'static,
) -> Self {
let read = relation.read;
Self {
relation: Declared::of(&relation, ColumnWidth::Grow),
project: Arc::new(move |_cx, row| {
read(row)
.loaded()
.map(|target| target.map(&project).unwrap_or_default())
}),
}
}
}
impl<M> RelationColumn<M> {
fn loaded_text(&self, cx: &Cx, row: &M) -> Option<String> {
(self.project)(cx, row)
}
}
#[derive_where(Clone, Debug)]
pub struct CountColumn<M> {
relation: Declared<M>,
#[derive_where(skip(Debug))]
count: Loaded<M, usize>,
}
impl<M> CountColumn<M>
where
M: toasty::schema::Model + Send + Sync + 'static,
{
pub fn of<S>(relation: RelationLens<M, Deferred<Vec<S::Model>>>) -> Self
where
S: OptionSource,
{
Self {
relation: Declared::of(&relation, ColumnWidth::Auto)
.scoped::<S>(restrict_many::<M, S>(relation.read))
.fieldless(),
count: count(relation.read, shows::<S>),
}
}
pub fn unscoped<T>(relation: RelationLens<M, Deferred<Vec<T>>>) -> Self
where
T: toasty::schema::Model + 'static,
{
Self {
relation: Declared::of(&relation, ColumnWidth::Auto).fieldless(),
count: count(relation.read, |_, _| true),
}
}
}
impl<M> CountColumn<M> {
fn loaded_text(&self, cx: &Cx, row: &M) -> Option<String> {
(self.count)(cx, row).map(|count| count.to_string())
}
}
fn count<M: 'static, T: 'static>(
read: fn(&M) -> &Deferred<Vec<T>>,
shown: fn(&Cx, &T) -> bool,
) -> Loaded<M, usize> {
Arc::new(move |cx, row| {
let records = read(row);
(!records.is_unloaded()).then(|| records.get().iter().filter(|r| shown(cx, r)).count())
})
}
#[derive_where(Clone, Debug)]
struct Declared<M> {
name: String,
label: Option<crate::Text>,
field_label: crate::Text,
misdeclared: Option<DeclarationErrorKind>,
includes: Includes<M>,
reads: crate::lens::Reads<M>,
width: ColumnWidth,
toggle: ColumnToggle,
source: Option<LabelSource>,
#[derive_where(skip(Debug))]
restrict: Option<Restrict<M>>,
related: Related,
#[derive_where(skip(Debug))]
masked: Option<Masked>,
}
impl<M> Declared<M> {
fn of<R>(relation: &RelationLens<M, R>, width: ColumnWidth) -> Self {
let (name, field_label, misdeclared) = match relation.field() {
Ok(name) => (name.to_string(), crate::Text::field(name), None),
Err(error) => (String::new(), crate::Text::from(""), Some(error)),
};
Self {
name,
label: None,
field_label,
misdeclared,
includes: relation.includes().clone(),
reads: relation.reads(),
width,
toggle: ColumnToggle::Fixed,
source: None,
restrict: None,
related: Related::Unmasked,
masked: None,
}
}
fn scoped<S: OptionSource>(self, restrict: Restrict<M>) -> Self {
Self {
source: Some((std::any::type_name::<S>(), S::available)),
restrict: Some(restrict),
related: Related::Source(std::any::TypeId::of::<S>()),
..self
}
}
fn masked(self, masked: Masked) -> Self {
Self {
masked: Some(masked),
..self
}
}
fn fieldless(self) -> Self {
Self {
related: Related::None,
..self
}
}
fn label(&self) -> crate::Text {
self.label
.clone()
.unwrap_or_else(|| self.field_label.clone())
}
fn relabel(&mut self, label: crate::Text) {
self.name = label.key();
self.label = Some(label);
}
fn text(&self, loaded: Option<String>) -> String {
if self.restrict.is_some() {
return loaded.unwrap_or_default();
}
debug_assert!(
loaded.is_some(),
"the `{}` column declares its include, but its row was loaded without it",
self.name
);
loaded.unwrap_or_else(|| UNLOADED.to_string())
}
}
mod shared;
#[cfg(test)]
mod tests;