pagetop/src/core/extension/all.rs
Manuel Cillero 28f1eee391 ♻️ (pagetop): Separa handle_component() en 2 métodos
`Theme::handle_component()` se divide en `setup_component()` (síncrono,
muta por tipo con la macro homónima) y `render_component()` (async,
decide el renderizado con su propia macro). Aplica mismo criterio que ya
separa `Component::setup()` de `prepare()`.
2026-09-06 13:07:01 +02:00

143 lines
5.2 KiB
Rust

use crate::core::action::add_action;
use crate::core::extension::ExtensionRef;
use crate::core::theme::ThemeRef;
use crate::core::theme::all::THEMES;
use crate::web::Router;
use crate::{global, serve_static_files, trace, web};
use std::sync::OnceLock;
static EXTENSIONS: OnceLock<Vec<ExtensionRef>> = OnceLock::new();
// **< REGISTRO DE LAS EXTENSIONES >****************************************************************
pub fn register_extensions(root_extension: Option<ExtensionRef>) {
// Garantiza que ocurre sólo una vez cuando los tests se ejecutan en paralelo.
EXTENSIONS.get_or_init(|| {
let mut list: Vec<ExtensionRef> = Vec::new();
// Primero añade el tema básico a la lista de extensiones habilitadas.
add_to_enabled(&mut list, &crate::base::theme::Basic);
// Si se proporciona la extensión raíz inicial, se añade a las extensiones habilitadas.
if let Some(extension) = root_extension {
add_to_enabled(&mut list, extension);
}
// Añade la página de bienvenida si no hay extensión raíz.
if root_extension.is_none() {
add_to_enabled(&mut list, &crate::base::extension::Welcome);
}
list
});
}
fn add_to_enabled(list: &mut Vec<ExtensionRef>, extension: ExtensionRef) {
// Verifica que la extensión no esté en la lista para evitar duplicados.
if !list.iter().any(|e| e.type_id() == extension.type_id()) {
// Añade primero (en orden inverso) las dependencias de la extensión.
for d in extension.dependencies().into_iter().rev() {
add_to_enabled(list, d);
}
// Añade la propia extensión a la lista.
list.push(extension);
// Comprueba si la extensión tiene un tema asociado que deba registrarse.
if let Some(theme) = extension.theme() {
check_theme_parent_chain(theme);
let mut registered_themes = THEMES.write();
// Asegura que el tema no esté ya registrado para evitar duplicados.
if !registered_themes
.iter()
.any(|t| t.type_id() == theme.type_id())
{
registered_themes.push(theme);
trace::debug!("Enabling \"{}\" theme", theme.short_name());
}
} else {
trace::debug!("Enabling \"{}\" extension", extension.short_name());
}
}
}
// Recorre la cadena de `Theme::parent()` para detectar referencias circulares. `parent()` se
// resuelve en tiempo de ejecución, así que un ciclo no puede descartarse al compilar. Se rechaza el
// arranque si detecta uno, antes de provocar un bucle infinito en el `ComponentRender::render()` o
// un desbordamiento de pila en los métodos predefinidos de `Theme` que delegan recursivamente en el
// tema padre.
fn check_theme_parent_chain(theme: ThemeRef) {
let mut chain: Vec<ThemeRef> = vec![theme];
let mut current = theme;
while let Some(parent) = current.parent() {
if let Some(pos) = chain.iter().position(|t| t.type_id() == parent.type_id()) {
let cycle: Vec<&str> = chain[pos..]
.iter()
.map(|t| t.short_name())
.chain(std::iter::once(parent.short_name()))
.collect();
panic!(
"Circular theme parent chain detected: {}",
cycle.join(" -> ")
);
}
chain.push(parent);
current = parent;
}
}
// **< REGISTRO DE LAS ACCIONES >*******************************************************************
pub fn register_actions() {
for extension in EXTENSIONS.get().into_iter().flatten() {
for a in extension.actions() {
add_action(a);
}
}
}
// **< INICIALIZA LAS EXTENSIONES >*****************************************************************
pub async fn initialize_extensions() {
trace::info!("Calling application bootstrap");
for e in EXTENSIONS.get().into_iter().flatten() {
e.initialize().await;
}
}
// **< CONFIGURA LAS RUTAS >************************************************************************
pub fn configure_routes(router: Router) -> Router {
// Sólo compila durante el desarrollo, para evitar errores 400 en la traza de eventos.
#[cfg(debug_assertions)]
let router = router.route(
"/.well-known/appspecific/com.chrome.devtools.json",
web::get(|| async { web::http::StatusCode::NOT_FOUND }),
);
let router = EXTENSIONS
.get()
.into_iter()
.flatten()
.fold(router, |r, e| e.configure_router(r));
serve_static_files!(router, [&global::SETTINGS.dev.pagetop_static_dir, assets] => "/pagetop");
router
}
// **< CONFIGURA EL MIDDLEWARE GLOBAL >*************************************************************
/// Aplica las capas de middleware globales de todas las extensiones sobre el router ya enrutado.
///
/// Se llama después de [`configure_routes`] para garantizar que las capas envuelven todas las
/// rutas de la aplicación, independientemente del orden de las extensiones.
pub fn configure_middleware(router: Router) -> Router {
EXTENSIONS
.get()
.into_iter()
.flatten()
.fold(router, |r, e| e.configure_middleware(r))
}