diff --git a/src/app.rs b/src/app.rs index b02b294..0bcece8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,6 +2,7 @@ mod figfont; +use crate::core::{extension, extension::ExtensionRef}; use crate::{global, locale, service, trace}; use substring::Substring; @@ -9,6 +10,12 @@ use substring::Substring; use std::io::Error; use std::sync::LazyLock; +/// Punto de entrada de una aplicación `PageTop`. +/// +/// No almacena datos, pero **encapsula** el ciclo completo de configuración y puesta en marcha. +/// Para instanciarla se puede usar [`new`](Application::new) o [`prepare`](Application::prepare). +/// Después sólo hay que llamar a [`run`](Application::run) (o a [`test`](Application::test) si se +/// está preparando un entorno de pruebas). pub struct Application; impl Default for Application { @@ -20,6 +27,23 @@ impl Default for Application { impl Application { /// Crea una instancia de la aplicación. pub fn new() -> Self { + Self::internal_prepare(None) + } + + /// Prepara una instancia de la aplicación a partir de una extensión raíz. + /// + /// Esa extensión suele declarar: + /// + /// - Sus propias dependencias (que se habilitarán automáticamente). + /// - Una lista de extensiones que deben deshabilitarse si estuvieran activadas. + /// + /// Esto simplifica el arranque en escenarios complejos. + pub fn prepare(root_extension: ExtensionRef) -> Self { + Self::internal_prepare(Some(root_extension)) + } + + // Método interno para preparar la aplicación, opcionalmente con una extensión. + fn internal_prepare(root_extension: Option) -> Self { // Al arrancar muestra una cabecera para la aplicación. Self::show_banner(); @@ -29,6 +53,12 @@ impl Application { // Valida el identificador de idioma por defecto. LazyLock::force(&locale::DEFAULT_LANGID); + // Registra las extensiones de la aplicación. + extension::all::register_extensions(root_extension); + + // Inicializa las extensiones. + extension::all::initialize_extensions(); + Self } @@ -73,7 +103,10 @@ impl Application { } } - /// Ejecuta el servidor web de la aplicación. + /// Arranca el servidor web de la aplicación. + /// + /// Devuelve [`std::io::Error`] si el *socket* no puede enlazarse (por puerto en uso, permisos, + /// etc.). pub fn run(self) -> Result { // Prepara el servidor web. Ok(service::HttpServer::new(move || { @@ -112,6 +145,6 @@ impl Application { InitError = (), >, > { - service::App::new() + service::App::new().configure(extension::all::configure_services) } } diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..5aebd1f --- /dev/null +++ b/src/core.rs @@ -0,0 +1,205 @@ +//! Tipos y funciones esenciales para crear extensiones. + +use std::any::Any; + +/// Selector para identificar segmentos de la ruta de un tipo. +#[derive(Clone, Copy, Debug)] +pub enum TypeInfo { + /// Ruta completa tal y como la devuelve [`core::any::type_name`]. + FullName, + /// Último segmento de la ruta – por ejemplo `Vec` en lugar de `alloc::vec::Vec`. + ShortName, + /// Conserva todo **desde** `start` inclusive hasta el final. + NameFrom(isize), + /// Conserva todo **hasta e incluyendo** `end`. + NameTo(isize), + /// Conserva la subruta comprendida entre `start` y `end` (ambos inclusive). + PartialName(isize, isize), +} + +impl TypeInfo { + /// Devuelve el segmento solicitado de la ruta para el tipo `T`. + pub fn of(&self) -> &'static str { + let type_name = std::any::type_name::(); + match self { + TypeInfo::FullName => type_name, + TypeInfo::ShortName => Self::partial(type_name, -1, None), + TypeInfo::NameFrom(start) => Self::partial(type_name, *start, None), + TypeInfo::NameTo(end) => Self::partial(type_name, 0, Some(*end)), + TypeInfo::PartialName(start, end) => Self::partial(type_name, *start, Some(*end)), + } + } + + // Extrae un rango de segmentos de `type_name` (tokens separados por `::`). + // + // Los argumentos `start` y `end` identifican los índices de los segmentos teniendo en cuenta: + // + // * Los índices positivos cuentan **desde la izquierda**, empezando en `0`. + // * Los índices negativos cuentan **desde la derecha**, `-1` es el último. + // * Si `end` es `None`, el corte llega hasta el último segmento. + // * Si la selección resulta vacía por índices desordenados o segmento inexistente, se devuelve + // la cadena vacía. + // + // Ejemplos (con `type_name = "alloc::vec::Vec"`): + // + // | Llamada | Resultado | + // |------------------------------|--------------------------| + // | `partial(..., 0, None)` | `"alloc::vec::Vec"` | + // | `partial(..., 1, None)` | `"vec::Vec"` | + // | `partial(..., -1, None)` | `"Vec"` | + // | `partial(..., 0, Some(-2))` | `"alloc::vec"` | + // | `partial(..., -5, None)` | `"alloc::vec::Vec"` | + // + // La porción devuelta vive tanto como `'static` porque `type_name` es `'static` y sólo se + // presta. + fn partial(type_name: &'static str, start: isize, end: Option) -> &'static str { + let maxlen = type_name.len(); + + // Localiza los límites de cada segmento a nivel 0 de `<…>`. + let mut segments = Vec::new(); + let mut segment_start = 0; // Posición inicial del segmento actual. + let mut angle_brackets = 0; // Profundidad dentro de '<…>'. + let mut previous_char = '\0'; // Se inicializa a carácter nulo, no hay aún carácter previo. + + for (idx, c) in type_name.char_indices() { + match c { + ':' if angle_brackets == 0 => { + if previous_char == ':' { + if segment_start < idx - 1 { + segments.push((segment_start, idx - 1)); // No incluye último '::'. + } + segment_start = idx + 1; // Nuevo segmento tras '::'. + } + } + '<' => angle_brackets += 1, + '>' => angle_brackets -= 1, + _ => {} + } + previous_char = c; + } + + // Incluye el último segmento si lo hubiese. + if segment_start < maxlen { + segments.push((segment_start, maxlen)); + } + + // Calcula la posición inicial. + let start_pos = segments + .get(if start >= 0 { + start as usize + } else { + segments.len().saturating_sub(start.unsigned_abs()) + }) + .map_or(0, |&(s, _)| s); + + // Calcula la posición final. + let end_pos = segments + .get(if let Some(end) = end { + if end >= 0 { + end as usize + } else { + segments.len().saturating_sub(end.unsigned_abs()) + } + } else { + segments.len() - 1 + }) + .map_or(maxlen, |&(_, e)| e); + + // Devuelve la cadena parcial basada en las posiciones calculadas. + if start_pos >= end_pos { + return ""; + } + &type_name[start_pos..end_pos] + } +} + +/// Proporciona información de tipo en tiempo de ejecución y conversión dinámica de tipos. +/// +/// Este *trait* se implementa automáticamente para **todos** los tipos que implementen [`Any`], de +/// modo que basta con traer [`AnyInfo`] al ámbito (`use crate::AnyInfo;`) para disponer de estos +/// métodos adicionales, o usar el [`prelude`](crate::prelude) de `PageTop`. +/// +/// # Ejemplo +/// +/// ```rust +/// use pagetop::prelude::*; +/// +/// let n = 3u32; +/// assert_eq!(n.type_name(), "u32"); +/// ``` +pub trait AnyInfo: Any { + /// Devuelve el nombre totalmente cualificado del tipo. + fn type_name(&self) -> &'static str; + + /// Devuelve el nombre corto del tipo (último segmento del nombre). + fn short_name(&self) -> &'static str; + + /// Devuelve una referencia a `dyn Any` para la conversión dinámica de tipos. + fn as_any_ref(&self) -> &dyn Any; + + /// Devuelve una referencia mutable a `dyn Any` para la conversión dinámica de tipos. + fn as_any_mut(&mut self) -> &mut dyn Any; +} + +impl AnyInfo for T { + #[inline] + fn type_name(&self) -> &'static str { + TypeInfo::FullName.of::() + } + + #[inline] + fn short_name(&self) -> &'static str { + TypeInfo::ShortName.of::() + } + + #[inline] + fn as_any_ref(&self) -> &dyn Any { + self + } + + #[inline] + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +/// Extiende [`AnyInfo`] con utilidades de *downcasting* para conversión de tipos. +/// +/// Preferible a usar directamente `Any::downcast_ref` porque conserva el *trait bound* [`AnyInfo`], +/// lo que permite seguir llamando a `type_name`, etc. +pub trait AnyCast: AnyInfo { + /// Comprueba si la instancia subyacente es de tipo `T`. + #[inline] + fn is(&self) -> bool + where + T: AnyInfo, + { + self.as_any_ref().is::() + } + + /// Intenta hacer *downcast* de un objeto para obtener una referencia de tipo `T`. + #[inline] + #[must_use] + fn downcast_ref(&self) -> Option<&T> + where + T: AnyInfo, + { + self.as_any_ref().downcast_ref() + } + + /// Intenta hacer *downcast* de un objeto para obtener una referencia mutable de tipo `T`. + #[inline] + #[must_use] + fn downcast_mut(&mut self) -> Option<&mut T> + where + T: AnyInfo, + { + self.as_any_mut().downcast_mut() + } +} + +/// Implementación automática para cualquier tipo que ya cumpla [`AnyInfo`]. +impl AnyCast for T {} + +// Infraestructura para ampliar funcionalidades mediante extensiones. +pub mod extension; diff --git a/src/core/extension.rs b/src/core/extension.rs new file mode 100644 index 0000000..9eb3bd2 --- /dev/null +++ b/src/core/extension.rs @@ -0,0 +1,9 @@ +//! Infraestructura para ampliar funcionalidades mediante extensiones. +//! +//! Cada funcionalidad adicional que quiera incorporarse a una aplicación `PageTop` se debe modelar +//! como una **extensión**. Todas comparten la misma interfaz declarada en [`ExtensionTrait`]. + +mod definition; +pub use definition::{ExtensionRef, ExtensionTrait}; + +pub(crate) mod all; diff --git a/src/core/extension/all.rs b/src/core/extension/all.rs new file mode 100644 index 0000000..d5bebda --- /dev/null +++ b/src/core/extension/all.rs @@ -0,0 +1,104 @@ +use crate::core::extension::ExtensionRef; +use crate::{service, trace}; + +use std::sync::{LazyLock, RwLock}; + +// EXTENSIONES ************************************************************************************* + +static ENABLED_EXTENSIONS: LazyLock>> = + LazyLock::new(|| RwLock::new(Vec::new())); + +static DROPPED_EXTENSIONS: LazyLock>> = + LazyLock::new(|| RwLock::new(Vec::new())); + +// REGISTRO DE LAS EXTENSIONES ********************************************************************* + +pub fn register_extensions(root_extension: Option) { + // Prepara la lista de extensiones habilitadas. + let mut enabled_list: Vec = Vec::new(); + + // Si se proporciona una extensión raíz inicial, se añade a la lista de extensiones habilitadas. + if let Some(extension) = root_extension { + add_to_enabled(&mut enabled_list, extension); + } + + // Guarda la lista final de extensiones habilitadas. + ENABLED_EXTENSIONS + .write() + .unwrap() + .append(&mut enabled_list); + + // Prepara una lista de extensiones deshabilitadas. + let mut dropped_list: Vec = Vec::new(); + + // Si se proporciona una extensión raíz, analiza su lista de dependencias. + if let Some(extension) = root_extension { + add_to_dropped(&mut dropped_list, extension); + } + + // Guarda la lista final de extensiones deshabilitadas. + DROPPED_EXTENSIONS + .write() + .unwrap() + .append(&mut dropped_list); +} + +fn add_to_enabled(list: &mut Vec, 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().iter().rev() { + add_to_enabled(list, *d); + } + + // Añade la propia extensión a la lista. + list.push(extension); + } +} + +fn add_to_dropped(list: &mut Vec, extension: ExtensionRef) { + // Recorre las extensiones que la actual recomienda deshabilitar. + for d in &extension.drop_extensions() { + // Verifica que la extensión no esté ya en la lista. + if !list.iter().any(|e| e.type_id() == d.type_id()) { + // Comprueba si la extensión está habilitada. Si es así, registra una advertencia. + if ENABLED_EXTENSIONS + .read() + .unwrap() + .iter() + .any(|e| e.type_id() == extension.type_id()) + { + trace::warn!( + "Trying to drop \"{}\" extension which is enabled", + extension.short_name() + ); + } else { + // Si la extensión no está habilitada, se añade a la lista y registra la acción. + list.push(*d); + trace::debug!("Extension \"{}\" dropped", d.short_name()); + // Añade recursivamente las dependencias de la extensión eliminada. + // De este modo, todas las dependencias se tienen en cuenta para ser deshabilitadas. + for dependency in &extension.dependencies() { + add_to_dropped(list, *dependency); + } + } + } + } +} + +// INICIALIZA LAS EXTENSIONES ********************************************************************** + +pub fn initialize_extensions() { + trace::info!("Calling application bootstrap"); + for extension in ENABLED_EXTENSIONS.read().unwrap().iter() { + extension.initialize(); + } +} + +// CONFIGURA LOS SERVICIOS ************************************************************************* + +pub fn configure_services(scfg: &mut service::web::ServiceConfig) { + for extension in ENABLED_EXTENSIONS.read().unwrap().iter() { + extension.configure_service(scfg); + } +} diff --git a/src/core/extension/definition.rs b/src/core/extension/definition.rs new file mode 100644 index 0000000..761fcee --- /dev/null +++ b/src/core/extension/definition.rs @@ -0,0 +1,77 @@ +use crate::core::AnyInfo; +use crate::locale::L10n; +use crate::service; + +/// Representa una referencia a una extensión. +/// +/// Las extensiones se definen como instancias estáticas globales para poder acceder a ellas desde +/// cualquier hilo de la ejecución sin necesidad de sincronización adicional. +pub type ExtensionRef = &'static dyn ExtensionTrait; + +/// Interfaz común que debe implementar cualquier extensión de `PageTop`. +/// +/// Este *trait* es fácil de implementar, basta con declarar la estructura de la extensión y +/// sobreescribir los métodos que sea necesario. +/// +/// ```rust +/// use pagetop::prelude::*; +/// +/// pub struct Blog; +/// +/// impl ExtensionTrait for Blog { +/// fn name(&self) -> L10n { L10n::n("Blog") } +/// fn description(&self) -> L10n { L10n::n("Sistema de blogs") } +/// } +/// ``` +pub trait ExtensionTrait: AnyInfo + Send + Sync { + /// Nombre legible para el usuario. + /// + /// Predeterminado por el [`short_name`](AnyInfo::short_name) del tipo asociado a la extensión. + fn name(&self) -> L10n { + L10n::n(self.short_name()) + } + + /// Descripción corta para paneles, listados, etc. + fn description(&self) -> L10n { + L10n::default() + } + + /// Otras extensiones que deben habilitarse **antes** de esta. + /// + /// `PageTop` las resolverá automáticamente respetando el orden durante el arranque de la + /// aplicación. + fn dependencies(&self) -> Vec { + vec![] + } + + /// Inicializa la extensión durante la lógica de arranque de la aplicación. + /// + /// Se llama una sola vez, después de que todas las dependencias se han inicializado y antes de + /// aceptar cualquier petición HTTP. + fn initialize(&self) {} + + /// Configura los servicios web de la extensión, como rutas, *middleware*, acceso a ficheros + /// estáticos, etc., usando [`ServiceConfig`](crate::service::web::ServiceConfig). + /// + /// ```rust,ignore + /// use pagetop::prelude::*; + /// + /// pub struct ExtensionSample; + /// + /// impl ExtensionTrait for ExtensionSample { + /// fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { + /// scfg.route("/sample", web::get().to(route_sample)); + /// } + /// } + /// ``` + #[allow(unused_variables)] + fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {} + + /// Permite crear extensiones para deshabilitar y desinstalar los recursos de otras extensiones + /// utilizadas en versiones anteriores de la aplicación. + /// + /// Actualmente no se usa, pero se deja como *placeholder* para futuras implementaciones. + fn drop_extensions(&self) -> Vec { + vec![] + } +} diff --git a/src/lib.rs b/src/lib.rs index 1ae3589..04a9939 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,6 +74,8 @@ pub mod html; pub mod locale; // Soporte a fechas y horas. pub mod datetime; +// Tipos y funciones esenciales para crear extensiones. +pub mod core; // Gestión del servidor y servicios web. pub mod service; // Prepara y ejecuta la aplicación. diff --git a/src/prelude.rs b/src/prelude.rs index e4aabf5..c391de8 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -33,4 +33,8 @@ pub use crate::datetime::*; pub use crate::service; +pub use crate::core::{AnyCast, AnyInfo, TypeInfo}; + +pub use crate::core::extension::*; + pub use crate::app::Application;