use std::{any::TypeId, collections::HashMap, sync::Arc};
use topcoat::{
context::Cx,
router::{PageFn, RouteFn},
};
use super::{
Root,
actions::{
resource_bulk_action, resource_bulk_delete, resource_delete, resource_export,
resource_options, resource_row_action,
},
build::{ResourceCheck, check_resource, route_path, validate_route_segment},
detail::resource_view,
forms::{resource_create, resource_create_post, resource_edit, resource_edit_post},
list::resource_list,
pages::page_handler,
relations::{Child, ChildSearchFn, relation_search, relation_table},
search::{RelationRequest, RelationSearchFn, SearchFn, list_search},
};
use crate::{
DeclarationError, DeclarationErrorKind, Page, Site,
form::RecordForm,
navigation::NavigationItem,
resource::{Mounted, Mounts, Resource, ResourceDef, ScopeFn},
schema::declare_with,
table::{
ACTION_ROUTE_PARAM, ACTIONS_ROUTE_SEGMENT, BULK_DELETE_ROUTE_SEGMENT, CREATE_ROUTE_SEGMENT,
DASH_ROUTE_SEGMENT, DELETE_ROUTE_SEGMENT, EDIT_ROUTE_SEGMENT, RECORD_ROUTE_PARAM,
TableSignals,
},
};
pub(super) trait Registration: Send {
fn register(self: Box<Self>, registry: &mut Registry);
}
type Customize<R> = Box<dyn FnOnce(ResourceDef<R>) -> ResourceDef<R> + Send>;
pub(super) struct ResourceRegistration<R: Resource>(pub(super) Customize<R>);
pub(super) struct PageRegistration<P> {
pub(super) home: bool,
pub(super) _page: std::marker::PhantomData<fn() -> P>,
}
pub(super) struct Registry {
pub(super) prefix: String,
schema: Option<Arc<toasty_core::Schema>>,
slugs: Vec<String>,
pub(super) urls: HashMap<TypeId, String>,
pub(super) nav_items: Vec<NavigationItem>,
pub(super) pages: Vec<PageFn>,
pub(super) routes: Vec<RouteFn>,
pub(super) root: Option<Root>,
pub(super) mounts: Mounts,
pub(super) resources: Vec<Registered>,
pub(super) search: HashMap<String, SearchFn>,
pub(super) relation_search: HashMap<(String, String), RelationSearchFn>,
pub(super) children: HashMap<TypeId, Child>,
pub(super) errors: Vec<DeclarationError>,
}
pub(super) struct Registered {
resource: TypeId,
name: &'static str,
slug: String,
pub(super) check: ResourceCheck,
relations: Vec<Link>,
search: ChildSearchFn,
}
struct Link {
child: TypeId,
child_name: &'static str,
scope_of: ScopeFn,
misdeclared: Option<DeclarationErrorKind>,
}
impl Registry {
pub(super) fn new(prefix: String, schema: Option<Arc<toasty_core::Schema>>) -> Self {
Self {
prefix,
schema,
slugs: Vec::new(),
urls: HashMap::new(),
nav_items: Vec::new(),
pages: Vec::new(),
routes: Vec::new(),
root: None,
mounts: Mounts::default(),
resources: Vec::new(),
search: HashMap::new(),
relation_search: HashMap::new(),
children: HashMap::new(),
errors: Vec::new(),
}
}
fn claim_slug<T: 'static>(&mut self, item: &'static str, slug: &str) -> Option<String> {
let refused = if let Err(error) = validate_route_segment(item, slug) {
Some(error)
} else if RESERVED_SLUGS.contains(&slug) {
Some(DeclarationErrorKind::ReservedSlug {
slug: slug.to_string(),
})
} else if self.slugs.iter().any(|taken| taken == slug) {
Some(DeclarationErrorKind::DuplicateSlug {
slug: slug.to_string(),
})
} else {
None
};
if let Some(error) = refused {
self.errors
.push(DeclarationError::of::<T>(Site::Registration, error));
return None;
}
let url = format!("{}/{slug}", self.prefix);
self.slugs.push(slug.to_string());
self.urls.insert(TypeId::of::<T>(), url.clone());
Some(url)
}
fn page(&mut self, method: http::Method, url: &str, handler: topcoat::router::PageRenderFn) {
self.pages
.push(PageFn::new(method, route_path(url), handler));
}
fn route(&mut self, method: http::Method, url: &str, handler: topcoat::router::RouteHandlerFn) {
self.routes
.push(RouteFn::new(method, route_path(url), handler));
}
pub(super) fn link_relations(&mut self) {
let slugs: HashMap<TypeId, &str> = self
.resources
.iter()
.map(|registered| (registered.resource, registered.slug.as_str()))
.collect();
for parent in &self.resources {
let mut seen = Vec::new();
for link in &parent.relations {
let child = self
.resources
.iter()
.find(|registered| registered.resource == link.child);
let site = Site::Relation(
child.map_or_else(|| link.child_name.to_string(), |child| child.slug.clone()),
);
let mut refuse = |kind| {
self.errors.push(DeclarationError {
resource: Some(parent.name),
site: site.clone(),
kind,
});
};
if let Some(kind) = &link.misdeclared {
refuse(kind.clone());
}
let Some(child) = child else {
refuse(DeclarationErrorKind::UnregisteredRelation);
continue;
};
if seen.contains(&link.child) {
refuse(DeclarationErrorKind::DuplicateRelation);
continue;
}
seen.push(link.child);
let (scope_of, search) = (Arc::clone(&link.scope_of), child.search);
let handler: RelationSearchFn = Arc::new(
move |cx: &Cx, request: RelationRequest, signals: TableSignals| {
let scope = scope_of(&request.seed);
search(cx, request, signals, scope)
},
);
self.relation_search.insert(
(parent.slug.clone(), slugs[&link.child].to_string()),
handler,
);
}
}
}
}
const RESERVED_SLUGS: &[&str] = &["login", "logout"];
impl<R: Resource> Registration for ResourceRegistration<R> {
fn register(self: Box<Self>, registry: &mut Registry) {
if registry.mounts.contains(TypeId::of::<R>()) {
registry.errors.push(DeclarationError::of::<R>(
Site::Registration,
DeclarationErrorKind::DuplicateResource,
));
return;
}
let Self(customize) = *self;
let prefix = registry.prefix.clone();
let mounted = declare_with(registry.schema.clone(), || {
Mounted::new(customize(R::declare()), &prefix)
});
let Some(url) = registry.claim_slug::<R>("ResourceDef::slug", &mounted.slug) else {
return;
};
register_routes::<R>(registry, &url, !mounted.actions.entries().is_empty());
registry.search.insert(url.clone(), list_search::<R>);
if registry.root.is_none() {
registry.root = Some(Root::Redirect(url));
}
registry.nav_items.push(mounted.navigation.clone());
registry.children.insert(
TypeId::of::<R>(),
Child {
slug: mounted.slug.clone(),
plural_label: mounted.plural_label.clone(),
render: relation_table::<R>,
},
);
registry.resources.push(Registered {
resource: TypeId::of::<R>(),
name: std::any::type_name::<R>(),
slug: mounted.slug.clone(),
check: check_resource::<R>,
relations: mounted
.relations
.iter()
.map(|relation| Link {
child: relation.child,
child_name: relation.child_name,
scope_of: Arc::clone(&relation.scope_of),
misdeclared: relation.misdeclared.clone(),
})
.collect(),
search: relation_search::<R>,
});
registry.mounts.insert(Arc::new(mounted));
}
}
fn register_routes<R: Resource>(registry: &mut Registry, url: &str, has_actions: bool) {
use http::Method;
registry.page(Method::GET, url, resource_list::<R>);
registry.page(
Method::GET,
&format!("{url}/{RECORD_ROUTE_PARAM}"),
resource_view::<R>,
);
registry.page(
Method::POST,
&format!("{url}/{RECORD_ROUTE_PARAM}/{DELETE_ROUTE_SEGMENT}"),
resource_delete::<R>,
);
registry.page(
Method::POST,
&format!("{url}/{BULK_DELETE_ROUTE_SEGMENT}"),
resource_bulk_delete::<R>,
);
if has_actions {
registry.page(
Method::POST,
&format!(
"{url}/{RECORD_ROUTE_PARAM}/{DASH_ROUTE_SEGMENT}/{ACTIONS_ROUTE_SEGMENT}/{ACTION_ROUTE_PARAM}"
),
resource_row_action::<R>,
);
registry.page(
Method::POST,
&format!("{url}/{DASH_ROUTE_SEGMENT}/{ACTIONS_ROUTE_SEGMENT}/{ACTION_ROUTE_PARAM}"),
resource_bulk_action::<R>,
);
}
registry.route(Method::GET, &format!("{url}/export"), resource_export::<R>);
if <R::Form as RecordForm>::HAS_FORM {
let create_url = format!("{url}/{CREATE_ROUTE_SEGMENT}");
registry.page(Method::GET, &create_url, resource_create::<R>);
registry.page(Method::POST, &create_url, resource_create_post::<R>);
let edit_url = format!("{url}/{RECORD_ROUTE_PARAM}/{EDIT_ROUTE_SEGMENT}");
registry.page(Method::GET, &edit_url, resource_edit::<R>);
registry.page(Method::POST, &edit_url, resource_edit_post::<R>);
registry.route(
Method::GET,
&format!("{url}/options"),
resource_options::<R>,
);
}
}
impl<P: Page> Registration for PageRegistration<P> {
fn register(self: Box<Self>, registry: &mut Registry) {
let url = if self.home {
if matches!(registry.root, Some(Root::Home)) {
registry.errors.push(DeclarationError::of::<P>(
Site::Registration,
DeclarationErrorKind::SecondHome,
));
return;
}
registry.root = Some(Root::Home);
registry
.urls
.insert(TypeId::of::<P>(), registry.prefix.clone());
registry.prefix.clone()
} else {
let Some(url) = registry.claim_slug::<P>("Page::slug", &P::slug()) else {
return;
};
url
};
registry.page(http::Method::GET, &url, page_handler::<P>);
let item = P::navigation().resolved(&url);
if self.home {
registry.nav_items.insert(0, item);
} else {
registry.nav_items.push(item);
}
}
}
#[cfg(test)]
impl super::Panel {
pub(super) fn registered(self) -> Registry {
let mut registry = Registry::new(self.prefix, None);
for registration in self.registrations {
registration.register(&mut registry);
}
registry
}
}