use std::sync::Arc;
use toasty::stmt::{Expr, Path};
use topcoat::{context::Cx, icon::IconData};
use super::{Action, Actions, HeaderAction, HeaderActions, Relation, Resource};
use crate::{
DeclarationErrorKind, Lens,
detail::Detail,
form::FormScalar,
navigation::NavigationItem,
policy::{Ability, Deny, Policy},
schema::Schema,
table::{Table, contains_expr},
tenancy::Tenancy,
toasty_compat::model::{self, ModelPath},
};
pub struct ResourceDef<R: Resource> {
pub(crate) slug: Option<String>,
pub(crate) label: Option<String>,
pub(crate) plural_label: Option<String>,
pub(crate) icon: Option<IconData>,
pub(crate) navigation_order: Option<i32>,
pub(crate) navigation_group: Option<String>,
pub(crate) navigation: Option<NavigationItem>,
pub(crate) policy: Arc<dyn Policy<R::Model>>,
pub(crate) tenancy: Tenancy<R::Model>,
pub(crate) table: Option<Table<R::Model>>,
pub(crate) form: Option<Schema<R::Form>>,
pub(crate) detail: Option<Detail<R::Model>>,
pub(crate) record_title: Option<RecordTitle<R::Model>>,
pub(crate) public_link: Option<PublicLinkFn<R::Model>>,
pub(crate) relations: Vec<Relation<R::Model>>,
pub(crate) actions: Actions<R>,
pub(crate) header_actions: HeaderActions,
pub(crate) create_columns: Vec<Result<String, DeclarationErrorKind>>,
}
impl<R: Resource> Default for ResourceDef<R> {
fn default() -> Self {
Self {
slug: None,
label: None,
plural_label: None,
icon: None,
navigation_order: None,
navigation_group: None,
navigation: None,
policy: Arc::new(Deny),
tenancy: Tenancy::none(),
table: None,
form: None,
detail: None,
record_title: None,
public_link: None,
relations: Vec::new(),
actions: Actions::default(),
header_actions: HeaderActions::new(),
create_columns: Vec::new(),
}
}
}
impl<R: Resource> ResourceDef<R> {
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<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn plural_label(mut self, label: impl Into<String>) -> Self {
self.plural_label = Some(label.into());
self
}
#[must_use]
pub fn icon(mut self, icon: IconData) -> Self {
self.icon = Some(icon);
self
}
#[must_use]
pub fn navigation_order(mut self, order: i32) -> Self {
self.navigation_order = Some(order);
self
}
#[must_use]
pub fn navigation_group(mut self, group: impl Into<String>) -> Self {
self.navigation_group = Some(group.into());
self
}
#[must_use]
pub fn navigation(mut self, item: NavigationItem) -> Self {
self.navigation = Some(item);
self
}
#[must_use]
pub fn policy(mut self, policy: impl Policy<R::Model>) -> Self {
self.policy = Arc::new(policy);
self
}
#[must_use]
pub fn policy_fn(
self,
policy: impl Fn(&Cx, Ability<'_, R::Model>) -> bool + Send + Sync + 'static,
) -> Self {
self.policy(policy)
}
#[must_use]
pub fn tenancy(mut self, tenancy: Tenancy<R::Model>) -> Self {
self.tenancy = tenancy;
self
}
#[must_use]
pub fn table(mut self, table: Table<R::Model>) -> Self {
self.table = Some(table);
self
}
#[must_use]
pub fn form(mut self, form: Schema<R::Form>) -> Self {
self.form = Some(form);
self
}
#[must_use]
pub fn detail(mut self, detail: Detail<R::Model>) -> Self {
self.detail = Some(detail);
self
}
#[must_use]
pub fn record_title<T>(mut self, lens: Lens<R::Model, T>) -> Self
where
T: FormScalar + toasty::schema::Field<Inner = String> + Send + Sync + 'static,
{
self.record_title = Some(RecordTitle::new(lens));
self
}
#[must_use]
pub fn public_link(
mut self,
link: impl Fn(&Cx, &R::Model) -> Option<PublicLink> + Send + Sync + 'static,
) -> Self {
self.public_link = Some(Arc::new(link));
self
}
#[must_use]
pub fn relation(mut self, relation: Relation<R::Model>) -> Self {
self.relations.push(relation);
self
}
#[must_use]
pub fn action<A: Action<R>>(mut self) -> Self {
self.actions = self.actions.add::<A>();
self
}
#[must_use]
pub fn header_action<A: HeaderAction>(mut self) -> Self {
self.header_actions = self.header_actions.add::<A>();
self
}
#[must_use]
pub fn create_column<T>(mut self, path: impl Into<Path<R::Model, T>>) -> Self {
let path = ModelPath::of(&path.into());
self.create_columns
.push(model::field::<R::Model>(&path).map(|field| field.name));
self
}
}
pub(crate) struct RecordTitle<M> {
read: Box<dyn Fn(&M) -> String + Send + Sync>,
search: TitleSearch,
}
type TitleSearch = Box<dyn Fn(&str) -> Option<Expr<bool>> + Send + Sync>;
impl<M: toasty::schema::Model + Send + Sync + 'static> RecordTitle<M> {
fn new<T>(lens: Lens<M, T>) -> Self
where
T: FormScalar + toasty::schema::Field<Inner = String> + Send + Sync + 'static,
{
let path = lens.clone();
Self {
read: Box::new(move |record| lens.read(record).to_label()),
search: Box::new(move |term| contains_expr(&path, term)),
}
}
pub(crate) fn read(&self, record: &M) -> Option<String> {
Some((self.read)(record)).filter(|title| !title.trim().is_empty())
}
pub(crate) fn search_expr(&self, term: &str) -> Option<Expr<bool>> {
(self.search)(term)
}
}
#[derive(Debug, Clone)]
pub struct PublicLink {
pub url: String,
pub label: &'static str,
}
pub(crate) type PublicLinkFn<M> = Arc<dyn Fn(&Cx, &M) -> Option<PublicLink> + Send + Sync>;
impl<R: Resource> std::fmt::Debug for ResourceDef<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResourceDef")
.field("slug", &self.slug)
.field("label", &self.label)
.field("plural_label", &self.plural_label)
.field("relations", &self.relations)
.field("actions", &self.actions)
.field("header_actions", &self.header_actions)
.finish_non_exhaustive()
}
}