use std::sync::Arc;
use toasty::{Deferred, stmt::Path};
use topcoat::context::Cx;
use super::{Column, ColumnWidth, Includes};
use crate::{DeclarationErrorKind, policy::Ability, schema::OptionSource, toasty_compat::model};
pub struct RelationLens<M, R> {
includes: Includes<M>,
field: Result<(String, String), DeclarationErrorKind>,
read: fn(&M) -> &R,
}
impl<M, R> RelationLens<M, R>
where
M: toasty::schema::Model,
{
#[doc(hidden)]
pub fn new<T>(path: impl Into<Path<M, T>>, read: fn(&M) -> &R) -> Self {
let path = path.into();
let field = model::field::<M>(&model::ModelPath::of(&path)).map(|f| (f.name, f.label));
Self {
includes: Includes::new().with(path),
field,
read,
}
}
}
impl<M, R> RelationLens<M, R> {
pub fn includes(&self) -> &Includes<M> {
&self.includes
}
pub fn read<'a>(&self, record: &'a M) -> &'a R {
(self.read)(record)
}
pub(crate) fn field(&self) -> Result<(&str, &str), DeclarationErrorKind> {
self.field
.as_ref()
.map(|(name, label)| (name.as_str(), label.as_str()))
.map_err(Clone::clone)
}
}
impl<M, R> Clone for RelationLens<M, R> {
fn clone(&self) -> Self {
Self {
includes: self.includes.clone(),
field: self.field.clone(),
read: self.read,
}
}
}
impl<M, R> std::fmt::Debug for RelationLens<M, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("RelationLens").field(&self.includes).finish()
}
}
#[macro_export]
macro_rules! relation {
($($model:ident)::+ . $field:ident) => {
$crate::RelationLens::new(
<$($model)::+>::fields().$field(),
|record: &$($model)::+| &record.$field,
)
};
}
const UNLOADED: &str = "(unloaded)";
type Loaded<M, T> = Arc<dyn Fn(&M) -> Option<T> + Send + Sync>;
type LabelSource = (&'static str, fn(&Cx) -> bool);
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())
}
}
pub struct RelationColumn<M> {
relation: Declared<M>,
project: LoadedText<M>,
}
impl<M> RelationColumn<M>
where
M: toasty::schema::Model,
{
pub fn new<T>(
relation: RelationLens<M, impl ToOneRelation<T> + 'static>,
project: impl Fn(&T) -> String + Send + Sync + 'static,
) -> Self
where
M: 'static,
{
let read = relation.read;
Self {
relation: Declared::of(&relation, ColumnWidth::Wide),
project: Arc::new(move |_cx, row| {
read(row)
.loaded()
.map(|target| target.map(&project).unwrap_or_default())
}),
}
}
pub fn of<S>(relation: RelationLens<M, impl ToOneRelation<S::Model> + 'static>) -> Self
where
S: OptionSource,
M: 'static,
{
let read = relation.read;
Self {
relation: Declared {
source: Some((std::any::type_name::<S>(), S::available)),
..Declared::of(&relation, ColumnWidth::Wide)
},
project: Arc::new(move |cx, row| {
read(row).loaded().map(|target| {
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,
M: 'static,
{
let read = relation.read;
Self {
relation: Declared {
source: Some((std::any::type_name::<S>(), S::available)),
..Declared::of(&relation, ColumnWidth::Wide)
},
project: Arc::new(move |cx, row| {
let records = read(row);
(!records.is_unloaded()).then(|| {
records
.get()
.iter()
.filter(|record| S::allows(cx, Ability::View(record)))
.map(|record| S::label(cx, record))
.collect::<Vec<_>>()
.join(", ")
})
}),
}
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.relation.relabel(label.into());
self
}
pub fn width(mut self, width: ColumnWidth) -> Self {
self.relation.width = width;
self
}
}
impl<M> RelationColumn<M> {
fn loaded_text(&self, cx: &Cx, row: &M) -> Option<String> {
(self.project)(cx, row)
}
}
pub struct CountColumn<M> {
relation: Declared<M>,
count: Loaded<M, usize>,
}
impl<M> CountColumn<M>
where
M: toasty::schema::Model,
{
pub fn new<T>(relation: RelationLens<M, Deferred<Vec<T>>>) -> Self
where
T: toasty::schema::Model + 'static,
M: 'static,
{
let read = relation.read;
Self {
relation: Declared::of(&relation, ColumnWidth::Narrow),
count: Arc::new(move |row| {
let records = read(row);
(!records.is_unloaded()).then(|| records.get().len())
}),
}
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.relation.relabel(label.into());
self
}
pub fn width(mut self, width: ColumnWidth) -> Self {
self.relation.width = width;
self
}
}
impl<M> CountColumn<M> {
fn loaded_text(&self, _cx: &Cx, row: &M) -> Option<String> {
(self.count)(row).map(|count| count.to_string())
}
}
struct Declared<M> {
name: String,
label: Option<String>,
field_label: String,
misdeclared: Option<DeclarationErrorKind>,
includes: Includes<M>,
width: ColumnWidth,
source: Option<LabelSource>,
}
impl<M> Declared<M> {
fn of<R>(relation: &RelationLens<M, R>, width: ColumnWidth) -> Self {
let (name, field_label, misdeclared) = match relation.field() {
Ok((name, label)) => (name.to_string(), label.to_string(), None),
Err(error) => (String::new(), String::new(), Some(error)),
};
Self {
name,
label: None,
field_label,
misdeclared,
includes: relation.includes().clone(),
width,
source: None,
}
}
fn label(&self) -> &str {
self.label.as_deref().unwrap_or(&self.field_label)
}
fn relabel(&mut self, label: String) {
self.name = label.to_lowercase();
self.label = Some(label);
}
fn text(&self, loaded: Option<String>) -> String {
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())
}
}
impl<M> Clone for Declared<M> {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
label: self.label.clone(),
field_label: self.field_label.clone(),
misdeclared: self.misdeclared.clone(),
includes: self.includes.clone(),
width: self.width,
source: self.source,
}
}
}
macro_rules! relation_column {
($ty:ident, $read:ident) => {
impl<M> Column<M> for $ty<M>
where
M: toasty::schema::Model + Send + Sync + 'static,
{
fn name(&self) -> &str {
&self.relation.name
}
fn label(&self) -> &str {
self.relation.label()
}
fn text(&self, cx: &Cx, row: &M) -> String {
self.relation.text(self.loaded_text(cx, row))
}
fn column_width(&self) -> ColumnWidth {
self.relation.width
}
fn includes(&self) -> Includes<M> {
self.relation.includes.clone()
}
fn misdeclared(&self) -> Option<DeclarationErrorKind> {
self.relation.misdeclared.clone()
}
fn unavailable_source(&self, cx: &Cx) -> Option<&'static str> {
self.relation
.source
.filter(|(_, available)| !available(cx))
.map(|(source, _)| source)
}
}
impl<M> Clone for $ty<M> {
fn clone(&self) -> Self {
Self {
relation: self.relation.clone(),
$read: Arc::clone(&self.$read),
}
}
}
impl<M> std::fmt::Debug for $ty<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!($ty))
.field("name", &self.relation.name)
.field("label", &self.relation.label())
.field("width", &self.relation.width)
.finish_non_exhaustive()
}
}
};
}
relation_column!(RelationColumn, project);
relation_column!(CountColumn, count);
#[cfg(test)]
mod tests;