use std::{any::TypeId, future::Future, sync::Arc};
use topcoat::{
Result,
context::Cx,
icon::IconData,
view::{BoxView, View, ViewExt},
};
use super::{Resource, ResourceDef};
use crate::{
DeclarationError, DeclarationErrorKind, Site, Text,
naming::{derived_id, kebab_case, sentence_case, slug_case, type_stem},
};
pub trait RecordPage<R: Resource>: Sized + Send + Sync + 'static {
fn declare() -> RecordPageDef<R::Model> {
RecordPageDef::new()
}
fn render(cx: &Cx, record: R::Model) -> impl Future<Output = Result<impl View>> + Send;
}
type AccessFn<M> = dyn Fn(&Cx, &M) -> bool + Send + Sync;
pub struct RecordPageDef<M> {
slug: Option<String>,
label: Option<Text>,
icon: Option<IconData>,
access: Arc<AccessFn<M>>,
}
impl<M> RecordPageDef<M> {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn slug(mut self, slug: impl Into<String>) -> Self {
self.slug = Some(slug.into());
self
}
#[must_use]
pub fn label(mut self, label: impl Into<Text>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn icon(mut self, icon: IconData) -> Self {
self.icon = Some(icon);
self
}
#[must_use]
pub fn access(mut self, access: impl Fn(&Cx, &M) -> bool + Send + Sync + 'static) -> Self {
self.access = Arc::new(access);
self
}
}
impl<M> Default for RecordPageDef<M> {
fn default() -> Self {
Self {
slug: None,
label: None,
icon: None,
access: Arc::new(|_, _| true),
}
}
}
impl<M> std::fmt::Debug for RecordPageDef<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RecordPageDef")
.field("slug", &self.slug)
.field("label", &self.label)
.finish_non_exhaustive()
}
}
impl<R: Resource> ResourceDef<R> {
#[must_use]
pub fn record_page<P: RecordPage<R>>(mut self) -> Self {
self.record_pages.push(RecordPageEntry::new::<P>());
self
}
}
type RenderFn<M> = for<'a> fn(&'a Cx, M) -> crate::BoxFuture<'a, Result<BoxView<'a>>>;
pub(crate) struct RecordPageEntry<R: Resource> {
pub(crate) page: TypeId,
pub(crate) name: &'static str,
pub(crate) slug: String,
pub(crate) label: Text,
pub(crate) icon: Option<IconData>,
access: Arc<AccessFn<R::Model>>,
pub(crate) render: RenderFn<R::Model>,
}
impl<R: Resource> RecordPageEntry<R> {
fn new<P: RecordPage<R>>() -> Self {
let def = P::declare();
let stem = type_stem::<P>("Page");
let label = def.label.unwrap_or_else(|| {
let noun = sentence_case(stem);
Text::derived(derived_id("page", &slug_case(&noun)), noun)
});
Self {
page: TypeId::of::<P>(),
name: std::any::type_name::<P>(),
slug: def.slug.unwrap_or_else(|| kebab_case(stem)),
label,
icon: def.icon,
access: def.access,
render: render::<R, P>,
}
}
pub(crate) fn admits(&self, cx: &Cx, record: &R::Model) -> bool {
(self.access)(cx, record)
}
}
impl<R: Resource> std::fmt::Debug for RecordPageEntry<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RecordPageEntry")
.field("name", &self.name)
.field("slug", &self.slug)
.finish_non_exhaustive()
}
}
fn render<R: Resource, P: RecordPage<R>>(
cx: &Cx,
record: R::Model,
) -> crate::BoxFuture<'_, Result<BoxView<'_>>> {
Box::pin(async move { Ok(P::render(cx, record).await?.boxed()) })
}
pub(crate) const RESERVED_SEGMENTS: &[&str] = &["edit", "delete", "-"];
pub(crate) fn check<R: Resource>(
pages: &[RecordPageEntry<R>],
validate: impl Fn(&str) -> Option<DeclarationErrorKind>,
errors: &mut Vec<DeclarationError>,
) -> bool {
let before = errors.len();
for (index, page) in pages.iter().enumerate() {
let slug = page.slug.clone();
let kind = if let Some(kind) = validate(&slug) {
kind
} else if RESERVED_SEGMENTS.contains(&slug.as_str()) {
DeclarationErrorKind::ReservedSlug { slug: slug.clone() }
} else if pages[..index].iter().any(|earlier| earlier.slug == slug) {
DeclarationErrorKind::DuplicateSlug { slug: slug.clone() }
} else {
continue;
};
errors.push(DeclarationError::of::<R>(Site::RecordPage(slug), kind));
}
errors.len() == before
}